/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 1999-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.axis;
import java.text.NumberFormat;
import java.util.Locale;
import org.geotools.resources.XMath;
/**
* Itérateur balayant les barres et étiquettes de graduation d'un axe.
* Cet itérateur retourne les positions des graduations à partir de la
* valeur minimale jusqu'à la valeur maximale.
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*/
class NumberIterator implements TickIterator {
/**
* Petite quantité utilisée pour éviter les
* erreurs d'arrondissements dans les comparaisons.
*/
private static final double EPS = 1E-8;
/**
* Valeur de la première graduation principale.
* Cette valeur est fixée par {@link #init}.
*/
private double minimum;
/**
* Valeur limite des graduations. La dernière
* graduation n'aura pas nécessairement cette
* valeur. Cette valeur est fixée par {@link #init}.
*/
private double maximum;
/**
* Intervalle entre deux graduations principales.
* Cette valeur est fixée par {@link #init}.
*/
private double increment;
/**
* Longueur de l'axe (en points). Cette information
* est conservée afin d'éviter de refaire toute la
* procédure {@link #init} si les paramètres n'ont
* pas changés.
*/
private float visualLength;
/**
* Espace à laisser (en points) entre les graduations principales.
* Cette information est conservée afin d'éviter de refaire toute
* la procédure {@link #init} si les paramètres n'ont pas changés.
*/
private float visualTickSpacing;
/**
* Nombre de sous-divisions dans une graduation principale.
* Cette valeur est fixée par {@link #init}.
*/
private int subTickCount;
/**
* Index de la première sous-graduation
* dans la première graduation principale.
* Cette valeur est fixée par {@link #init}.
*/
private int subTickStart;
/**
* Index de la graduation principale en cours de traçage.
* Cette valeur commence à 0 et sera modifiée à chaque
* appel à {@link #next}.
*/
private int tickIndex;
/**
* Index de la graduation secondaire en cours de traçage. Cette
* valeur va de 0 inclusivement jusqu'à {@link #subTickCount}
* exclusivement. Elle sera modifiée à chaque appel à {@link #next}.
*/
private int subTickIndex;
/**
* Valeur de la graduation principale ou secondaire actuelle.
* Cette valeur sera modifiée à chaque appel à {@link #next}.
*/
private double value;
/**
* Format à utiliser pour écrire les étiquettes de graduation. Ce format ne
* sera construit que la première fois où {@link #currentLabel} sera appelée.
*/
private transient NumberFormat format;
/**
* Indique si {@link #format} est valide. Le format peut
* devenir invalide si {@link #init} a été appelée. Dans
* ce cas, il peut falloir changer le nombre de chiffres
* après la virgule qu'il écrit.
*/
private transient boolean formatValid;
/**
* Conventions à utiliser pour
* le formatage des nombres.
*/
private Locale locale;
/**
* Construit un itérateur par défaut. La méthode {@link #init}
* <u>doit</u> être appelée avant que cet itérateur ne soit
* utilisable.
*
* @param locale Conventions à utiliser pour le formatage des nombres.
*/
protected NumberIterator(final Locale locale) {
this.locale = locale;
}
/**
* Initialise l'itérateur.
*
* @param minimum Valeur minimale de la première graduation.
* @param maximum Valeur limite des graduations. La dernière
* graduation n'aura pas nécessairement cette valeur.
* @param visualLength Longueur visuelle de l'axe sur laquelle tracer la graduation.
* Cette longueur doit être exprimée en pixels ou en points.
* @param visualTickSpacing Espace à laisser visuellement entre deux marques de graduation.
* Cet espace doit être exprimé en pixels ou en points (1/72 de pouce).
*/
protected void init( double minimum,
final double maximum,
final float visualLength,
final float visualTickSpacing)
{
if (minimum == this.minimum &&
maximum == this.maximum &&
visualLength == this.visualLength &&
visualTickSpacing == this.visualTickSpacing)
{
rewind();
return;
}
AbstractGraduation.ensureFinite ("minimum", minimum);
AbstractGraduation.ensureFinite ("maximum", maximum);
AbstractGraduation.ensureFinite ("visualLength", visualLength); // May be 0.
AbstractGraduation.ensureNonNull("visualTickSpacing", visualTickSpacing);
this.visualLength = visualLength;
this.visualTickSpacing = visualTickSpacing;
/*
* Estime le pas qui donnera au moins l'espacement spécifié entre
* chaque graduation. Détermine ensuite si ce pas est de l'ordre
* des dizaines, centaines ou autre et on ramènera temporairement
* ce pas à l'ordre des unitées.
*/
double increment = (maximum - minimum) * (visualTickSpacing / visualLength);
final double factor = XMath.pow10((int) Math.floor(Math.log10(increment)));
increment /= factor;
if (Double.isNaN(increment) || Double.isInfinite(increment) || increment==0) {
this.minimum = minimum;
this.maximum = maximum;
this.increment = Double.NaN;
this.value = Double.NaN;
this.tickIndex = 0;
this.subTickIndex = 0;
this.subTickStart = 0;
this.subTickCount = 1;
this.formatValid = false;
return;
}
/*
* Le pas se trouve maintenant entre 1 et 10. On l'ajuste maintenant
* pour lui donner des valeurs qui ne sont habituellement pas trop
* difficiles à lire.
*/
final int subTickCount;
if (increment <= 1.0) {increment = 1.0; subTickCount=5;}
else if (increment <= 2.0) {increment = 2.0; subTickCount=4;}
else if (increment <= 2.5) {increment = 2.5; subTickCount=5;}
else if (increment <= 4.0) {increment = 4.0; subTickCount=4;}
else if (increment <= 5.0) {increment = 5.0; subTickCount=5;}
else {increment =10.0; subTickCount=5;}
increment = increment*factor;
/*
* Arrondie maintenant le minimum sur une des graduations principales.
* Détermine ensuite combien de graduations secondaires il faut sauter
* sur la première graduation principale.
*/
final double tmp = minimum;
minimum = Math.floor(minimum/increment+EPS)*increment;
int subTickStart = (int)Math.ceil((tmp-minimum-EPS)*(subTickCount/increment));
final int extra = subTickStart / subTickCount;
minimum += extra*increment;
subTickStart -= extra*subTickCount;
this.increment = increment;
this.subTickCount = subTickCount;
this.maximum = maximum + Math.abs(maximum*EPS);
this.minimum = minimum;
this.subTickStart = subTickStart;
this.subTickIndex = subTickStart;
this.tickIndex = 0;
this.value = minimum + increment*(subTickStart/(double)subTickCount);
this.formatValid = false;
}
/**
* Indique s'il reste des graduations à retourner. Cette méthode retourne {@code true}
* tant que {@link #currentValue} ou {@link #currentLabel} peuvent être appelées.
*/
public boolean hasNext() {
return value <= maximum;
}
/**
* Indique si la graduation courante est une graduation majeure.
*
* @return {@code true} si la graduation courante est une
* graduation majeure, ou {@code false} si elle
* est une graduation mineure.
*/
public boolean isMajorTick() {
return subTickIndex == 0;
}
/**
* Returns the position where to draw the current tick. The position is scaled
* from the graduation's minimum to maximum. This is usually the same number
* than {@link #currentValue}. The mean exception is for logarithmic graduation,
* in which the tick position is not proportional to the tick value.
*/
public double currentPosition() {
return value;
}
/**
* Retourne la valeur de la graduation courante. Cette méthode
* peut être appelée pour une graduation majeure ou mineure.
*/
public double currentValue() {
return value;
}
/**
* Retourne l'étiquette de la graduation courante. On n'appele généralement
* cette méthode que pour les graduations majeures, mais elle peut aussi
* être appelée pour les graduations mineures. Cette méthode retourne
* {@code null} s'il n'y a pas d'étiquette pour la graduation courante.
*/
public String currentLabel() {
if (!formatValid) {
if (format == null) {
format = NumberFormat.getNumberInstance(locale);
}
/*
* Trouve le nombre de chiffres après la virgule nécessaires pour représenter les
* étiquettes de la graduation. Impose une limite de six chiffres, limite qui pourrait
* être atteinte notamment avec les nombres périodiques (par exemple des intervalles
* de temps exprimés en fractions de jours).
*/
int precision;
double step = Math.abs(increment);
for (precision=0; precision<6; precision++) {
final double check = Math.rint(step*1E+4) % 1E+4;
if (!(check > step*EPS)) { // 'step' may be NaN
break;
}
step *= 10;
}
format.setMinimumFractionDigits(precision);
format.setMaximumFractionDigits(precision);
formatValid = true;
}
return format.format(currentValue());
}
/**
* Passe à la graduation suivante.
*/
public void next() {
if (++subTickIndex >= subTickCount) {
subTickIndex = 0;
tickIndex++;
}
// On n'utilise pas "+=" afin d'éviter les erreurs d'arrondissements.
value = minimum + increment * (tickIndex + subTickIndex / (double)subTickCount);
}
/**
* Passe directement à la graduation majeure suivante.
*/
public void nextMajor() {
subTickIndex = 0;
value = minimum + increment * (++tickIndex);
}
/**
* Replace l'itérateur sur la première graduation.
*/
public void rewind() {
tickIndex = 0;
subTickIndex = subTickStart;
value = minimum + increment*(subTickStart/(double)subTickCount);
}
/**
* Retourne les conventions à utiliser pour
* écrire les étiquettes de graduation.
*/
public final Locale getLocale() {
return locale;
}
/**
* Modifie les conventions à utiliser pour
* écrire les étiquettes de graduation.
*/
public final void setLocale(final Locale locale) {
if (!locale.equals(this.locale)) {
this.locale = locale;
this.format = null;
formatValid = false;
}
}
}