001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.legend;
034
035import java.awt.Shape;
036import java.util.ArrayList;
037import java.awt.Font;
038import java.awt.Graphics2D;
039import java.awt.Insets;
040import java.awt.BasicStroke;
041import java.awt.Color;
042import java.awt.geom.Dimension2D;
043import java.awt.geom.Rectangle2D;
044import java.util.List;
045import java.util.Map;
046import java.awt.FontMetrics;
047import java.text.DecimalFormat;
048import java.text.NumberFormat;
049
050import org.jfree.chart3d.Orientation;
051import org.jfree.chart3d.data.Range;
052import org.jfree.chart3d.graphics2d.Fit2D;
053import org.jfree.chart3d.graphics2d.TextAnchor;
054import org.jfree.chart3d.internal.Args;
055import org.jfree.chart3d.internal.TextUtils;
056import org.jfree.chart3d.renderer.ColorScale;
057import org.jfree.chart3d.table.AbstractTableElement;
058import org.jfree.chart3d.table.ElementDimension;
059import org.jfree.chart3d.table.TableElement;
060import org.jfree.chart3d.table.TableElementOnDraw;
061import org.jfree.chart3d.table.TableElementVisitor;
062
063/**
064 * A {@link TableElement} that displays a {@link ColorScale}.
065 * <br><br>
066 * NOTE: This class is serializable, but the serialization format is subject 
067 * to change in future releases and should not be relied upon for persisting 
068 * instances of this class. 
069 * 
070 * @since 1.1
071 */
072@SuppressWarnings("serial")
073public class ColorScaleElement extends AbstractTableElement 
074        implements TableElement {
075
076    /** The color scale. */
077    private final ColorScale scale;
078    
079    /** The orientation (horizontal or vertical). */
080    private final Orientation orientation;
081    
082    /** The length of the bar. */
083    private final double barLength;
084    
085    /** The width of the bar. */
086    private final double barWidth;
087    
088    /** The gap between the color scale bar and the text labels. */
089    private final double textOffset;
090    
091    /** The font for the text labels. */
092    private final Font font;
093    
094    /** The text color. */
095    private final Color textColor;
096    
097    /** The number formatter. */
098    private final NumberFormat formatter;
099    
100    /**
101     * Creates a new {@code ColorScaleElement} with the specified 
102     * attributes.
103     * 
104     * @param scale  the color scale ({@code null} not permitted).
105     * @param orientation  the orientation ({@code null} not permitted).
106     * @param barWidth  the bar width (in Java2D units).
107     * @param barLength  the bar length (in Java2D units).
108     * @param font  the font ({@code null} not permitted).
109     * @param textColor  the text color ({@code null} not permitted).
110     * 
111     * @since 1.2
112     */
113    public ColorScaleElement(ColorScale scale, Orientation orientation, 
114            double barWidth, double barLength, Font font, Color textColor) {
115        super();
116        Args.nullNotPermitted(scale, "scale");
117        Args.nullNotPermitted(orientation, "orientation");
118        Args.nullNotPermitted(font, "font");
119        this.scale = scale;
120        this.orientation = orientation;
121        this.barWidth = barWidth;
122        this.barLength = barLength;
123        this.textOffset = 2;
124        this.font = font;
125        this.textColor = textColor;
126        this.formatter = new DecimalFormat("0.00");
127    }
128    
129    /**
130     * Returns the color scale.
131     * 
132     * @return The color scale (never {@code null}). 
133     */
134    public ColorScale getColorScale() {
135        return this.scale;
136    }
137    
138    /**
139     * Returns the orientation.
140     * 
141     * @return The orientation (never {@code null}). 
142     */
143    public Orientation getOrientation() {
144        return this.orientation;
145    }
146    
147    /**
148     * Returns the bar width.
149     * 
150     * @return The bar width.
151     */
152    public double getBarWidth() {
153        return this.barWidth;
154    }
155    
156    /**
157     * Returns the bar length.
158     * 
159     * @return The bar length. 
160     */
161    public double getBarLength() {
162        return this.barLength;
163    }
164    
165    /**
166     * Returns the font used to display the labels on the color scale.
167     * 
168     * @return The font (never {@code null}). 
169     */
170    public Font getFont() {
171        return this.font;
172    }
173    
174    /**
175     * Returns the text color.
176     * 
177     * @return The text color (never {@code null}). 
178     */
179    public Color getTextColor() {
180        return this.textColor;
181    }
182    
183    /**
184     * Receives a visitor.  This is part of a general mechanism to perform
185     * operations on an arbitrary hierarchy of table elements.  You will not 
186     * normally call this method directly.
187     * 
188     * @param visitor  the visitor ({@code null} not permitted). 
189     * 
190     * @since 1.2
191     */
192    @Override
193    public void receive(TableElementVisitor visitor) {
194        visitor.visit(this);
195    }
196
197    /**
198     * Returns the preferred size for this element.
199     * 
200     * @param g2  the graphics target.
201     * @param bounds  the available drawing space.
202     * 
203     * @return The preferred size (never {@code null}). 
204     */
205    @Override
206    public Dimension2D preferredSize(Graphics2D g2, Rectangle2D bounds) {
207        return preferredSize(g2, bounds, null); 
208    }
209
210    /**
211     * Returns the preferred size for this element.
212     * 
213     * @param g2  the graphics target.
214     * @param bounds  the available drawing space.
215     * @param constraints  layout constraints (ignored here).
216     * 
217     * @return The preferred size (never {@code null}). 
218     */
219    @Override
220    public Dimension2D preferredSize(Graphics2D g2, Rectangle2D bounds, 
221            Map<String, Object> constraints) {
222        g2.setFont(this.font);
223        FontMetrics fm = g2.getFontMetrics();
224        Range r = this.scale.getRange();
225        String minStr = this.formatter.format(r.getMin());
226        String maxStr = this.formatter.format(r.getMax());
227        Rectangle2D minStrBounds = TextUtils.getTextBounds(minStr, fm);
228        Rectangle2D maxStrBounds = TextUtils.getTextBounds(maxStr, fm);
229        double maxStrWidth = Math.max(minStrBounds.getWidth(),
230                maxStrBounds.getWidth());
231        Insets insets = getInsets();
232        double w, h;
233        if (this.orientation == Orientation.HORIZONTAL) {
234            w = Math.min(this.barLength + insets.left + insets.right, 
235                bounds.getWidth());
236            h = Math.min(insets.top + this.barWidth + this.textOffset 
237                    + minStrBounds.getHeight() + insets.bottom,
238                bounds.getHeight());
239        } else {
240            w = Math.min(insets.left + this.barWidth + this.textOffset 
241                    + maxStrWidth + insets.right, bounds.getWidth());
242            h = Math.min(insets.top + this.barLength + this.textOffset 
243                    + minStrBounds.getHeight() + insets.bottom,
244                bounds.getHeight());
245           
246        }
247        return new ElementDimension(w, h);
248    }
249
250    @Override
251    public List<Rectangle2D> layoutElements(Graphics2D g2, Rectangle2D bounds, 
252            Map<String, Object> constraints) {
253        List<Rectangle2D> result = new ArrayList<>(1);
254        Dimension2D prefDim = preferredSize(g2, bounds);
255        Fit2D fitter = Fit2D.getNoScalingFitter(getRefPoint());
256        Rectangle2D dest = fitter.fit(prefDim, bounds);
257        result.add(dest);
258        return result;
259    }
260
261    /**
262     * Draws the element within the specified bounds.
263     * 
264     * @param g2  the graphics target ({@code null} not permitted).
265     * @param bounds  the bounds ({@code null} not permitted).
266     */
267    @Override
268    public void draw(Graphics2D g2, Rectangle2D bounds) {
269        draw(g2, bounds, null);
270    }
271    
272    /**
273     * Draws the element within the specified bounds.
274     * 
275     * @param g2  the graphics target ({@code null} not permitted).
276     * @param bounds  the bounds ({@code null} not permitted).
277     * @param onDrawHandler  receives notification before and after the element
278     *     is drawn ({@code null} permitted);
279     * 
280     * @since 1.3
281     */
282    @Override
283    public void draw(Graphics2D g2, Rectangle2D bounds, 
284            TableElementOnDraw onDrawHandler) {
285        
286        if (onDrawHandler != null) {
287            onDrawHandler.beforeDraw(this, g2, bounds);
288        }
289        
290        Shape savedClip = g2.getClip();
291        g2.clip(bounds);
292        List<Rectangle2D> layoutInfo = layoutElements(g2, bounds, null);
293        Rectangle2D dest = layoutInfo.get(0);        
294        if (getBackground() != null) {
295            getBackground().fill(g2, dest);
296        }
297        g2.setFont(this.font);
298        FontMetrics fm = g2.getFontMetrics();
299        Range r = this.scale.getRange();
300        String minStr = this.formatter.format(r.getMin());
301        String maxStr = this.formatter.format(r.getMax());
302        Rectangle2D minStrBounds = TextUtils.getTextBounds(minStr, fm);
303        Rectangle2D maxStrBounds = TextUtils.getTextBounds(maxStr, fm);
304        Insets insets = getInsets();
305        if (this.orientation == Orientation.HORIZONTAL) {
306            double x0 = dest.getX() + insets.left 
307                    + minStrBounds.getWidth() / 2.0;
308            double x1 = dest.getMaxX() - insets.right 
309                    - maxStrBounds.getWidth() / 2.0;
310            double y0 = dest.getY() + insets.top;
311            double y1 = y0 + this.barWidth;
312            
313            drawHorizontalScale(this.scale, g2, new Rectangle2D.Double(
314                    (int) x0, (int) y0, (int) (x1 - x0), (int) this.barWidth));
315            // fill the bar with the color scale
316            g2.setPaint(this.textColor);
317            TextUtils.drawAlignedString(minStr, g2, (float) x0, 
318                    (float) (y1 + this.textOffset), TextAnchor.TOP_CENTER);
319            TextUtils.drawAlignedString(maxStr, g2, (float) x1, 
320                    (float) (y1 + this.textOffset), TextAnchor.TOP_CENTER);
321            
322        } else { // VERTICAL
323            double maxStrWidth = Math.max(minStrBounds.getWidth(), 
324                    maxStrBounds.getWidth());
325            double x1 = dest.getMaxX() - insets.right - maxStrWidth 
326                    - this.textOffset;
327            double x0 = x1 - this.barWidth;
328            double y0 = dest.getY() + insets.top 
329                    + maxStrBounds.getHeight() / 2.0;
330            double y1 = y0 + this.barLength;            
331            
332            drawVerticalScale(this.scale, g2, new Rectangle2D.Double(
333                    (int) x0, (int) y0, (int) (x1 - x0), (int) this.barLength));
334            g2.setPaint(this.textColor);
335            TextUtils.drawAlignedString(minStr, g2, 
336                    (float) (x1 + this.textOffset), (float) y1, 
337                    TextAnchor.HALF_ASCENT_LEFT);
338            TextUtils.drawAlignedString(maxStr, g2, 
339                    (float) (x1 + this.textOffset), (float) y0, 
340                    TextAnchor.HALF_ASCENT_LEFT);
341        }
342        g2.setClip(savedClip);
343
344        if (onDrawHandler != null) {
345            onDrawHandler.afterDraw(this, g2, bounds);
346        }
347    }
348    
349    /**
350     * Draws the color scale horizontally within the specified bounds.
351     * 
352     * @param colorScale  the color scale.
353     * @param g2  the graphics target.
354     * @param bounds  the bounds.
355     */
356    private void drawHorizontalScale(ColorScale colorScale, Graphics2D g2, 
357            Rectangle2D bounds) {
358        g2.setStroke(new BasicStroke(1.0f));
359        for (int x = (int) bounds.getX(); x < bounds.getMaxX(); x++) {
360            double p = (x - bounds.getX()) / bounds.getWidth();
361            double value = colorScale.getRange().value(p);
362            g2.setColor(colorScale.valueToColor(value));
363            g2.drawLine(x, (int) bounds.getMinY(), x, (int) bounds.getMaxY());
364        }    
365    }
366    
367    /**
368     * Draws the color scale vertically within the specified bounds.
369     * 
370     * @param colorScale  the color scale.
371     * @param g2  the graphics target.
372     * @param bounds  the bounds.
373     */
374    private void drawVerticalScale(ColorScale colorScale, Graphics2D g2, 
375            Rectangle2D bounds) {
376        g2.setStroke(new BasicStroke(1.0f));
377        for (int y = (int) bounds.getY(); y < bounds.getMaxY(); y++) {
378            double p = (y - bounds.getY()) / bounds.getHeight();
379            double value = colorScale.getRange().value(1 - p);
380            g2.setColor(this.scale.valueToColor(value));
381            g2.drawLine((int) bounds.getX(), y, (int) bounds.getMaxX(), y);
382        }    
383    }
384
385    @Override
386    public boolean equals(Object obj) {
387        if (obj == this) {
388            return true;
389        }
390        if (!(obj instanceof ColorScaleElement)) {
391            return false;
392        }
393        ColorScaleElement that = (ColorScaleElement) obj;
394        if (!this.scale.equals(that.scale)) {
395            return false;
396        }
397        if (!this.orientation.equals(that.orientation)) {
398            return false;
399        }
400        if (this.barLength != that.barLength) {
401            return false;
402        }
403        if (this.barWidth != that.barWidth) {
404            return false;
405        }
406        if (!this.font.equals(that.font)) {
407            return false;
408        }
409        return super.equals(obj);
410    }
411}