/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.misc.survey; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.LineMetrics; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.ResourceBundleFactory; import org.pentaho.reporting.engine.classic.core.imagemap.ImageMap; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.StyleSheet; import org.pentaho.reporting.engine.classic.core.style.TextStyleKeys; import org.pentaho.reporting.engine.classic.core.util.ReportDrawable; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.serializer.SerializerHelper; /** * Draws a survey scale. By implementing the Drawable interface, instances can be displayed within a report using * elements that use the {@link org.pentaho.reporting.engine.classic.core.filter.types.ContentType} class. * * @author David Gilbert */ public class SurveyScale implements ReportDrawable, Serializable { private static final Number[] EMPTY_VALUES = new Number[0]; /** * The lowest response value on the scale. */ private int lowest; /** * The highest response value on the scale. */ private int highest; /** * The lower margin. */ private double lowerMargin = 0.10; /** * The upper margin. */ private double upperMargin = 0.10; /** * A list of flags that control whether or not the shapes are filled. */ private ArrayList<Boolean> fillShapes; /** * The values to display. */ private Number[] values; /** * The lower bound of the highlighted range. */ private Number rangeLowerBound; /** * The upper bound of the highlighted range. */ private Number rangeUpperBound; /** * Draw the tick marks? */ private boolean drawTickMarks; /** * Draw the scale values. */ private boolean drawScaleValues; /** * The font used to display the scale values. */ private Font scaleValueFont; /** * The paint used to draw the scale values. */ private transient Paint scaleValuePaint; /** * The range paint. */ private transient Paint rangePaint; /** * The shapes to display. */ private transient ArrayList<Shape> shapes; /** * The fill paint. */ private transient Paint fillPaint; /** * The outline stroke for the shapes. */ private transient Stroke outlineStroke; /** * The default shape, if no shape is defined in the shapeList for the given value. */ private transient Shape defaultShape; /** * The tick mark paint. */ private transient Paint tickMarkPaint; private transient Paint borderPaint; private int range; private double lowerBound; private double upperBound; private boolean useFontMetricsGetStringBounds; private transient StyleSheet styleSheet; private boolean autoConfigure; /** * Creates a new default instance. */ public SurveyScale() { this( 1, 5, SurveyScale.EMPTY_VALUES ); } /** * Creates a new instance. * * @param lowest * the lowest response value on the scale. * @param highest * the highest response value on the scale. * @param values * the values to display. */ public SurveyScale( final int lowest, final int highest, final Number[] values ) { final String configFontMetricsStringBounds = ClassicEngineBoot.getInstance().getGlobalConfig().getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.misc.survey.UseFontMetricsGetStringBounds", "auto" ); if ( "auto".equals( configFontMetricsStringBounds ) ) { useFontMetricsGetStringBounds = ( ObjectUtilities.isJDK14() == true ); } else { useFontMetricsGetStringBounds = "true".equals( configFontMetricsStringBounds ); } this.lowest = lowest; this.highest = highest; if ( values == null ) { this.values = SurveyScale.EMPTY_VALUES; } else { this.values = values.clone(); } this.drawTickMarks = true; this.tickMarkPaint = Color.gray; this.scaleValuePaint = Color.black; this.defaultShape = new Ellipse2D.Double( -3.0, -3.0, 6.0, 6.0 ); this.rangeLowerBound = null; this.rangeUpperBound = null; this.rangePaint = Color.LIGHT_GRAY; this.shapes = createShapeList(); this.fillShapes = new ArrayList<Boolean>(); this.fillShapes.add( 0, Boolean.TRUE ); this.fillPaint = Color.BLACK; this.outlineStroke = new BasicStroke( 0.5f ); recompute(); } public boolean isAutoConfigure() { return autoConfigure; } public void setAutoConfigure( final boolean autoConfigure ) { this.autoConfigure = autoConfigure; recompute(); } public int getLowest() { return lowest; } public void setLowest( final int lowest ) { this.lowest = lowest; recompute(); } public int getHighest() { return highest; } public void setHighest( final int highest ) { this.highest = highest; recompute(); } /** * This method is called whenever lowest or highest has changed. It will recompute the range and upper and lower * bounds. */ protected void recompute() { this.range = Math.max( 0, this.highest - this.lowest ); this.lowerBound = this.lowest - ( range * this.lowerMargin ); this.upperBound = this.highest + ( range * this.upperMargin ); } protected int getRange() { return range; } protected void setRange( final int range ) { this.range = range; } protected double getLowerBound() { return lowerBound; } protected void setLowerBound( final double lowerBound ) { this.lowerBound = lowerBound; } protected double getUpperBound() { return upperBound; } protected void setUpperBound( final double upperBound ) { this.upperBound = upperBound; } /** * Creates the shape list used when drawing the scale. The list returned must contain exactly 6 elements. * * @return */ protected ArrayList<Shape> createShapeList() { final ArrayList<Shape> shapes = new ArrayList<Shape>(); shapes.add( new Ellipse2D.Double( -3.0, -3.0, 6.0, 6.0 ) ); shapes.add( SurveyScale.createDownTriangle( 4.0f ) ); shapes.add( SurveyScale.createUpTriangle( 4.0f ) ); shapes.add( SurveyScale.createDiamond( 4.0f ) ); shapes.add( new Rectangle2D.Double( -4.0, -4.0, 8.0, 8.0 ) ); shapes.add( new Ellipse2D.Double( -4.0, -4.0, 8.0, 8.0 ) ); return shapes; } /** * Creates a diamond shape. * * @param s * the size factor (equal to half the height of the diamond). * @return A diamond shape. */ public static Shape createDiamond( final float s ) { final GeneralPath p0 = new GeneralPath(); p0.moveTo( 0.0f, -s ); p0.lineTo( s, 0.0f ); p0.lineTo( 0.0f, s ); p0.lineTo( -s, 0.0f ); p0.closePath(); return p0; } /** * Creates a triangle shape that points upwards. * * @param s * the size factor (equal to half the height of the triangle). * @return A triangle shape. */ public static Shape createUpTriangle( final float s ) { final GeneralPath p0 = new GeneralPath(); p0.moveTo( 0.0f, -s ); p0.lineTo( s, s ); p0.lineTo( -s, s ); p0.closePath(); return p0; } /** * Creates a triangle shape that points downwards. * * @param s * the size factor (equal to half the height of the triangle). * @return A triangle shape. */ public static Shape createDownTriangle( final float s ) { final GeneralPath p0 = new GeneralPath(); p0.moveTo( 0.0f, s ); p0.lineTo( s, -s ); p0.lineTo( -s, -s ); p0.closePath(); return p0; } /** * Returns the lower bound of the highlighted range. A <code>null</code> value indicates that no range is set for * highlighting. * * @return The lower bound (possibly <code>null</code>). */ public Number getRangeLowerBound() { return this.rangeLowerBound; } /** * Sets the lower bound for the range that is highlighted on the scale. * * @param bound * the lower bound (<code>null</code> permitted). */ public void setRangeLowerBound( final Number bound ) { this.rangeLowerBound = bound; } /** * Returns the upper bound of the highlighted range. A <code>null</code> value indicates that no range is set for * highlighting. * * @return The upper bound (possibly <code>null</code>). */ public Number getRangeUpperBound() { return this.rangeUpperBound; } /** * Sets the upper bound for the range that is highlighted on the scale. * * @param bound * the upper bound (<code>null</code> permitted). */ public void setRangeUpperBound( final Number bound ) { this.rangeUpperBound = bound; } /** * Returns the flag that controls whether the tick marks are drawn. * * @return A boolean. */ public boolean isDrawTickMarks() { return this.drawTickMarks; } /** * Sets the flag that controls whether the tick marks are drawn. * * @param flag * a boolean. */ public void setDrawTickMarks( final boolean flag ) { this.drawTickMarks = flag; } /** * Returns a flag that controls whether or not scale values are drawn. * * @return a boolean. */ public boolean isDrawScaleValues() { return this.drawScaleValues; } /** * Sets a flag that controls whether or not scale values are drawn. * * @param flag * the flag. */ public void setDrawScaleValues( final boolean flag ) { this.drawScaleValues = flag; } /** * Returns the font used to display the scale values. * * @return A font (never <code>null</code>). */ public Font getScaleValueFont() { return this.scaleValueFont; } /** * Sets the font used to display the scale values. * * @param font * the font (<code>null</code> not permitted). */ public void setScaleValueFont( final Font font ) { this.scaleValueFont = font; } /** * Returns the color used to draw the scale values (if they are visible). * * @return A paint (never <code>null</code>). */ public Paint getScaleValuePaint() { return this.scaleValuePaint; } /** * Sets the color used to draw the scale values. * * @param paint * the paint (<code>null</code> not permitted). */ public void setScaleValuePaint( final Paint paint ) { if ( paint == null ) { throw new IllegalArgumentException( "Null 'paint' argument." ); //$NON-NLS-1$ } this.scaleValuePaint = paint; } /** * Returns the shape used to indicate the value of a response. * * @param index * the value index (zero-based). * @return The shape. */ public Shape getShape( final int index ) { if ( index < 0 ) { throw new IndexOutOfBoundsException(); } if ( index < shapes.size() ) { return this.shapes.get( index ); } return null; } /** * Sets the shape used to mark a particular value in the dataset. * * @param index * the value index (zero-based). * @param shape * the shape (<code>null</code> not permitted). */ public void setShape( final int index, final Shape shape ) { if ( index < 0 ) { throw new IndexOutOfBoundsException(); } if ( shapes.size() > index ) { this.shapes.set( index, shape ); } else { while ( shapes.size() < index ) { shapes.ensureCapacity( index ); shapes.add( null ); shapes.add( shape ); } } } /** * Sets the shape used to mark a particular value in the dataset. * * @param index * the value index (zero-based). * @param shape * the shape (<code>null</code> not permitted). */ public void setShape( final int index, final SurveyScaleShapeType shape ) { if ( index < 0 ) { throw new IndexOutOfBoundsException(); } if ( shapes.size() > index ) { this.shapes.set( index, shape.getShape() ); } else { while ( shapes.size() < index ) { shapes.ensureCapacity( index ); shapes.add( null ); shapes.add( shape.getShape() ); } } } /** * Returns a flag that controls whether the shape for a particular value should be filled. * * @param index * the value index (zero-based). * @return A boolean. */ public boolean isShapeFilled( final int index ) { if ( index < 0 ) { throw new IndexOutOfBoundsException(); } if ( index < fillShapes.size() ) { final Boolean b = this.fillShapes.get( index ); if ( b != null ) { return b.booleanValue(); } } return false; } /** * Sets the flag that controls whether the shape for a particular value should be filled. * * @param index * the value index (zero-based). * @param fill * the flag. */ public void setShapeFilled( final int index, final boolean fill ) { // noinspection ConditionalExpression this.fillShapes.set( index, fill ? Boolean.TRUE : Boolean.FALSE ); } /** * Returns the paint used to highlight the range. * * @return A {@link Paint} object (never <code>null</code>). */ public Paint getRangePaint() { return this.rangePaint; } /** * Sets the paint used to highlight the range (if one is specified). * * @param paint * the paint (<code>null</code> not permitted). */ public void setRangePaint( final Paint paint ) { if ( paint == null ) { throw new IllegalArgumentException( "Null 'paint' argument." ); //$NON-NLS-1$ } this.rangePaint = paint; } /** * Returns the default shape, which is used, if a shape for a certain value is not defined. * * @return the default shape, never null. */ public Shape getDefaultShape() { return defaultShape; } /** * Redefines the default shape. * * @param defaultShape * the default shape * @throws NullPointerException * if the given shape is null. */ public void setDefaultShape( final Shape defaultShape ) { if ( defaultShape == null ) { throw new NullPointerException( "The default shape must not be null." ); //$NON-NLS-1$ } this.defaultShape = defaultShape; } public void setDefaultShape( SurveyScaleShapeType shapeType ) { if ( shapeType == null ) { throw new NullPointerException( "The default shape must not be null." ); //$NON-NLS-1$ } setDefaultShape( shapeType.getShape() ); } public Paint getTickMarkPaint() { return tickMarkPaint; } public void setTickMarkPaint( final Paint tickMarkPaint ) { if ( tickMarkPaint == null ) { throw new NullPointerException(); } this.tickMarkPaint = tickMarkPaint; } public Number[] getValues() { return values.clone(); } public Paint getFillPaint() { return fillPaint; } public void setFillPaint( final Paint fillPaint ) { if ( fillPaint == null ) { throw new NullPointerException(); } this.fillPaint = fillPaint; } public Stroke getOutlineStroke() { return outlineStroke; } public void setOutlineStroke( final Stroke outlineStroke ) { if ( outlineStroke == null ) { throw new NullPointerException(); } this.outlineStroke = outlineStroke; } public double getUpperMargin() { return upperMargin; } public void setUpperMargin( final double upperMargin ) { this.upperMargin = upperMargin; } public double getLowerMargin() { return lowerMargin; } public void setLowerMargin( final double lowerMargin ) { this.lowerMargin = lowerMargin; } /** * Draws the survey scale. * * @param g2 * the graphics device. * @param area * the area. */ public void draw( final Graphics2D g2, final Rectangle2D area ) { drawRangeArea( area, g2 ); // draw tick marks... if ( isDrawTickMarks() ) { drawTickMarks( g2, area ); } // draw scale values... if ( isDrawScaleValues() ) { drawScaleValues( g2, area ); } drawValues( g2, area ); } protected void drawValues( final Graphics2D g2, final Rectangle2D area ) { // draw data values... final Number[] values = getValues(); if ( values.length == 0 ) { return; } final double y = area.getCenterY(); final Stroke outlineStroke = getOutlineStroke(); final Shape defaultShape = getDefaultShape(); g2.setPaint( getFillPaint() ); for ( int i = 0; i < values.length; i++ ) { final Number n = values[i]; if ( n == null ) { continue; } final double v = n.doubleValue(); final double x = valueToJava2D( v, area ); Shape valueShape = getShape( i ); if ( valueShape == null ) { valueShape = defaultShape; } if ( isShapeFilled( i ) ) { g2.translate( x, y ); g2.fill( valueShape ); g2.translate( -x, -y ); } else { g2.setStroke( outlineStroke ); g2.translate( x, y ); g2.draw( valueShape ); g2.translate( -x, -y ); } } } protected void drawScaleValues( final Graphics2D g2, final Rectangle2D area ) { g2.setPaint( getScaleValuePaint() ); final Font valueFont = getScaleValueFont(); if ( valueFont != null ) { g2.setFont( valueFont ); } else if ( styleSheet != null ) { final String fontName = (String) styleSheet.getStyleProperty( TextStyleKeys.FONT ); final int fontSize = styleSheet.getIntStyleProperty( TextStyleKeys.FONTSIZE, 10 ); final boolean bold = styleSheet.getBooleanStyleProperty( TextStyleKeys.BOLD ); final boolean italic = styleSheet.getBooleanStyleProperty( TextStyleKeys.ITALIC ); int style = 0; if ( bold ) { style |= Font.BOLD; } if ( italic ) { style |= Font.ITALIC; } g2.setFont( new Font( fontName, style, fontSize ) ); } final Font f = g2.getFont(); final FontMetrics fm = g2.getFontMetrics( f ); final FontRenderContext frc = g2.getFontRenderContext(); final double y = area.getCenterY(); final int highest = getHighest(); for ( int i = getLowest(); i <= highest; i++ ) { final double x = valueToJava2D( i, area ); final String text = String.valueOf( i ); final float width; if ( useFontMetricsGetStringBounds ) { final Rectangle2D bounds = fm.getStringBounds( text, g2 ); // getStringBounds() can return incorrect height for some Unicode // characters...see bug parade 6183356, let's replace it with // something correct width = (float) bounds.getWidth(); } else { width = fm.stringWidth( text ); } final LineMetrics metrics = f.getLineMetrics( text, frc ); final float descent = metrics.getDescent(); final float leading = metrics.getLeading(); final float yAdj = -descent - leading + (float) ( metrics.getHeight() / 2.0 ); final float xAdj = -width / 2.0f; g2.drawString( text, (float) ( x + xAdj ), (float) ( y + yAdj ) ); } } protected void drawTickMarks( final Graphics2D g2, final Rectangle2D area ) { g2.setPaint( getTickMarkPaint() ); g2.setStroke( new BasicStroke( 0.1f ) ); final int highest = getHighest(); for ( int i = getLowest(); i <= highest; i++ ) { for ( int j = 0; j < 10; j++ ) { final double xx = valueToJava2D( i + j / 10.0, area ); final Line2D mark = new Line2D.Double( xx, area.getCenterY() - 2.0, xx, area.getCenterY() + 2.0 ); g2.draw( mark ); } } final double xx = valueToJava2D( highest, area ); final Line2D mark = new Line2D.Double( xx, area.getCenterY() - 2.0, xx, area.getCenterY() + 2.0 ); g2.draw( mark ); } protected void drawRangeArea( final Rectangle2D area, final Graphics2D g2 ) { final Number rangeUpperBound = getRangeUpperBound(); final Number rangeLowerBound = getRangeLowerBound(); if ( rangeLowerBound == null || rangeUpperBound == null ) { return; } final double x0 = valueToJava2D( rangeLowerBound.doubleValue(), area ); final double x1 = valueToJava2D( rangeUpperBound.doubleValue(), area ); final Rectangle2D rangeArea = new Rectangle2D.Double( x0, area.getY(), ( x1 - x0 ), area.getHeight() ); g2.setPaint( getRangePaint() ); g2.fill( rangeArea ); } /** * Translates a data value to Java2D coordinates. * * @param value * the value. * @param area * the area. * @return The Java2D coordinate. */ private double valueToJava2D( final double value, final Rectangle2D area ) { final double upperBound = getUpperBound(); final double lowerBound = getLowerBound(); return area.getMinX() + ( ( value - lowerBound ) / ( upperBound - lowerBound ) * area.getWidth() ); } private void writeObject( final ObjectOutputStream out ) throws IOException { out.defaultWriteObject(); final SerializerHelper helper = SerializerHelper.getInstance(); helper.writeObject( scaleValuePaint, out ); helper.writeObject( rangePaint, out ); helper.writeObject( fillPaint, out ); helper.writeObject( outlineStroke, out ); helper.writeObject( defaultShape, out ); helper.writeObject( tickMarkPaint, out ); helper.writeObject( borderPaint, out ); final int size = shapes.size(); out.writeInt( size ); for ( int i = 0; i < size; i++ ) { final Shape s = shapes.get( i ); helper.writeObject( s, out ); } } private void readObject( final ObjectInputStream in ) throws IOException, ClassNotFoundException { in.defaultReadObject(); final SerializerHelper helper = SerializerHelper.getInstance(); scaleValuePaint = (Paint) helper.readObject( in ); rangePaint = (Paint) helper.readObject( in ); fillPaint = (Paint) helper.readObject( in ); outlineStroke = (Stroke) helper.readObject( in ); defaultShape = (Shape) helper.readObject( in ); tickMarkPaint = (Paint) helper.readObject( in ); borderPaint = (Paint) helper.readObject( in ); shapes = new ArrayList<Shape>(); final int size = in.readInt(); for ( int i = 0; i < size; i++ ) { final Shape s = (Shape) helper.readObject( in ); shapes.add( s ); } } /** * Provides the current report configuration of the current report process to the drawable. The report configuration * can be used to configure the drawing process through the report. * * @param config * the report configuration. */ public void setConfiguration( final Configuration config ) { } /** * Provides the computed stylesheet of the report element that contained this drawable. The stylesheet is immutable. * * @param style * the stylesheet. */ public void setStyleSheet( final StyleSheet style ) { this.styleSheet = style; if ( autoConfigure && this.styleSheet != null ) { this.scaleValuePaint = (Paint) style.getStyleProperty( ElementStyleKeys.PAINT, this.scaleValuePaint ); this.fillPaint = (Paint) style.getStyleProperty( ElementStyleKeys.FILL_COLOR, this.fillPaint ); final String fontName = (String) style.getStyleProperty( TextStyleKeys.FONT, "SansSerif" ); final boolean bold = style.getBooleanStyleProperty( TextStyleKeys.BOLD ); final boolean italics = style.getBooleanStyleProperty( TextStyleKeys.ITALIC ); final int size = style.getIntStyleProperty( TextStyleKeys.FONTSIZE, 10 ); int fontStyle = Font.PLAIN; if ( bold ) { fontStyle |= Font.BOLD; } if ( italics ) { fontStyle |= Font.ITALIC; } this.scaleValueFont = new Font( fontName, fontStyle, size ); } } public StyleSheet getStyleSheet() { return styleSheet; } /** * Defines the resource-bundle factory that can be used to localize the drawing process. * * @param bundleFactory * the resource-bundle factory. */ public void setResourceBundleFactory( final ResourceBundleFactory bundleFactory ) { } public ImageMap getImageMap( final Rectangle2D bounds ) { return null; } }