/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2000, Institut de Recherche pour le Développement
 *    (C) 1999, Pêches et Océans Canada
 *
 *    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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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;

// Date and time
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;


/**
 * Itérateur balayant les barres et étiquettes de graduation d'un axe du temps. Cet itérateur
 * retourne les positions des graduations à partir de la date la plus ancienne jusqu'à la date
 * la plus récente. Il choisit les intervalles de graduation en supposant qu'on utilise un
 * calendrier grégorien.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/axis/DateIterator.java $
 * @version $Id: DateIterator.java 20883 2006-08-07 13:48:09Z jgarnett $
 * @author Martin Desruisseaux
 */
final class DateIterator implements TickIterator {
    /**
     * Nombre de millisecondes dans certaines unités de temps.
     */
    private static final long SEC  = 1000,
                              MIN  = 60*SEC,
                              HRE  = 60*MIN,
                              DAY  = 24*HRE,
                              YEAR = 365*DAY + (DAY/4) - (DAY/100) + (DAY/400),
                              MNT  = YEAR/12;

    /**
     * Liste des intervales souhaités pour la graduation. Les éléments de
     * cette table doivent obligatoirement apparaître en ordre croissant.
     * Voici un exemple d'interprétation: la présence de {@@code 5*MIN}
     * suivit de {@code 10*MIN} implique que si le pas estimé se trouve
     * entre 5 et 10 minutes, ce sera le pas de 10 minutes qui sera sélectionné.
     */
    private static final long[] INTERVAL = {
        SEC,  2*SEC,  5*SEC, 10*SEC, 15*SEC, 20*SEC, 30*SEC,
        MIN,  2*MIN,  5*MIN, 10*MIN, 15*MIN, 20*MIN, 30*MIN,
        HRE,  2*HRE,  3*HRE,  4*HRE,  6*HRE,  8*HRE, 12*HRE,
        DAY,  2*DAY,  3*DAY,  7*DAY, 14*DAY, 21*DAY,
        MNT,  2*MNT,  3*MNT,  4*MNT,  6*MNT,
        YEAR, 2*YEAR, 3*YEAR, 4*YEAR, 5*YEAR
    };

