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.axis;
034
035import java.io.Serializable;
036import java.text.DecimalFormat;
037import java.text.Format;
038import org.jfree.chart3d.internal.Args;
039
040/**
041 * A {@link TickSelector} implementation that selects tick units in multiples 
042 * of 1, 2 and 5.
043 * <br><br>
044 * NOTE: This class is serializable, but the serialization format is subject 
045 * to change in future releases and should not be relied upon for persisting 
046 * instances of this class. 
047 */
048@SuppressWarnings("serial")
049public class NumberTickSelector implements TickSelector, Serializable {
050
051    private int power = 0;
052    
053    private int factor = 1;
054    
055    /** 
056     * A flag to track if the units are percentage values, in which case the
057     * formatter should display less decimal places.
058     */
059    private boolean percentage;
060    
061    /**
062     * Creates a new instance.
063     */
064    public NumberTickSelector() {
065        this(false);
066    }
067    
068    /**
069     * Creates a new instance, with the option to display the tick values as
070     * percentages.  The axis follows the normal convention that values in the
071     * range 0.0 to 1.0 a represented as 0% to 100%.
072     * 
073     * @param percentage  format the tick values as percentages. 
074     */
075    public NumberTickSelector(boolean percentage) {
076        this.power = 0;
077        this.factor = 1;
078        this.percentage = percentage;
079    }
080    
081    /**
082     * Selects and returns a standard tick size that is greater than or equal to 
083     * the specified reference value and, ideally, as close to it as possible 
084     * (to minimise the number of iterations used by axes to determine the tick
085     * size to use).  After a call to this method, the 
086     * {@link #getCurrentTickSize()} method should return the selected tick 
087     * size (there is a "pointer" to this tick size), the {@link #next()} 
088     * method should move the pointer to the next (larger) standard tick size, 
089     * and the {@link #previous()} method should move the pointer to the 
090     * previous (smaller) standard tick size.
091     * 
092     * @param reference  the reference value (must be positive and finite).
093     * 
094     * @return The selected tick size. 
095     */
096    @Override
097    public double select(double reference) {
098        Args.finitePositiveRequired(reference, "reference");
099        this.power = (int) Math.ceil(Math.log10(reference));
100        this.factor = 1;
101        return getCurrentTickSize();
102    }
103
104    /**
105     * Move the cursor to the next (larger) tick size, if there is one.  
106     * Returns {@code true} in the case that the cursor is moved, and 
107     * {@code false} where there are a finite number of tick sizes and the
108     * current tick size is the largest available.
109     */
110    @Override
111    public boolean next() {
112        if (factor == 1) {
113            factor = 2;
114            return true;
115        } 
116        if (factor == 2) {
117            factor = 5;
118            return true;  
119        } 
120        if (factor == 5) {
121            power++;
122            factor = 1;
123            return true;
124        } 
125        throw new IllegalStateException("We should never get here.");
126    }
127
128    /**
129     * Move the cursor to the previous (smaller) tick size, if there is one.  
130     * Returns {@code true} in the case that the cursor is moved, and 
131     * {@code false} where there are a finite number of tick sizes and the
132     * current tick size is the smallest available.
133     */
134    @Override
135    public boolean previous() {
136        if (factor == 1) {
137            factor = 5;
138            power--;
139            return true;
140        } 
141        if (factor == 2) {
142            factor = 1;
143            return true;  
144        } 
145        if (factor == 5) {
146            factor = 2;
147            return true;
148        } 
149        throw new IllegalStateException("We should never get here.");
150    }
151
152    @Override
153    public double getCurrentTickSize() {
154        return this.factor * Math.pow(10.0, this.power);
155    }
156    
157    private final DecimalFormat dfNeg4 = new DecimalFormat("0.0000");
158    private final DecimalFormat dfNeg3 = new DecimalFormat("0.000");
159    private final DecimalFormat dfNeg2 = new DecimalFormat("0.00");
160    private final DecimalFormat dfNeg1 = new DecimalFormat("0.0");
161    private final DecimalFormat df0 = new DecimalFormat("#,##0");
162    private final DecimalFormat dfNeg4P = new DecimalFormat("0.00%");
163    private final DecimalFormat dfNeg3P = new DecimalFormat("0.0%");
164    private final DecimalFormat dfNeg2P = new DecimalFormat("0%");
165    private final DecimalFormat dfNeg1P = new DecimalFormat("0%");
166    private final DecimalFormat df0P = new DecimalFormat("#,##0%");
167
168    @Override
169    public Format getCurrentTickLabelFormat() {
170        if (power == -4) {
171            return this.percentage ? dfNeg4P : dfNeg4;
172        }
173        if (power == -3) {
174            return this.percentage ? dfNeg3P : dfNeg3;
175        }
176        if (power == -2) {
177            return this.percentage ? dfNeg2P : dfNeg2;
178        }
179        if (power == -1) {
180            return this.percentage ? dfNeg1P : dfNeg1;
181        }
182        if (power >= 0 && power <= 6) {
183            return this.percentage ? df0P : df0;
184        }
185        return this.percentage ? new DecimalFormat("0.0000E0%") 
186                : new DecimalFormat("0.0000E0");
187    }
188    
189    /**
190     * Tests this instance for equality with an arbitrary object.
191     * 
192     * @param obj  the object ({@code null} permitted).
193     * 
194     * @return A boolean. 
195     */
196    @Override
197    public boolean equals(Object obj) {
198        if (obj ==  this) {
199            return true;
200        }
201        if (!(obj instanceof NumberTickSelector)) {
202            return false;
203        }
204        NumberTickSelector that = (NumberTickSelector) obj;
205        if (this.percentage != that.percentage) {
206            return false;
207        }
208        return true;
209    }
210    
211}