001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
006 * 
007 * http://www.object-refinery.com/orsoncharts/index.html
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 com.orsoncharts.axis;
034
035import java.awt.FontMetrics;
036import java.awt.Graphics2D;
037import java.awt.Shape;
038import java.awt.geom.Line2D;
039import java.awt.geom.Point2D;
040import java.awt.font.LineMetrics;
041import java.text.DecimalFormat;
042import java.text.Format;
043import java.io.Serializable;
044import java.util.ArrayList;
045import java.util.HashMap;
046import java.util.Map;
047import java.util.List;
048
049import com.orsoncharts.Chart3DHints;
050import com.orsoncharts.Range;
051import com.orsoncharts.graphics3d.RenderedElement;
052import com.orsoncharts.graphics3d.RenderingInfo;
053import com.orsoncharts.graphics3d.Utils2D;
054import com.orsoncharts.interaction.InteractiveElementType;
055import com.orsoncharts.plot.CategoryPlot3D;
056import com.orsoncharts.plot.XYZPlot;
057import com.orsoncharts.util.TextUtils;
058import com.orsoncharts.util.TextAnchor;
059import com.orsoncharts.util.ArgChecks;
060import com.orsoncharts.util.ObjectUtils;
061
062/**
063 * A numerical axis for use with 3D plots (implements {@link ValueAxis3D}).
064 * In a {@link CategoryPlot3D} the value axis (the vertical one) is numerical, 
065 * and in an {@link XYZPlot} all the axes (x, y and z) are numerical - for
066 * all these cases an instance of this class can be used.
067 * <br><br>
068 * NOTE: This class is serializable, but the serialization format is subject 
069 * to change in future releases and should not be relied upon for persisting 
070 * instances of this class. 
071 */
072@SuppressWarnings("serial")
073public class NumberAxis3D extends AbstractValueAxis3D implements ValueAxis3D,
074        Serializable {
075
076    /** 
077     * A flag indicating whether or not the auto-range calculation should
078     * include zero.
079     */
080    private boolean autoRangeIncludesZero;
081    
082    /**
083     * A flag that controls how zero is handled when it falls within the
084     * margins.  If {@code true}, the margin is truncated at zero, if
085     * {@code false} the margin is not changed.
086     */
087    private boolean autoRangeStickyZero;
088        
089    /** 
090     * The tick selector (if not {@code null}, then auto-tick selection is 
091     * used). 
092     */
093    private TickSelector tickSelector;
094
095    /** 
096     * The tick size.  If the tickSelector is not {@code null} then it is 
097     * used to auto-select an appropriate tick size and format.
098     */
099    private double tickSize;
100
101    /** The tick formatter (never {@code null}). */
102    private Format tickLabelFormatter;
103
104    /**
105     * Creates a new axis with the specified label and default attributes.
106     * 
107     * @param label  the axis label ({@code null} permitted). 
108     */
109    public NumberAxis3D(String label) {
110        this(label, new Range(0.0, 1.0));
111    }
112    
113    /**
114     * Creates a new axis with the specified label and range.
115     *
116     * @param label  the axis label ({@code null} permitted).
117     * @param range  the range ({@code null} not permitted).
118     */
119    public NumberAxis3D(String label, Range range) {
120        super(label, range);
121        this.autoRangeIncludesZero = false;
122        this.autoRangeStickyZero = true;
123        this.tickSelector = new NumberTickSelector();
124        this.tickSize = range.getLength() / 10.0;
125        this.tickLabelFormatter = new DecimalFormat("0.00");
126    }
127      
128    /**
129     * Returns the flag that determines whether or not the auto range 
130     * mechanism should force zero to be included in the range.  The default
131     * value is {@code false}.
132     * 
133     * @return A boolean.
134     */
135    public boolean getAutoRangeIncludesZero() {
136        return this.autoRangeIncludesZero;
137    }
138    
139    /**
140     * Sets the flag that controls whether or not the auto range mechanism 
141     * should force zero to be included in the axis range, and sends an
142     * {@link Axis3DChangeEvent} to all registered listeners.
143     * 
144     * @param include  the new flag value.
145     */
146    public void setAutoRangeIncludeZero(boolean include) {
147        this.autoRangeIncludesZero = include;
148        fireChangeEvent(true);
149    }
150    
151    /**
152     * Returns the flag that controls the behaviour of the auto range 
153     * mechanism when zero falls into the axis margins.  The default value
154     * is {@code true}.
155     * 
156     * @return A boolean. 
157     * 
158     * @see #setAutoRangeStickyZero(boolean) 
159     */
160    public boolean getAutoRangeStickyZero() {
161        return this.autoRangeStickyZero;
162    }
163    
164    /**
165     * Sets the flag that controls the behaviour of the auto range mechanism 
166     * when zero falls into the axis margins.  If {@code true}, when
167     * zero is in the axis margin the axis range is truncated at zero.  If
168     * {@code false}, there is no special treatment.
169     * 
170     * @param sticky  the new flag value. 
171     */
172    public void setAutoRangeStickyZero(boolean sticky) {
173        this.autoRangeStickyZero = sticky;
174        fireChangeEvent(true);
175    }
176  
177    /**
178     * Returns the tick selector, an object that is responsible for choosing
179     * standard tick units for the axis.  The default value is a default
180     * instance of {@link NumberTickSelector}.
181     * 
182     * @return The tick selector. 
183     * 
184     * @see #setTickSelector(TickSelector) 
185     */
186    public TickSelector getTickSelector() {
187        return this.tickSelector;    
188    }
189    
190    /**
191     * Sets the tick selector and sends an {@link Axis3DChangeEvent} to all
192     * registered listeners.
193     * 
194     * @param selector  the selector ({@code null} permitted).
195     * 
196     * @see #getTickSelector() 
197     */
198    public void setTickSelector(TickSelector selector) {
199        this.tickSelector = selector;
200        fireChangeEvent(false);
201    }
202    
203    /**
204     * Returns the tick size (to be used when the tick selector is 
205     * {@code null}).
206     * 
207     * @return The tick size.
208     */
209    public double getTickSize() {
210        return this.tickSize;
211    }
212
213    /**
214     * Sets the tick size and sends an {@link Axis3DChangeEvent} to all 
215     * registered listeners.
216     * 
217     * @param tickSize  the new tick size.
218     */
219    public void setTickSize(double tickSize) {
220        this.tickSize = tickSize;
221        fireChangeEvent(false);
222    }
223    
224    /**
225     * Returns the tick label formatter.  The default value is
226     * {@code DecimalFormat("0.00")}.
227     * 
228     * @return The tick label formatter (never {@code null}). 
229     */
230    public Format getTickLabelFormatter() {
231        return this.tickLabelFormatter;
232    }
233    
234    /**
235     * Sets the formatter for the tick labels and sends an 
236     * {@link Axis3DChangeEvent} to all registered listeners.
237     * 
238     * @param formatter  the formatter ({@code null} not permitted).
239     */
240    public void setTickLabelFormatter(Format formatter) {
241        ArgChecks.nullNotPermitted(formatter, "formatter");
242        this.tickLabelFormatter = formatter;
243        fireChangeEvent(false);
244    }
245    
246    /**
247     * Adjusts the range by adding the lower and upper margins and taking into
248     * account also the {@code autoRangeStickyZero} flag.
249     * 
250     * @param range  the range ({@code null} not permitted).
251     * 
252     * @return The adjusted range. 
253     */
254    @Override
255    protected Range adjustedDataRange(Range range) {
256        ArgChecks.nullNotPermitted(range, "range");
257        double lm = range.getLength() * getLowerMargin();
258        double um = range.getLength() * getUpperMargin();
259        double lowerBound = range.getMin() - lm;
260        double upperBound = range.getMax() + um;
261        // does zero fall in the margins?
262        if (this.autoRangeStickyZero) {
263            if (0.0 <= range.getMin() && 0.0 > lowerBound) {
264                lowerBound = 0.0;
265            }
266            if (0.0 >= range.getMax() && 0.0 < upperBound) {
267                upperBound = 0.0;
268            }
269        }
270        if ((upperBound - lowerBound) < getMinAutoRangeLength()) {
271            double adj = (getMinAutoRangeLength() - (upperBound - lowerBound)) 
272                    / 2.0;
273            lowerBound -= adj;
274            upperBound += adj;
275        }
276        return new Range(lowerBound, upperBound);
277    }
278    
279    /**
280     * Draws the axis to the supplied graphics target ({@code g2}, with the
281     * specified starting and ending points for the line.  This method is used
282     * internally, you should not need to call it directly.
283     *
284     * @param g2  the graphics target ({@code null} not permitted).
285     * @param pt0  the starting point ({@code null} not permitted).
286     * @param pt1  the ending point ({@code null} not permitted).
287     * @param opposingPt  an opposing point (to determine which side of the 
288     *     axis line the labels should appear, {@code null} not permitted).
289     * @param tickData  tick details ({@code null} not permitted).
290     * @param info  an object to be populated with rendering info 
291     *     ({@code null} permitted).
292     * @param hinting  perform element hinting?
293     */
294    @Override
295    public void draw(Graphics2D g2, Point2D pt0, Point2D pt1, 
296            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
297            boolean hinting) {
298        
299        if (!isVisible()) {
300            return;
301        }
302        if (pt0.equals(pt1)) {
303            return;
304        }
305        
306        // draw a line for the axis
307        g2.setStroke(getLineStroke());
308        g2.setPaint(getLineColor());
309        Line2D axisLine = new Line2D.Float(pt0, pt1);  
310        g2.draw(axisLine);
311        
312        // draw the tick marks and labels
313        g2.setFont(getTickLabelFont());
314        // we track the max width or height of the labels to know how far to
315        // offset the axis label when we draw it later
316        double maxTickLabelDim = 0.0;
317        if (getTickLabelOrientation().equals(LabelOrientation.PARALLEL)) {
318            LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2);
319            maxTickLabelDim = lm.getHeight();
320        }
321        double tickMarkLength = getTickMarkLength();
322        double tickLabelOffset = getTickLabelOffset();
323        g2.setPaint(getTickMarkPaint());
324        g2.setStroke(getTickMarkStroke());
325        for (TickData t : tickData) {
326            if (tickMarkLength > 0.0) {
327                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine, 
328                       t.getAnchorPt(), tickMarkLength, opposingPt);
329                g2.draw(tickLine);
330            }
331            String tickLabel = this.tickLabelFormatter.format(t.getDataValue());
332            if (getTickLabelOrientation().equals(
333                    LabelOrientation.PERPENDICULAR)) {
334                maxTickLabelDim = Math.max(maxTickLabelDim, 
335                        g2.getFontMetrics().stringWidth(tickLabel));
336            }
337        }
338            
339        if (getTickLabelsVisible()) {
340            g2.setPaint(getTickLabelColor());
341            if (getTickLabelOrientation().equals(
342                    LabelOrientation.PERPENDICULAR)) {
343                drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData,
344                        info, hinting);
345            } else {
346                drawParallelTickLabels(g2, axisLine, opposingPt, tickData, 
347                        info, hinting);
348            }
349        } else {
350            maxTickLabelDim = 0.0;
351        }
352
353        // draw the axis label (if any)...
354        if (getLabel() != null) {
355            Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine, 
356                    opposingPt, maxTickLabelDim + tickMarkLength 
357                    + tickLabelOffset + getLabelOffset(), info, hinting);
358        }
359    }
360    
361    /**
362     * Draws tick labels parallel to the axis.
363     * 
364     * @param g2  the graphics target ({@code null} not permitted).
365     * @param axisLine  the axis line ({@code null} not permitted).
366     * @param opposingPt  an opposing point (to determine on which side the 
367     *     labels appear, {@code null} not permitted).
368     * @param tickData  the tick data ({@code null} not permitted).
369     * @param info  if not {@code null} this object will be updated with
370     *     {@link RenderedElement} instances for each of the tick labels.
371     */
372    private void drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
373            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
374            boolean hinting) {
375        
376        g2.setFont(getTickLabelFont());
377        double halfAscent = g2.getFontMetrics().getAscent() / 2.0;
378        for (TickData t : tickData) {
379            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
380                    t.getAnchorPt(), getTickMarkLength()
381                    + getTickLabelOffset() + halfAscent, opposingPt);
382            double axisTheta = Utils2D.calculateTheta(axisLine);
383            TextAnchor textAnchor = TextAnchor.CENTER;
384            if (axisTheta >= Math.PI / 2.0) {
385                axisTheta = axisTheta - Math.PI;
386            } else if (axisTheta <= -Math.PI / 2) {
387                axisTheta = axisTheta + Math.PI;  
388            }
389            String tickLabel = this.tickLabelFormatter.format(
390                    t.getDataValue());
391            if (hinting) {
392                Map<String, String> m = new HashMap<String, String>();
393                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \"" 
394                        + axisStr() + "\", \"value\": \"" 
395                        + t.getDataValue() + "\"}");
396                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
397            }
398            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
399                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
400                    textAnchor, axisTheta, textAnchor);
401            if (hinting) {
402                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
403            }
404            if (info != null) {
405                RenderedElement tickLabelElement = new RenderedElement(
406                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
407                tickLabelElement.setProperty("axis", axisStr());
408                tickLabelElement.setProperty("value", 
409                        Double.valueOf(t.getDataValue()));
410                info.addOffsetElement(tickLabelElement);
411            }
412        }
413    }
414    
415    /**
416     * Draws tick labels perpendicular to the axis.
417     * 
418     * @param g2  the graphics target ({@code null} not permitted).
419     * @param axisLine  the axis line ({@code null} not permitted).
420     * @param opposingPt  an opposing point (to determine on which side the 
421     *     labels appear, {@code null} not permitted).
422     * @param tickData  the tick data ({@code null} not permitted).
423     * @param info  if not {@code null} this object will be updated with
424     *     {@link RenderedElement} instances for each of the tick labels.
425     */
426    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
427            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
428            boolean hinting) {
429        for (TickData t : tickData) {
430            double theta = Utils2D.calculateTheta(axisLine);
431            double thetaAdj = theta + Math.PI / 2.0;
432            if (thetaAdj < -Math.PI / 2.0) {
433                thetaAdj = thetaAdj + Math.PI;
434            }
435            if (thetaAdj > Math.PI / 2.0) {
436                thetaAdj = thetaAdj - Math.PI;
437            }
438
439            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
440                    t.getAnchorPt(), getTickMarkLength() + getTickLabelOffset(), 
441                    opposingPt);
442            double perpTheta = Utils2D.calculateTheta(perpLine);  
443            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
444            if (Math.abs(perpTheta) > Math.PI / 2.0) {
445                textAnchor = TextAnchor.CENTER_RIGHT;
446            } 
447            String tickLabel = this.tickLabelFormatter.format(
448                    t.getDataValue());
449            if (hinting) {
450                Map<String, String> m = new HashMap<String, String>();
451                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \"" 
452                        + axisStr() + "\", \"value\": \"" 
453                        + t.getDataValue() + "\"}");
454                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
455            }
456            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
457                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
458                    textAnchor, thetaAdj, textAnchor);
459            if (hinting) {
460                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
461            }
462            if (info != null) {
463                RenderedElement tickLabelElement = new RenderedElement(
464                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
465                tickLabelElement.setProperty("axis", axisStr());
466                tickLabelElement.setProperty("value", 
467                        Double.valueOf(t.getDataValue()));
468                info.addOffsetElement(tickLabelElement);
469            }
470        }   
471    }
472    
473    /**
474     * Converts a data value to world coordinates, taking into account the
475     * current axis range (assumes the world axis is zero-based and has the
476     * specified length).
477     * 
478     * @param value  the data value (in axis units).
479     * @param length  the length of the (zero based) world axis.
480     * 
481     * @return A world coordinate.
482     */
483    @Override
484    public double translateToWorld(double value, double length) {
485        double p = getRange().percent(value, isInverted());
486        return length * p;
487    }
488  
489    /**
490     * Selects a tick size that is appropriate for drawing the axis from
491     * {@code pt0} to {@code pt1}.
492     * 
493     * @param g2  the graphics target ({@code null} not permitted).
494     * @param pt0  the starting point for the axis.
495     * @param pt1  the ending point for the axis.
496     * @param opposingPt  a point on the opposite side of the line from where
497     *     the labels should be drawn.
498     */
499    @Override
500    public double selectTick(Graphics2D g2, Point2D pt0, Point2D pt1, 
501            Point2D opposingPt) {
502        
503        if (this.tickSelector == null) {
504            return this.tickSize;
505        }
506        g2.setFont(getTickLabelFont()); 
507        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());        
508        double length = pt0.distance(pt1);
509        LabelOrientation orientation = getTickLabelOrientation();
510        if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
511            // based on the font height, we can determine roughly how many tick
512            // labels will fit in the length available
513            double height = fm.getHeight();
514            // the tickLabelFactor allows some control over how dense the labels
515            // will be
516            int maxTicks = (int) (length / (height * getTickLabelFactor()));
517            if (maxTicks > 2 && this.tickSelector != null) {
518                double rangeLength = getRange().getLength();
519                this.tickSelector.select(rangeLength / 2.0);
520                // step through until we have too many ticks OR we run out of 
521                // tick sizes
522                int tickCount = (int) (rangeLength 
523                        / this.tickSelector.getCurrentTickSize());
524                while (tickCount < maxTicks) {
525                    this.tickSelector.previous();
526                    tickCount = (int) (rangeLength
527                            / this.tickSelector.getCurrentTickSize());
528                }
529                this.tickSelector.next();
530                this.tickSize = this.tickSelector.getCurrentTickSize();
531                this.tickLabelFormatter 
532                        = this.tickSelector.getCurrentTickLabelFormat();
533            } else {
534                this.tickSize = Double.NaN;
535            }
536        } else if (orientation.equals(LabelOrientation.PARALLEL)) {
537            // choose a unit that is at least as large as the length of the axis
538            this.tickSelector.select(getRange().getLength());
539            boolean done = false;
540            while (!done) {
541                if (this.tickSelector.previous()) {
542                    // estimate the label widths, and do they overlap?
543                    Format f = this.tickSelector.getCurrentTickLabelFormat();
544                    String s0 = f.format(this.range.getMin());
545                    String s1 = f.format(this.range.getMax());
546                    double w0 = fm.stringWidth(s0);
547                    double w1 = fm.stringWidth(s1);
548                    double w = Math.max(w0, w1);
549                    int n = (int) (length / (w * this.getTickLabelFactor()));
550                    if (n < getRange().getLength() 
551                            / tickSelector.getCurrentTickSize()) {
552                        tickSelector.next();
553                        done = true;
554                    }
555                } else {
556                    done = true;
557                }
558            }
559            this.tickSize = this.tickSelector.getCurrentTickSize();
560            this.tickLabelFormatter 
561                    = this.tickSelector.getCurrentTickLabelFormat();
562        }
563        return this.tickSize;
564    }
565
566    /**
567     * Generates a list of tick data items for the specified tick unit.  This
568     * data will be passed to the 3D engine and will be updated with a 2D
569     * projection that can later be used to write the axis tick labels in the
570     * appropriate places.
571     * <br><br>
572     * If {@code tickUnit} is {@code Double.NaN}, then tick data is
573     * generated for just the bounds of the axis.
574     * 
575     * @param tickUnit  the tick unit.
576     * 
577     * @return A list of tick data (never {@code null}). 
578     */
579    @Override
580    public List<TickData> generateTickData(double tickUnit) {
581        List<TickData> result = new ArrayList<TickData>();
582        if (Double.isNaN(tickUnit)) {
583            result.add(new TickData(0, getRange().getMin()));
584            result.add(new TickData(1, getRange().getMax()));
585        } else {
586            double x = tickUnit * Math.ceil(this.range.getMin() / tickUnit);
587            while (x <= this.range.getMax()) {
588                result.add(new TickData(this.range.percent(x, isInverted()), 
589                        x));
590                x += tickUnit;
591            }
592        }
593        return result;
594    }
595
596    /**
597     * Tests this instance for equality with an arbitrary object.
598     * 
599     * @param obj  the object to test against ({@code null} permitted).
600     * 
601     * @return A boolean. 
602     */
603    @Override
604    public boolean equals(Object obj) {
605        if (obj == this) {
606            return true;
607        }
608        if (!(obj instanceof NumberAxis3D)) {
609            return false;
610        }
611        NumberAxis3D that = (NumberAxis3D) obj;
612        if (this.autoRangeIncludesZero != that.autoRangeIncludesZero) {
613            return false;
614        }
615        if (this.autoRangeStickyZero != that.autoRangeStickyZero) {
616            return false;
617        }
618        if (this.tickSize != that.tickSize) {
619            return false;
620        }
621        if (!ObjectUtils.equals(this.tickSelector, that.tickSelector)) {
622            return false;
623        }
624        if (!this.tickLabelFormatter.equals(that.tickLabelFormatter)) {
625            return false;
626        }
627        return super.equals(obj);
628    }
629
630    /**
631     * Returns a hash code for this instance.
632     * 
633     * @return A hash code. 
634     */
635    @Override
636    public int hashCode() {
637        int hash = 3;
638        hash = 59 * hash + (int) (Double.doubleToLongBits(this.tickSize) 
639                ^ (Double.doubleToLongBits(this.tickSize) >>> 32));
640        hash = 59 * hash + ObjectUtils.hashCode(this.tickLabelFormatter);
641        return hash;
642    }
643    
644}