    /**
     * Intervalles des graduations principales et des sous-graduations correspondants
     * à chaque des intervalles du tableau {@link #INTERVAL}.  Cette classe cherchera
     * d'abord un intervalle en millisecondes dans le tableau {@link #INTERVAL}, puis
     * traduira cet intervalle en champ du calendrier grégorien en lisant les éléments
     * correspondants de ce tableau {@link #ROLL}.
     */
    private static final byte[] ROLL = {
         1, (byte)Calendar.SECOND,        25, (byte)Calendar.MILLISECOND, // x10 millis
         2, (byte)Calendar.SECOND,        50, (byte)Calendar.MILLISECOND, // x10 millis
         5, (byte)Calendar.SECOND,         1, (byte)Calendar.SECOND,
        10, (byte)Calendar.SECOND,         2, (byte)Calendar.SECOND,
        15, (byte)Calendar.SECOND,         5, (byte)Calendar.SECOND,
        20, (byte)Calendar.SECOND,         5, (byte)Calendar.SECOND,
        30, (byte)Calendar.SECOND,         5, (byte)Calendar.SECOND,
         1, (byte)Calendar.MINUTE,        10, (byte)Calendar.SECOND,
         2, (byte)Calendar.MINUTE,        30, (byte)Calendar.SECOND,
         5, (byte)Calendar.MINUTE,         1, (byte)Calendar.MINUTE,
        10, (byte)Calendar.MINUTE,         2, (byte)Calendar.MINUTE,
        15, (byte)Calendar.MINUTE,         5, (byte)Calendar.MINUTE,
        20, (byte)Calendar.MINUTE,         5, (byte)Calendar.MINUTE,
        30, (byte)Calendar.MINUTE,         5, (byte)Calendar.MINUTE,
         1, (byte)Calendar.HOUR_OF_DAY,   15, (byte)Calendar.MINUTE,
         2, (byte)Calendar.HOUR_OF_DAY,   30, (byte)Calendar.MINUTE,
         3, (byte)Calendar.HOUR_OF_DAY,   30, (byte)Calendar.MINUTE,
         4, (byte)Calendar.HOUR_OF_DAY,    1, (byte)Calendar.HOUR_OF_DAY,
         6, (byte)Calendar.HOUR_OF_DAY,    1, (byte)Calendar.HOUR_OF_DAY,
         8, (byte)Calendar.HOUR_OF_DAY,    2, (byte)Calendar.HOUR_OF_DAY,
        12, (byte)Calendar.HOUR_OF_DAY,    2, (byte)Calendar.HOUR_OF_DAY,
         1, (byte)Calendar.DAY_OF_MONTH,   4, (byte)Calendar.HOUR_OF_DAY,
         2, (byte)Calendar.DAY_OF_MONTH,   6, (byte)Calendar.HOUR_OF_DAY,
         3, (byte)Calendar.DAY_OF_MONTH,  12, (byte)Calendar.HOUR_OF_DAY,
         7, (byte)Calendar.DAY_OF_MONTH,   1, (byte)Calendar.DAY_OF_MONTH,
        14, (byte)Calendar.DAY_OF_MONTH,   2, (byte)Calendar.DAY_OF_MONTH,
        21, (byte)Calendar.DAY_OF_MONTH,   7, (byte)Calendar.DAY_OF_MONTH,
         1, (byte)Calendar.MONTH,          7, (byte)Calendar.DAY_OF_MONTH,
         2, (byte)Calendar.MONTH,         14, (byte)Calendar.DAY_OF_MONTH,
         3, (byte)Calendar.MONTH,         14, (byte)Calendar.DAY_OF_MONTH,
         4, (byte)Calendar.MONTH,          1, (byte)Calendar.MONTH,
         6, (byte)Calendar.MONTH,          1, (byte)Calendar.MONTH,
         1, (byte)Calendar.YEAR,           2, (byte)Calendar.MONTH,
         2, (byte)Calendar.YEAR,           4, (byte)Calendar.MONTH,
         3, (byte)Calendar.YEAR,           6, (byte)Calendar.MONTH,
         4, (byte)Calendar.YEAR,           1, (byte)Calendar.YEAR,
         5, (byte)Calendar.YEAR,           1, (byte)Calendar.YEAR
    };

    /**
     * Nombre de colonne dans le tableau {@link ROLL}. Le tableau {@link ROLL} doit être
     * interprété comme une matrice de 4 colonnes et d'un nombre indéterminé de lignes.
     */
    private static final int ROLL_WIDTH = 4;

    /**
     * Liste des champs de dates qui apparaissent dans le tableau {@link ROLL}. Cette liste
     * doit être du champ le plus grand (YEAR) vers le champ le plus petit (MILLISECOND).
     */
    private static final int[] FIELD = {
        Calendar.YEAR,
        Calendar.MONTH,
        Calendar.DAY_OF_MONTH,
        Calendar.HOUR_OF_DAY,
        Calendar.MINUTE,
        Calendar.SECOND,
        Calendar.MILLISECOND
    };

    /**
     * Liste des noms des champs (à des fins de déboguage seulement).
     * Cette liste doit être dans le même ordre que les éléments de {@link #FIELD}.
     */
    private static final String[] FIELD_NAME = {
        "YEAR",
        "MONTH",
        "DAY",
        "HOUR",
        "MINUTE",
        "SECOND",
        "MILLISECOND"
    };

    /**
     * Date de la première graduation principale. Cette valeur est fixée par {@link #init}.
     */
    private long minimum;

    /**
     * Date limite des graduations. La dernière graduation ne sera pas nécessairement à
     * cette date. Cette valeur est fixée par {@link #init}.
     */
    private long maximum;

    /**
     * Estimation de l'intervalle entre deux graduations principales.
     * Cette valeur est fixée par {@link #init}.
     */
    private long 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 fois qu'il faut incrémenter le champ {@link #tickField} du
     * calendrier pour passer à la graduation suivante. Cette opération peut
     * se faire avec {@code calendar.add(tickField, tickAdd)}.
     */
    private int tickAdd;

    /**
     * Champ du calendrier qu'il faut incrémenter pour passer à la graduation suivante.
     * Cette opération peut se faire avec {@code calendar.add(tickField, tickAdd)}.
     */
    private int tickField;

    /**
     * Nombre de fois qu'il faut incrémenter le champ {@link #tickField} du calendrier pour
     * passer à la sous-graduation suivante. Cette opération peut se faire avec
     * {@code calendar.add(tickField, tickAdd)}.
     */
    private int subTickAdd;

    /**
     * Champ du calendrier qu'il faut incrémenter pour passer à la sous-graduation suivante.
     * Cette opération peut se faire avec {@code calendar.add(tickField, tickAdd)}.
     */
    private int subTickField;

    /**
     * Date de la graduation principale ou secondaire actuelle.
     * Cette valeur sera modifiée à chaque appel à {@link #next}.
     */
    private long value;

    /**
     * Date de la prochaine graduation principale. Cette
     * valeur sera modifiée à chaque appel à {@link #next}.
     */
    private long nextTick;

    /**
     * Date de la prochaine graduation secondaire. Cette
     * valeur sera modifiée à chaque appel à {@link #next}.
     */
    private long nextSubTick;

    /**
     * Indique si {@link #value} représente une graduation principale.
     */
    private boolean isMajorTick;

    /**
     * Valeurs de {@link #value}, {@link #nextTick} et {@link #nextSubTick0}
     * immédiatement après l'appel de {@link #rewind}.
     */
    private long value0, nextTick0, nextSubTick0;

    /**
     * Valeur de {@link #isMajorTick} immédiatement après l'appel de {@link #rewind}.
     */
    private boolean isMajorTick0;

    /**
     * Calendrier servant à avancer d'une certaine période de temps (jour, semaine, mois...).
     * <strong>Note: Par convention et pour des raisons de performances (pour éviter d'imposer
     * au calendrier de recalculer ses champs trop souvent), ce calendrier devrait toujours
     * contenir la date {@link #nextSubTick}.
     */
    private Calendar calendar;

    /**
     * Objet temporaire à utiliser pour passer des dates
     * en argument à {@link #calendar} et {@link #format}.
     */
    private final Date date = new Date();

    /**
     * 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 DateFormat format;

    /**
     * Code du format utilisé pour construire le champ de date de {@link #format}. Les codes
     * valides sont notamment  {@link DateFormat#SHORT}, {@link DateFormat#MEDIUM} ou {@link
     * DateFormat#LONG}. La valeur -1 indique que le format ne contient pas de champ de date,
     * seulement un champ des heures.
     */
    private transient int dateFormat = -1;

    /**
     * Code du format utilisé pour construire le champ des heures de {@link #format}. Les codes
     * valides sont notamment  {@link DateFormat#SHORT},  {@link DateFormat#MEDIUM}  ou  {@link
     * DateFormat#LONG}. La valeur -1 indique que le format ne contient pas de champ des heures,
     * seulement un champ de date.
     */
    private transient int timeFormat = -1;

    /**
     * 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 pour la graduation d'un axe du temps. La méthode {@link #init}
     * <u>doit</u> être appelée avant que l'itérateur ne soit utilisable.
     *
     * @param timezone Fuseau horaire des dates.
     * @param locale   Conventions à utiliser pour le formatage des dates.
     */
    protected DateIterator(final TimeZone timezone, final Locale locale) {
        assert INTERVAL.length*ROLL_WIDTH == ROLL.length;
        calendar = Calendar.getInstance(timezone, locale);
        this.locale=locale;
    }

    /**
     * Initialise l'itérateur.
     *
     * @param minimum         Date minimale de la première graduation.
     * @param maximum         Date limite des graduations. La dernière graduation
     *                        ne sera pas nécessairement à cette date.
     * @param visualLength    Longueur visuelle de l'axe sur laquelle tracer la graduation.
     *                        Cette longueur doit être exprimée en pixels ou en points.
     * @param visualTickSpace 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(final long  minimum,
                        final long  maximum,
                        final float visualLength,
                        final float visualTickSpacing)
    {
        if (minimum           == this.minimum      &&
            maximum           == this.maximum      &&
            visualLength      == this.visualLength &&
            visualTickSpacing == this.visualTickSpacing)
        {
            rewind();
            return;
        }
        AbstractGraduation.ensureNonNull("visualLength",      visualLength);
        AbstractGraduation.ensureNonNull("visualTickSpacing", visualTickSpacing);
        this.visualLength      = visualLength;
        this.visualTickSpacing = visualTickSpacing;
        this.formatValid       = false;
        this.minimum           = minimum;
        this.maximum           = maximum;
        this.increment         = Math.round((maximum-minimum) *
                                            ((double)visualTickSpacing/(double)visualLength));
        /*
         * Après avoir fait une estimation de l'intervalle d'échantillonage,
         * vérifie si on trouve cette estimation dans le tableau 'INTERVAL'.
         * Si on trouve la valeur exacte, tant mieux! Sinon, on cherchera
         * l'intervalle le plus proche.
         */
        int index = Arrays.binarySearch(INTERVAL, increment);
        if (index < 0) {
            index= ~index;
            if (index == 0) {
                // L'intervalle est plus petit que le
                // plus petit élément de 'INTERVAL'.
                round(Calendar.MILLISECOND);
                findFirstTick();
                return;
            } else if (index>=INTERVAL.length) {
                // L'intervalle est plus grand que le
                // plus grand élément de 'INTERVAL'.
                increment /= YEAR;
                round(Calendar.YEAR);
                increment *= YEAR;
                findFirstTick();
                return;
            } else {
                // L'index pointe vers un intervalle plus grand que
                // l'intervalle demandé. Vérifie si l'intervalle
                // inférieur ne serait pas plus proche.
                if (increment-INTERVAL[index-1] < INTERVAL[index]-increment) {
                    index--;
                }
            }
        }
        this.increment    = INTERVAL[index]; index *= ROLL_WIDTH;
        this.tickAdd      = ROLL[index+0];
        this.tickField    = ROLL[index+1];
        this.subTickAdd   = ROLL[index+2];
        this.subTickField = ROLL[index+3];
        if (subTickField == Calendar.MILLISECOND) {
            subTickAdd *= 10;
        }
        findFirstTick();
    }

    /**
     * Arrondi {@link #increment} à un nombre qui se lit bien. Le nombre
     * choisit sera un de ceux de la suite 1, 2, 5, 10, 20, 50, 100, 200, 500, etc.
     */
    private void round(final int field) {
        int factor=1;
        while (factor <= increment) {
            factor *= 10;
        }
        if (factor >= 10) {
            factor /= 10;
        }
        increment /= factor;
        if      (increment<=0                ) increment= 1;
        else if (increment>=3 && increment<=4) increment= 5;
        else if (increment>=6                ) increment=10;
        increment    = Math.max(increment*factor, 5);
        tickAdd      = (int)increment;
        subTickAdd   = (int)(increment/(increment==2 ? 4 : 5));
        tickField    = field;
        subTickField = field;
    }

    /**
     * Replace l'itérateur sur la première graduation. La position de la première graduation
     * sera calculée et retenue pour un positionnement plus rapide à l'avenir.
     */
    private void findFirstTick() {
        calendar.clear();
        value = minimum;
        date.setTime(value);
        calendar.setTime(date);
        if (true) {
            // Arrondie la date de départ. Note: ce calcul exige que
            // tous les champs commencent à 0 plutôt que 1, y compris
            // les mois et le jour du mois.
            final int offset = calendar.getActualMinimum(tickField);
            int      toRound = calendar.get(tickField)-offset;
            toRound = (toRound/tickAdd)*tickAdd;
            calendar.set(tickField, toRound+offset);
        }
        truncate(calendar, tickField);
        nextTick=calendar.getTime().getTime();
        nextSubTick=nextTick;
        while (nextTick < minimum) {
            calendar.add(tickField, tickAdd);
            nextTick = calendar.getTime().getTime();
        }
        date.setTime(nextSubTick);
        calendar.setTime(date);
        while (nextSubTick < minimum) {
            calendar.add(subTickField, subTickAdd);
            nextSubTick = calendar.getTime().getTime();
        }
        /* 'calendar' a maintenant la valeur 'nextSubTick', comme le veut la spécification de
         * ce champ. On appelle maintenant 'next' pour transférer cette valeur 'nextSubTick'
         * vers 'value'. Notez que 'next' peut être appelée même si value>maximum.
         */
        next();

        // Retient les positions trouvées.
        this.value0       = this.value;
        this.nextTick0    = this.nextTick;
        this.nextSubTick0 = this.nextSubTick;
        this.isMajorTick0 = this.isMajorTick;

        assert calendar.getTime().getTime() == nextSubTick;
    }

    /**
     * Met à 0 tous les champs du calendrier inférieur au champ {@code field}
     * spécifié. Note: si le calendrier spécifié est {@link #calendar},  il est de
     * la responsabilité de l'appelant de restituer {@link #calendar} dans son état
     * correct après l'appel de cette méthode.
     */
    private static void truncate(final Calendar calendar, int field) {
        for (int i=0; i<FIELD.length; i++) {
            if (FIELD[i] == field) {
                calendar.get(field); // Force la mise à jour des champs.
                while (++i < FIELD.length) {
                    field = FIELD[i];
                    calendar.set(field, calendar.getActualMinimum(field));
                }
                break;
            }
        }
    }

    /**
     * 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 isMajorTick;
    }

    /**
     * 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}.
     */
    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) {
            date.setTime(minimum);
            calendar.setTime(date);
            final int firstDay =calendar.get(Calendar.DAY_OF_YEAR);
            final int firstYear=calendar.get(Calendar.YEAR);

            date.setTime(maximum);
            calendar.setTime(date);
            final int lastDay =calendar.get(Calendar.DAY_OF_YEAR);
            final int lastYear=calendar.get(Calendar.YEAR);

            final int dateFormat = (firstYear==lastYear && firstDay==lastDay) ? -1 : DateFormat.MEDIUM;
            final int timeFormat;

            if      (increment >= DAY) timeFormat= -1;
            else if (increment >= MIN) timeFormat=DateFormat.SHORT ;
            else if (increment >= SEC) timeFormat=DateFormat.MEDIUM;
            else                       timeFormat=DateFormat.LONG  ;

            if (dateFormat!=this.dateFormat || timeFormat!=this.timeFormat || format==null) {
                this.dateFormat = dateFormat;
                this.timeFormat = timeFormat;
                if (dateFormat == -1) {
                    if (timeFormat == -1) {
                        format = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
                    } else {
                        format = DateFormat.getTimeInstance(timeFormat, locale);
                    }
                } else if (timeFormat == -1) {
                    format = DateFormat.getDateInstance(dateFormat, locale);
                } else {
                    format = DateFormat.getDateTimeInstance(dateFormat, timeFormat, locale);
                }
                format.setCalendar(calendar);
            }
            formatValid = true;
        }
        date.setTime(value);
        final String label = format.format(date);
        // Remet 'calendar' dans l'état qu'il est sencé avoir
        // d'après la spécification du champ {@link #calendar}.
        date.setTime(nextSubTick);
        calendar.setTime(date);
        return label;
    }

    /**
     * Passe à la graduation suivante.
     */
    public void next() {
        assert calendar.getTime().getTime() == nextSubTick;
        if (nextSubTick < nextTick) {
            isMajorTick = false;
            value = nextSubTick;
            /*
             * IMPORTANT: On suppose ici que 'calendar' a déjà la date 'nextSubTick'. Si ce
             *            n'était pas le cas, il faudrait ajouter les lignes suivantes:
             */
            if (false) {
                date.setTime(value);
                calendar.setTime(date);
                // 'setTime' oblige 'calendar' à recalculer ses
                // champs, ce qui a un impact sur la performance.
            }
            calendar.add(subTickField, subTickAdd);
            nextSubTick=calendar.getTime().getTime();
            // 'calendar' contient maintenant la date 'nextSubTick',
            // comme le veut la spécification du champ {@link #calendar}.
        } else {
            nextMajor();
        }
    }

    /**
     * Passe directement à la graduation majeure suivante.
     */
    public void nextMajor() {
        isMajorTick = true;
        value = nextTick;
        date.setTime(value);

        calendar.setTime(date);
        calendar.add(tickField, tickAdd);
        truncate(calendar, tickField);
        nextTick=calendar.getTime().getTime();

        calendar.setTime(date);
        calendar.add(subTickField, subTickAdd);
        nextSubTick=calendar.getTime().getTime();
        // 'calendar' contient maintenant la date 'nextSubTick',
        // comme le veut la spécification du champ {@link #calendar}.
    }

    /**
     * Replace l'itérateur sur la première graduation.
     */
    public void rewind() {
        this.value       = value0;
        this.nextTick    = nextTick0;
        this.nextSubTick = nextSubTick0;
        this.isMajorTick = isMajorTick0;
        // Pour être en accord avec la spécification
        // du champs {@link #calendar}...
        date.setTime(nextSubTick);
        calendar.setTime(date);
    }

    /**
     * Retourne les conventions à utiliser pour écrire les étiquettes de graduation.
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Modifie les conventions à utiliser pour écrire les étiquettes de graduation.
     */
    public void setLocale(final Locale locale) {
        if (!locale.equals(this.locale)) {
            calendar    = Calendar.getInstance(getTimeZone(), locale);
            format      = null;
            formatValid = false;
            // Pour être en accord avec la spécification du champs {@link #calendar}...
            date.setTime(nextSubTick);
            calendar.setTime(date);
        }
        assert calendar.getTime().getTime() == nextSubTick;
    }

    /**
     * Retourne le fuseau horaire utilisé pour exprimer les dates dans la graduation.
     */
    public TimeZone getTimeZone() {
        return calendar.getTimeZone();
    }

    /**
     * Modifie le fuseau horaire utilisé pour exprimer les dates dans la graduation.
     */
    public void setTimeZone(final TimeZone timezone) {
        if (!timezone.equals(getTimeZone())) {
            calendar.setTimeZone(timezone);
            format      = null;
            formatValid = false;
            // Pour être en accord avec la spécification
            // du champs {@link #calendar}...
            date.setTime(nextSubTick);
            calendar.setTime(date);
        }
        assert calendar.getTime().getTime() == nextSubTick;
    }

    /**
     * Retourne le nom du champ de {@link Calendar} correspondant à la valeur spécifiée.
     */
    private static String getFieldName(final int field) {
        for (int i=0; i<FIELD.length; i++) {
            if (FIELD[i] == field) {
                return FIELD_NAME[i];
            }
        }
        return String.valueOf(field);
    }

    /**
     * Returns a string representation of this iterator. Used for debugging purpose only.
     */
    public String toString() {
        if (true) {
            // Note: in this particular case, using PrintWriter with 'println' generates
            //       less bytecodes than chaining StringBuffer.append(...) calls.
            final StringWriter  buf = new StringWriter();
            final PrintWriter   out = new PrintWriter(buf);
            final DateFormat format = DateFormat.getDateTimeInstance();
            format.setTimeZone(calendar.getTimeZone());
            out.print("Minimum      = "); out.println(format.format(new Date(minimum)));
            out.print("Maximum      = "); out.println(format.format(new Date(maximum)));
            out.print("Increment    = "); out.print(increment/(24*3600000f));    out.println(" days");
            out.print("Tick inc.    = "); out.print(   tickAdd); out.print(' '); out.println(getFieldName(   tickField));
            out.print("SubTick inc. = "); out.print(subTickAdd); out.print(' '); out.println(getFieldName(subTickField));
            out.print("Next tick    = "); out.println(format.format(new Date(   nextTick)));
            out.print("Next subtick = "); out.println(format.format(new Date(nextSubTick)));
            out.flush();
            return buf.toString();
        } else {
            return super.toString();
        }
    }
}
