/*
* 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.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.IllegalPathStateException;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Locale;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.io.Serializable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import static java.lang.Double.isNaN;
import static java.lang.Double.NaN;
import org.opengis.util.Cloneable;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.AxisDirection;
import org.geotools.resources.Classes;
import org.geotools.resources.geometry.XDimension2D;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.referencing.operation.matrix.XAffineTransform;
/**
* An axis as a graduated line. {@code Axis2D} objets are really {@link Line2D}
* objects with a {@link Graduation}. Because axis are {@link Line2D}, they can be
* located anywhere in a widget with any orientation. Lines are drawn from starting
* point
* ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
* to end point
* ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}),
* using a graduation from minimal value {@link Graduation#getMinimum} to maximal
* value {@link Graduation#getMaximum}.
*
* Note the line's coordinates (<var>x<sub>1</sub></var>,<var>y<sub>1</sub></var>) and
* (<var>x<sub>2</sub></var>,<var>y<sub>2</sub></var>) are completly independant of
* graduation minimal and maximal values. Line's coordinates should be expressed in
* some units convenient for rendering, as pixels or point (1/72 of inch). On the
* opposite, graduation can have any arbitrary units, which is given by
* {@link Graduation#getUnit}. The static method {@link #createAffineTransform} can
* be used for mapping logical coordinates to pixels coordinates for an arbitrary
* pair of {@code Axis2D} objects, which doesn't need to be perpendicular.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*
* @see DefaultCoordinateSystemAxis
* @see AxisDirection
* @see Graduation
*/
public class Axis2D extends Line2D implements Cloneable, Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = -8396436909942389360L;
/**
* Coordonnées des premier et dernier points de l'axe. Ces coordonnées
* sont exprimées en "points" (1/72 de pouce), ce qui n'a rien à voir
* avec les unités de {@link Graduation#getMinimum} et {@link Graduation#getMaximum}.
*/
private float x1=8, y1=8, x2=648, y2=8;
/**
* Longueur des graduations, en points. Chaque graduations sera tracée à partir de
* {@code [sub]TickStart} (généralement 0) jusqu'à {@code [sub]TickEnd}.
* Par convention, des valeurs positives désignent l'intérieur du graphique et des
* valeurs négatives l'extérieur.
*/
private float tickStart=0, tickEnd=9, subTickStart=0, subTickEnd=5;
/**
* Indique dans quelle direction se trouve la graduation de l'axe. La valeur -1 indique
* qu'il faudrait tourner l'axe dans le sens des aiguilles d'une montre pour qu'il soit
* par-dessus sa graduation. La valeur +1 indique au contraire qu'il faudrait le tourner
* dans le sens inverse des aiguilles d'une montre pour le même effet.
*/
private byte relativeCCW = +1;
/**
* Modèle qui contient les minimum, maximum et la graduation de l'axe.
*/
private final Graduation graduation;
/**
* The coordinate system axis object associated to this axis, or {@code null} if it has
* not been created yet.
*/
private transient DefaultCoordinateSystemAxis information;
/**
* Compte le nombre de modifications apportées à l'axe,
* afin de détecter les changements faits pendant qu'un
* itérateur balaye la graduation.
*/
private transient int modCount;
/**
* Indique si {@link #getPathIterator} doit retourner {@link #iterator}.
* Ce champ prend temporairement la valeur de {@code true} pendant
* l'exécution de {@link #paint}.
*/
private transient boolean isPainting;
/**
* Itérateur utilisé pour dessiner l'axe lors du dernier appel de
* la méthode {@link #paint}. Cet itérateur sera réutilisé autant
* que possible afin de diminuer le nombre d'objets créés lors de
* chaque traçage.
*/
private transient TickPathIterator iterator;
/**
* Coordonnées de la boîte englobant l'axe (<u>sans</u> ses étiquettes
* de graduation) lors du dernier traçage par la méthode {@link #paint}.
* Ces coordonnées sont indépendantes de {@link #lastContext} et ont été
* obtenues sans transformation affine "utilisateur".
*/
private transient Rectangle2D axisBounds;
/**
* Coordonnées de la boîte englobant les étiquettes de graduations (<u>sans</u>
* le reste de l'axe) lors du dernier traçage par la méthode {@link #paint}. Ces
* coordonnées ont été calculées en utilisant {@link #lastContext} mais ont été
* obtenues sans transformation affine "utilisateur".
*/
private transient Rectangle2D labelBounds;
/**
* Coordonnées de la boîte englobant la légende de l'axe lors du dernier traçage
* par la méthode {@link #paint}. Ces coordonnées ont été calculées en utilisant
* {@link #lastContext} mais ont été obtenues sans transformation affine "utilisateur".
*/
private transient Rectangle2D legendBounds;
/**
* Dernier objet {@link FontRenderContext} a avoir été
* utilisé lors du traçage par la méthode {@link #paint}.
*/
private transient FontRenderContext lastContext;
/**
* Largeur et hauteur maximales des étiquettes de la graduation, ou
* {@code null} si cette dimension n'a pas encore été déterminée.
*/
private transient Dimension2D maximumSize;
/**
* A default font to use when no rendering hint were provided for the
* {@link Graduation#TICK_LABEL_FONT} key. Cached here only for performance.
*/
private transient Font defaultFont;
/**
* A set of rendering hints for this axis.
*/
private RenderingHints hints;
/**
* Constructs an axis with a default {@link NumberGraduation}.
*/
public Axis2D() {
this(new NumberGraduation(null));
}
/**
* Constructs an axis with the specified graduation.
*/
public Axis2D(final Graduation graduation) {
this.graduation = graduation;
graduation.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent event) {
synchronized (Axis2D.this) {
modCount++;
clearCache();
}
}
});
}
/**
* Returns the axis's graduation.
*/
public Graduation getGraduation() {
return graduation;
}
/**
* Returns the <var>x</var> coordinate of the start point. By convention,
* this coordinate should be in pixels or points (1/72 of inch) for proper
* positionning of ticks and labels.
*
* @see #getY1
* @see #getX2
* @see #setLine
*/
public double getX1() {
return x1;
}
/**
* Returns the <var>x</var> coordinate of the end point. By convention,
* this coordinate should be in pixels or points (1/72 of inch) for proper
* positionning of ticks and labels.
*
* @see #getY2
* @see #getX1
* @see #setLine
*/
public double getX2() {
return x2;
}
/**
* Returns the <var>y</var> coordinate of the start point. By convention,
* this coordinate should be in pixels or points (1/72 of inch) for proper
* positionning of ticks and labels.
*
* @see #getX1
* @see #getY2
* @see #setLine
*/
public double getY1() {
return y1;
}
/**
* Returns the <var>y</var> coordinate of the end point. By convention,
* this coordinate should be in pixels or points (1/72 of inch) for proper
* positionning of ticks and labels.
*
* @see #getX2
* @see #getY1
* @see #setLine
*/
public double getY2() {
return y2;
}
/**
* Returns the (<var>x</var>,<var>y</var>) coordinates of the start point.
* By convention, those coordinates should be in pixels or points (1/72 of
* inch) for proper positionning of ticks and labels.
*/
public synchronized Point2D getP1() {
return new Point2D.Float(x1,y1);
}
/**
* Returns the (<var>x</var>,<var>y</var>) coordinates of the end point.
* By convention, those coordinates should be in pixels or points (1/72 of
* inch) for proper positionning of ticks and labels.
*/
public synchronized Point2D getP2() {
return new Point2D.Float(x2,y2);
}
/**
* Returns the axis length. This is the distance between starting point (@link #getP1 P1})
* and end point ({@link #getP2 P2}). This length is usually measured in pixels or points
* (1/72 of inch).
*/
public synchronized double getLength() {
return Math.hypot(getX1() - getX2(), getY1() - getY2());
}
/**
* Returns a bounding box for this axis. The bounding box includes the axis's line
* ({@link #getP1 P1}) to ({@link #getP2 P2}), the axis's ticks and all labels.
*
* @see #getX1
* @see #getY1
* @see #getX2
* @see #getY2
*/
public synchronized Rectangle2D getBounds2D() {
if (axisBounds == null) {
paint(null); // Force the computation of bounding box size.
}
final Rectangle2D bounds = (Rectangle2D) axisBounds.clone();
if (labelBounds !=null) bounds.add(labelBounds );
if (legendBounds!=null) bounds.add(legendBounds);
return bounds;
}
/**
* Sets the location of the endpoints of this {@code Axis2D} to the specified
* coordinates. Coordinates should be in pixels (for screen rendering) or points
* (for paper rendering). Using points units make it easy to render labels with
* a raisonable font size, no matter the screen resolution or the axis graduation.
*
* @param x1 Coordinate <var>x</var> of starting point.
* @param y1 Coordinate <var>y</var> of starting point
* @param x2 Coordinate <var>x</var> of end point.
* @param y2 Coordinate <var>y</var> of end point.
* @throws IllegalArgumentException If a coordinate is {@code NaN} or infinite.
*
* @see #getX1
* @see #getY1
* @see #getX2
* @see #getY2
*/
public synchronized void setLine(final double x1, final double y1,
final double x2, final double y2)
throws IllegalArgumentException
{
final float fx1 = (float) x1; AbstractGraduation.ensureFinite("x1", fx1);
final float fy1 = (float) y1; AbstractGraduation.ensureFinite("y1", fy1);
final float fx2 = (float) x2; AbstractGraduation.ensureFinite("x2", fx2);
final float fy2 = (float) y2; AbstractGraduation.ensureFinite("y2", fy2);
modCount++; // Must be first
this.x1 = fx1;
this.y1 = fy1;
this.x2 = fx2;
this.y2 = fy2;
clearCache();
}
/**
* Returns {@code true} if the axis would have to rotate clockwise in order to
* overlaps its graduation.
*/
public boolean isLabelClockwise() {
return relativeCCW < 0;
}
/**
* Sets the label's locations relative to this axis. Value {@code true} means
* that the axis would have to rotate clockwise in order to overlaps its graduation.
* Value {@code false} means that the axis would have to rotate counter-clockwise
* in order to overlaps its graduation.
*/
public synchronized void setLabelClockwise(final boolean c) {
modCount++; // Must be first
relativeCCW = c ? (byte) -1 : (byte) +1;
}
/**
* Returns a default font to use when no rendering hint were provided for
* the {@link Graduation#TICK_LABEL_FONT} key.
*
* @return A default font (never {@code null}).
*/
private synchronized Font getDefaultFont() {
if (defaultFont == null) {
defaultFont = new Font("SansSerif", Font.PLAIN, 9);
}
return defaultFont;
}
/**
* Returns an iterator object that iterates along the {@code Axis2D} boundary
* and provides access to the geometry of the shape outline. The shape includes
* the axis line, graduation and labels. If an optional {@link AffineTransform}
* is specified, the coordinates returned in the iteration are transformed accordingly.
*/
@Override
public PathIterator getPathIterator(final AffineTransform transform) {
return getPathIterator(transform, NaN);
}
/**
* Returns an iterator object that iterates along the {@code Axis2D} boundary
* and provides access to the geometry of the shape outline. The shape includes
* the axis line, graduation and labels. If an optional {@link AffineTransform}
* is specified, the coordinates returned in the iteration are transformed accordingly.
*/
@Override
public synchronized PathIterator getPathIterator(final AffineTransform transform,
final double flatness)
{
if (isPainting) {
if (iterator != null) {
iterator.rewind(transform);
} else {
iterator = new TickPathIterator(transform);
}
return iterator;
}
return new CompletePathIterator(transform, flatness);
}
/**
* Draw this axis in the specified graphics context. This method is equivalents
* to {@code Graphics2D.draw(this)}. However, this method may be slightly
* faster and produce better quality output.
*
* @param graphics The graphics context to use for drawing.
*/
public synchronized void paint(final Graphics2D graphics) {
if (!(getLength()>0)) {
return;
}
/*
* Initialise l'itérateur en appelant 'init' (contrairement à 'getPathIterator'
* qui n'appelle que 'rewind') pour des résultats plus rapides et plus constants.
*/
if (iterator != null) {
iterator.init(null);
} else {
iterator = new TickPathIterator(null);
}
final TickPathIterator iterator = this.iterator;
final boolean sameContext;
final Shape clip;
if (graphics != null) {
clip = graphics.getClip();
iterator.setFontRenderContext(graphics.getFontRenderContext());
iterator.setRenderingHint(graphics, Graduation.AXIS_TITLE_FONT);
iterator.setRenderingHint(graphics, Graduation.TICK_LABEL_FONT);
final FontRenderContext context = iterator.getFontRenderContext();
sameContext = clip!=null && context.equals(lastContext);
} else {
clip = null;
sameContext = false;
iterator.setFontRenderContext(null);
}
/*
* Calcule (si ce n'était pas déjà fait) les coordonnées d'un rectangle qui englobe l'axe et
* sa graduation (mais sans les étiquettes de graduation). Cette information nous permettra
* de vérifier s'il est vraiment nécessaire de redessiner l'axe en vérifiant s'il intercepte
* avec le "clip" du graphique.
*/
if (axisBounds == null) {
axisBounds = new Rectangle2D.Float(Math.min(x1,x2), Math.min(y1,y2),
Math.abs(x2-x1), Math.abs(y2-y1));
while (!iterator.isDone()) {
axisBounds.add(iterator.point);
iterator.next();
}
}
/*
* Dessine l'axe et ses barres de graduation (mais sans les étiquettes).
*/
if (graphics != null) {
if (clip==null || clip.intersects(axisBounds)) try {
isPainting = true;
graphics.draw(this);
} finally {
isPainting = false;
}
}
/*
* Dessine les étiquettes de graduations. Ce bloc peut etre exécuté même si
* 'graphics' est nul. Dans ce cas, les étiquettes ne seront pas dessinées
* mais le calcul de l'espace qu'elles occupent sera quand même effectué.
*/
if (!sameContext || labelBounds==null || clip.intersects(labelBounds) || maximumSize==null)
{
Rectangle2D lastLabelBounds = labelBounds = null;
double maxWidth = 0;
double maxHeight = 0;
iterator.rewind();
while (iterator.hasNext()) {
if (iterator.isMajorTick()) {
final GlyphVector glyphs = iterator.currentLabelGlyphs();
final Rectangle2D bounds = iterator.currentLabelBounds();
if (glyphs!=null && bounds!=null) {
if (lastLabelBounds==null || !lastLabelBounds.intersects(bounds)) {
if (graphics!=null && (clip==null || clip.intersects(bounds))) {
graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(),
(float)bounds.getMaxY());
}
lastLabelBounds = bounds;
final double width = bounds.getWidth();
final double height = bounds.getHeight();
if (width > maxWidth) maxWidth =width;
if (height > maxHeight) maxHeight=height;
}
if (labelBounds == null) {
labelBounds = new Rectangle2D.Float();
labelBounds.setRect(bounds);
} else {
labelBounds.add(bounds);
}
}
}
iterator.nextMajor();
}
maximumSize = new XDimension2D.Float((float)maxWidth, (float)maxHeight);
}
/*
* Ecrit la légende de l'axe. Ce bloc peut etre exécuté même si
* 'graphics' est nul. Dans ce cas, la légende ne sera pas écrite
* mais le calcul de l'espace qu'elle occupe sera quand même effectué.
*/
if (!sameContext || legendBounds==null || clip.intersects(legendBounds)) {
final String title = graduation.getTitle(true);
if (title != null) {
final Font font = iterator.getTitleFont();
final GlyphVector glyphs = font.createGlyphVector(iterator.getFontRenderContext(), title);
final AffineTransform rotatedTr = new AffineTransform();
final Rectangle2D bounds = iterator.centerAxisLabel(glyphs.getVisualBounds(),
rotatedTr, maximumSize);
if (graphics != null) {
final AffineTransform currentTr = graphics.getTransform();
try {
graphics.transform(rotatedTr);
graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(),
(float)bounds.getMaxY());
} finally {
graphics.setTransform(currentTr);
}
}
legendBounds = XAffineTransform.transform(rotatedTr, bounds, bounds);
}
}
lastContext = iterator.getFontRenderContext();
}
/**
* Returns the value of a single preference for the rendering algorithms. Hint categories
* include controls for label fonts and colors. Some of the keys and their associated values
* are defined in the {@link Graduation} interface.
*
* @param key The key corresponding to the hint to get.
* @return An object representing the value for the specified hint key, or {@code null}
* if no value is associated to the specified key.
*
* @see Graduation#TICK_LABEL_FONT
* @see Graduation#AXIS_TITLE_FONT
*/
public synchronized Object getRenderingHint(final RenderingHints.Key key) {
return (hints!=null) ? hints.get(key) : null;
}
/**
* Sets the value of a single preference for the rendering algorithms. Hint categories
* include controls for label fonts and colors. Some of the keys and their associated
* values are defined in the {@link Graduation} interface.
*
* @param key The key of the hint to be set.
* @param value The value indicating preferences for the specified hint category.
* A {@code null} value removes any hint for the specified key.
*
* @see Graduation#TICK_LABEL_FONT
* @see Graduation#AXIS_TITLE_FONT
*/
public synchronized void setRenderingHint(final RenderingHints.Key key, final Object value) {
modCount++;
if (value != null) {
if (hints == null) {
hints = new RenderingHints(key, value);
clearCache();
} else {
if (!value.equals(hints.put(key, value))) {
clearCache();
}
}
} else if (hints != null) {
if (hints.remove(key) != null) {
clearCache();
}
if (hints.isEmpty()) {
hints = null;
}
}
}
/**
* Efface la cache interne. Cette méthode doit être appelée
* chaque fois que des propriétés de l'axe ont changées.
*/
private void clearCache() {
axisBounds = null;
labelBounds = null;
legendBounds = null;
maximumSize = null;
information = null;
}
/**
* Returns a string representation of this axis.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this));
return buffer.append("[\"").append(graduation.getTitle(true)).append("\"]").toString();
}
/**
* Returns this axis name and direction. Information include a name (usually the
* {@linkplain Graduation#getTitle graduation title}) and an direction. The direction is usually
* {@linkplain AxisDirection#DISPLAY_UP up} or {@linkplain AxisDirection#DISPLAY_DOWN down}
* for vertical axis, {@linkplain AxisDirection#DISPLAY_RIGHT right} or
* {@linkplain AxisDirection#DISPLAY_LEFT left} for horizontal axis, or
* {@linkplain AxisDirection#OTHER other} otherwise.
*/
public synchronized DefaultCoordinateSystemAxis toCoordinateSystemAxis() {
if (information == null) {
String abbreviation = "z";
AxisDirection direction = AxisDirection.OTHER;
if (x1 == x2) {
if (y1 < y2) {
direction = AxisDirection.DISPLAY_UP;
} else if (y1 > y2) {
direction = AxisDirection.DISPLAY_DOWN;
}
abbreviation = "y";
} else if (y1 == y2) {
if (x1 < x2) {
direction = AxisDirection.DISPLAY_RIGHT;
} else if (x1 > x2) {
direction = AxisDirection.DISPLAY_LEFT;
}
abbreviation = "x";
}
information = new DefaultCoordinateSystemAxis(
Collections.singletonMap(IdentifiedObject.NAME_KEY, graduation.getTitle(false)),
abbreviation, direction, graduation.getUnit());
}
return information;
}
/**
* Creates an affine transform mapping logical to pixels coordinates for a pair
* of axis. The affine transform will maps coordinates in the following way:
*
* <ul>
* <li>For each input coordinates (<var>x</var>,<var>y</var>), the <var>x</var> and
* <var>y</var> values are expressed in the same units than the {@code xAxis}
* and {@code yAxis} graduations, respectively.</li>
* <li>The output point is the pixel's coordinates for the (<var>x</var>,<var>y</var>)
* values. Changing the <var>x</var> value move the pixel location in parallel with
* the {@code xAxis}, which may or may not be horizontal. Changing the <var>y</var>
* value move the pixel location in parallel with the {@code yAxis}, which may or
* may not be vertical.</li>
* </ul>
*
* @param xAxis The <var>x</var> axis. This axis doesn't have to be horizontal;
* it can have any orientation, including vertical.
* @param yAxis The <var>y</var> axis. This axis doesn't have to be vertical;
* it can have any orientation, including horizontal.
* @return An affine transform mapping logical to pixels coordinates.
*/
public static AffineTransform createAffineTransform(final Axis2D xAxis, final Axis2D yAxis) {
/* x
* |
* |\
* | \ P Soit: X : l'axe des <var>x</var> du graphique.
* | | Y : l'axe des <var>y</var> du graphique.
* \ | P : un point à placer sur le graphique.
* \| (Px,Py) : les composantes du point P selon les axes x et y.
* \ y (Pi,Pj) : les composantes du point P en coordonnées "pixels".
*
* Désignons par <b>ex</b> et <b>ey</b> des vecteurs unitaires dans la direction de l'axe des
* <var>x</var> et l'axe des <var>y</var> respectivement. Désignons par <b>i</b> et <b>j</b>
* des vecteurs unitaires vers le droite et vers le haut de l'écran respectivement. On peut
* décomposer les vecteurs unitaires <b>ex</b> et <b>ey</b> par:
*
* ex = exi*i + exj*j
* ey = eyi*i + eyj*j
* Donc, P = Px*ex + Py*ey = (Px*exi+Py*eyi)*i + (Px*exj + Py*eyj)*j
*
* Cette relation ne s'applique que si les deux systèmes de coordonnées (xy et ij) ont
* la même origine. En pratique, ce ne sera pas le cas. Il faut donc compliquer un peu:
*
* Pi = (Px-Ox)*exi+(Py-Oy)*eyi + Oi où (Ox,Oy) sont les minimums des axes des x et y.
* Pj = (Px-Ox)*exj+(Py-Oy)*eyj + Oj (Oi,Oj) est l'origine du système d'axe ij.
*
* [ Pi ] [ exi eyi Oi-(Ox*exi+Oy*eyi) ][ Px ] [ exi*Px + eyi*Py + Oi-(Ox*exi+Oy*oyi) ]
* [ Pj ] = [ exj eyj Oj-(Ox*exj+Oy*eyj) ][ Py ] = [ exj*Px + eyj*Py + Oj-(Ox*exj+Oy*oyj) ]
* [ 1 ] [ 0 0 1 ][ 1 ] [ 1 ]
*/
synchronized (xAxis) {
synchronized (yAxis) {
final Graduation mx = xAxis.getGraduation();
final Graduation my = yAxis.getGraduation();
double ox = mx.getRange();
double oy = my.getRange();
double exi = (xAxis.getX2() - xAxis.getX1()) / ox;
double exj = (xAxis.getY2() - xAxis.getY1()) / ox;
double eyi = (yAxis.getX2() - yAxis.getX1()) / oy;
double eyj = (yAxis.getY2() - yAxis.getY1()) / oy;
ox = mx.getMinimum();
oy = my.getMinimum();
return new AffineTransform(exi, exj, eyi, eyj,
xAxis.x1 - (ox*exi+oy*eyi),
yAxis.y1 - (ox*exj+oy*eyj));
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// TICK AND PATH ITERATORS ////////
//////// ////////
////////////////////////////////////////////////////////////////////////////////////////////
/**
* Iterates along the graduation ticks and provides access to the graduation values. Each
* {@code Axis2D.TickIterator} object traverses the graduation of the unclosing {@link Axis2D}
* object independently from any other {@link TickIterator} objects in use at the same time.
* If a change occurs in the underlying {@link Axis2D} object during the iteration, then
* {@link #refresh} must be invoked in order to reset the iterator as if a new instance was
* created. Except for {@link #refresh} method, using the iterator after a change in the
* underlying {@link Axis2D} may thrown a {@link ConcurrentModificationException}.
*
* @since 2.0
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*/
public class TickIterator implements org.geotools.axis.TickIterator {
/**
* The underyling tick iterator.
*/
private org.geotools.axis.TickIterator iterator;
/**
* A copy of {@link Axis2D#hints} rendering hints. A copy is required because some hints
* (especially {@link Graduation#VISUAL_AXIS_LENGTH} and
* {@link Graduation#VISUAL_TICK_SPACING}) are going to be overwriten. This set may also
* contains additional hints provided by {@link Graphics2D} in the {@link Axis2D#paint}
* method. This object will never be {@code null}.
*/
private final RenderingHints hints;
/**
* {@code scaleX} and {@code scaleY} are used for scaling
* logical coordinates to pixel coordinates. Those scale factors
* <strong>must</strong> be the same than the one that appears in
* {@link Axis2D#createAffineTransform}.
*/
private double scaleX, scaleY;
/**
* {@code (tickX, tickY)} is a unitary vector perpendicular to the axis.
*/
private double tickX, tickY;
/**
* The minimum value {@link Graduation#getMinimum}.
* This value is copied here for faster access.
*/
private double minimum;
/**
* Value returned by the last call to {@link #currentLabel}.
* This value is cached here in order to avoid that
* {@link #getGlyphVector} compute it again.
*/
private transient String label;
/**
* Value returned by the last call to {@link #getGlyphVector}. This
* value is cached here in order to avoir that {@link #getBounds}
* compute it again.
*/
private transient GlyphVector glyphs;
/**
* The font to use for rendering tick. If a rendering hint was provided for the
* {@link Graduation#TICK_LABEL_FONT} key, then the value is used as the font.
* Otherwise, a default font is created and used.
*/
private transient Font font;
/**
* The font context from {@link Graphics2D#getFontContext},
* or {@code null} for a default one.
*/
private transient FontRenderContext fontContext;
/**
* Value of {@link Axis2D#modCount} when {@link #init} was
* last invoked. This value is used in order to detect
* changes to the underlying {@link Axis2D} during iteration.
*/
private transient int modCount;
/**
* Construct an iterator.
*
* @param fontContext Information needed to correctly measure text, or
* {@code null} if unknow. This object is usually given by
* {@link Graphics2D#getFontRenderContext}.
*/
public TickIterator(final FontRenderContext fontContext) {
this.hints = new RenderingHints(null);
this.hints.putAll(Axis2D.this.hints);
this.fontContext = fontContext;
refresh();
}
/**
* Copy a rendering hints from the specified {@link Graphics2D}, providing that
* it is not already defined.
*/
final void setRenderingHint(final Graphics2D graphics, final RenderingHints.Key key) {
if (hints.get(key) == null) {
final Object value = graphics.getRenderingHint(key);
if (value != null) {
hints.put(key, value);
}
}
}
/**
* Tests if the iterator has more ticks.
*/
public boolean hasNext() {
return iterator.hasNext();
}
/**
* Tests if the current tick is a major one.
*
* @return {@code true} if current tick is a major tick,
* or {@code false} if it is a minor tick.
*/
public boolean isMajorTick() {
return iterator.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}. The mean exception is for logarithmic graduation,
* in which the tick position is not proportional to the tick value.
*/
public double currentPosition() {
return iterator.currentPosition();
}
/**
* Returns the value for current tick. The current tick may be major or minor.
*/
public double currentValue() {
return iterator.currentValue();
}
/**
* Returns the coordinates of the intersection point between current tick
* and the underlying axis. Units are the same than axis start point
* ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
* and end point
* ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
* This is usually pixels.
*
* @param dest A destination point that stores the intersection coordinates,
* or {@code null} to create a new {@link Point2D} object.
* @return {@code dest}, or a new {@link Point2D} object if {@code dest} was null.
*/
public Point2D currentPosition(final Point2D dest) {
final double position = currentPosition()-minimum;
final double x = position*scaleX + getX1();
final double y = position*scaleY + getY1();
ensureValid();
if (dest != null) {
dest.setLocation(x,y);
return dest;
}
return new Point2D.Float((float)x, (float)y);
}
/**
* Returns the coordinates of the current tick.
* Units are the same than axis start point
* ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
* and end point
* ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
* This is usually pixels.
*
* @param dest A destination line that stores the current tick coordinates,
* or {@code null} to create a new {@link Line2D} object.
* @return {@code dest}, or a new {@link Line2D} object if {@code dest} was null.
*/
public Line2D currentTick(final Line2D dest) {
final boolean isMajorTick = isMajorTick();
final double position = currentPosition()-minimum;
final double x = position*scaleX + getX1();
final double y = position*scaleY + getY1();
final double s1 = isMajorTick ? tickStart : subTickStart;
final double s2 = isMajorTick ? tickEnd : subTickEnd;
final double x1 = x+tickX*s1;
final double y1 = y+tickY*s1;
final double x2 = x+tickX*s2;
final double y2 = y+tickY*s2;
ensureValid();
if (dest != null) {
dest.setLine(x1, y1, x2, y2);
return dest;
}
return new Line2D.Float((float)x1, (float)y1, (float)x2, (float)y2);
}
/**
* Returns the label for current tick. This method is usually invoked
* only for major ticks, but may be invoked for minor ticks as well.
* This method returns {@code null} if it can't produces a label
* for current tick.
*/
public String currentLabel() {
if (label == null) {
label = iterator.currentLabel();
}
return label;
}
/**
* Returns the label for current tick as a glyphs vector. This method is used
* together with {@link #currentLabelBounds} for labels rendering. <strong>Do
* not change the returned {@link GlyphVector}</strong>, since the glyphs vector
* is not cloned for performance raisons. This method returns {@code null}
* if it can't produces a glyph vector for current tick.
*/
public GlyphVector currentLabelGlyphs() {
if (glyphs == null) {
final String label = currentLabel();
if (label != null) {
glyphs = getTickFont().createGlyphVector(getFontRenderContext(), label);
}
}
return glyphs;
}
/**
* Returns a bounding vector for the current tick label.
* Units are the same than axis start point
* ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
* and end point
* ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
* This is usually pixels. This method can be used as in the example below:
*
* <pre>
* {@link Axis2D.TickIterator} iterator = axis.new {@link Axis2D.TickIterator TickIterator}(null};
* while (iterator.{@link #hasNext() hasNext()}) {
* {@link GlyphVector} glyphs = iterator.{@link Axis2D.TickIterator#currentLabelGlyphs() currentLabelGlyphs()};
* {@link Rectangle2D} bounds = iterator.{@link Axis2D.TickIterator#currentLabelBounds() currentLabelBounds()};
* graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(), (float)bounds.getMaxY());
* iterator.{@link #next() next()};
* }
* </pre>
*
* This method returns {@code null} if it can't compute bounding box for current tick.
*/
public Rectangle2D currentLabelBounds() {
final GlyphVector glyphs = currentLabelGlyphs();
if (glyphs == null) {
return null;
}
final Rectangle2D bounds = glyphs.getVisualBounds();
final double height = bounds.getHeight();
final double width = bounds.getWidth();
final double tickStart = (0.5*height)-Math.min(Axis2D.this.tickStart, 0);
final double position = currentPosition()-minimum;
final double x= position*scaleX + getX1();
final double y= position*scaleY + getY1();
bounds.setRect(x - (1+tickX)*(0.5*width) - tickX*tickStart,
y + (1-tickY)*(0.5*height) - tickY*tickStart - height,
width, height);
ensureValid();
return bounds;
}
/**
* Returns the font for tick labels. This is the font used for drawing the tick label
* formatted by {@link TickIterator#currentLabel}.
*
* @return The font (never {@code null}).
*/
private Font getTickFont() {
if (font == null) {
Object candidate = hints.get(Graduation.TICK_LABEL_FONT);
if (candidate instanceof Font) {
font = (Font) candidate;
} else {
font = getDefaultFont();
}
}
return font;
}
/**
* Returns the font for axis title. This is the font used for drawing the title
* formatted by {@link Graduation#getTitle}.
*
* @return The font (never {@code null}).
*/
final Font getTitleFont() {
Object candidate = hints.get(Graduation.AXIS_TITLE_FONT);
if (candidate instanceof Font) {
return (Font) candidate;
}
final Font font = getTickFont();
return font.deriveFont(Font.BOLD, font.getSize2D() * (12f/9));
}
/**
* Retourne un rectangle centré vis-à-vis l'axe. Les coordonnées de ce rectangle seront
* les mêmes que celles de l'axe, habituellement des pixels ou des points (1/72 de pouce).
* Cette méthode s'utilise typiquement comme suit:
*
* <pre>
* Graphics2D graphics = ...
* FontRenderContext fontContext = graphics.getFontRenderContext();
* TickIterator iterator = axis.new TickIterator(graphics.getFontRenderContext());
* Font font = iterator.getTitleFont();
* String title = axis.getGraduation().getTitle(true);
* GlyphVector glyphs = font.createGlyphVector(fontContext, title);
* Rectangle2D bounds = centerAxisLabel(glyphs.getVisualBounds());
* graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(), (float)bounds.getMaxY());
* </pre>
*
* @param bounds Un rectangle englobant les caractères à écrire. La position
* (<var>x</var>,<var>y</var>) de ce rectangle est généralement
* (mais pas obligatoirement) l'origine (0,0). Ce rectangle est
* habituellement obtenu par un appel à
* {@link Font#createGlyphVector(FontContext,String)}.
* @param toRotate Si non-nul, transformation affine sur laquelle appliquer une rotation
* égale à l'angle de l'axe. Cette méthode peut limiter la rotation aux
* quadrants 1 et 2 afin de conserver une lecture agréable du texte.
* @param maximumSize Largeur et hauteur maximales des étiquettes de graduation. Cette
* information est utilisée pour écarter l'étiquette de l'axe suffisament
* pour qu'elle n'écrase pas les étiquettes de graduation.
* @return Le rectangle {@code bounds}, modifié pour être centré sur l'axe.
*/
final Rectangle2D centerAxisLabel(final Rectangle2D bounds,
final AffineTransform toRotate,
final Dimension2D maximumSize)
{
final double height = bounds.getHeight();
final double width = bounds.getWidth();
final double tx = 0;
final double ty = height + Math.abs(maximumSize.getWidth()*tickX) + Math.abs(maximumSize.getHeight()*tickY);
final double x1 = getX1();
final double y1 = getY1();
final double x2 = getX2();
final double y2 = getY2();
/////////////////////////////////////
//// Compute unit vector (ux,uy) ////
/////////////////////////////////////
double ux = x2 - x1;
double uy = y2 - y1;
double ul = Math.hypot(ux, uy);
ux /= ul;
uy /= ul;
//////////////////////////////////////////////
//// Get the central position of the axis ////
//////////////////////////////////////////////
double x = 0.5 * (x1+x2);
double y = 0.5 * (y1+y2);
////////////////////////////////////////
//// Apply the parallel translation ////
////////////////////////////////////////
x += ux*tx;
y += uy*tx;
////////////////////////////////////
//// Adjust sign of unit vector ////
////////////////////////////////////
ux *= relativeCCW;
uy *= relativeCCW;
/////////////////////////////////////////////
//// Apply the perpendicular translation ////
/////////////////////////////////////////////
x += uy*ty;
y -= ux*ty;
///////////////////////////////////
//// Offset the point for text ////
///////////////////////////////////
final double anchorX = x;
final double anchorY = y;
if (toRotate == null) {
y += 0.5*height * (1-ux);
x -= 0.5*width * (1-uy);
} else {
if (ux < 0) {
ux = -ux;
uy = -uy;
y += height;
}
x -= 0.5*width;
toRotate.rotate(Math.atan2(uy,ux), anchorX, anchorY);
}
bounds.setRect(x, y-height, width, height);
ensureValid();
return bounds;
}
/**
* Moves the iterator to the next minor or major tick.
*/
public void next() {
this.label = null;
this.glyphs = null;
iterator.next();
}
/**
* Moves the iterator to the next major tick. This move ignore any minor ticks
* between current position and the next major tick.
*/
public void nextMajor() {
this.label = null;
this.glyphs = null;
iterator.nextMajor();
}
/**
* Reset the iterator on its first tick. All other properties are left unchanged.
*/
public void rewind() {
this.label = null;
this.glyphs = null;
iterator.rewind();
}
/**
* Reset the iterator on its first tick. If some axis properies have changed (e.g. minimum
* and/or maximum values), then the new settings are taken in account. This {@link #refresh}
* method help to reduce garbage-collection by constructing an {@code Axis2D.TickIterator}
* object only once and reuse it for each axis's rendering.
*/
public void refresh() {
synchronized (Axis2D.this) {
this.label = null;
this.glyphs = null;
// Do NOT modify 'fontContext'.
final Graduation graduation = getGraduation();
final double dx = getX2()-getX1();
final double dy = getY2()-getY1();
final double range = graduation.getRange();
final double length = Math.sqrt(dx*dx + dy*dy);
hints.put(Graduation.VISUAL_AXIS_LENGTH, length);
this.scaleX = dx/range;
this.scaleY = dy/range;
this.tickX = -dy/length*relativeCCW;
this.tickY = +dx/length*relativeCCW;
this.minimum = graduation.getMinimum();
this.iterator = graduation.getTickIterator(hints, iterator);
this.modCount = Axis2D.this.modCount;
}
}
/**
* Returns the locale used for formatting tick labels.
*/
public Locale getLocale() {
return iterator.getLocale();
}
/**
* Retourne le contexte utilisé pour dessiner les caractères.
* Cette méthode ne retourne jamais {@code null}.
*/
final FontRenderContext getFontRenderContext() {
if (fontContext == null) {
fontContext = new FontRenderContext(null, false, false);
}
return fontContext;
}
/**
* Spécifie le contexte à utiliser pour dessiner les caractères,
* ou {@code null} pour utiliser un contexte par défaut.
*/
final void setFontRenderContext(final FontRenderContext context) {
fontContext = context;
}
/**
* Vérifie que l'axe n'a pas changé depuis le dernier appel de {@link #init}.
* Cette méthode doit être appelée <u>à la fin</u> des méthodes de cette classe
* qui lisent les champs de {@link Axis2D}.
*/
final void ensureValid() {
if (this.modCount != Axis2D.this.modCount) {
throw new ConcurrentModificationException();
}
}
}
/**
* Itérateur balayant l'axe et ses barres de graduations pour leur traçage. Cet itérateur ne
* balaye pas les étiquettes de graduations. Puisque cet itérateur ne retourne que des droites
* et jamais de courbes, il ne prend pas d'argument {@code flatness}.
*
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*/
private class TickPathIterator extends TickIterator implements PathIterator {
/**
* Transformation affine à appliquer sur les données. Il doit s'agir d'une transformation
* affine appropriée pour l'écriture de texte (généralement en pixels ou en points). Il ne
* s'agit <u>pas</u> de la transformation affine créée par
* {@link Axis2D#createAffineTransform}.
*/
protected AffineTransform transform;
/**
* Coordonnées de la prochaine graduation à retourner par une des méthodes
* {@code currentSegment(...)}. Ces coordonnées n'auront <u>pas</u>
* été transformées selon la transformation affine {@link #transform}.
*/
private final Line2D.Double line = new Line2D.Double();
/**
* Coordonnées du prochain point à retourner par une des méthodes
* {@code currentSegment(...)}. Ces coordonnées auront été
* transformées selon la transformation affine {@link #transform}.
*/
private final Point2D.Double point = new Point2D.Double();
/**
* Type du prochain segment. Ce type est retourné par les méthodes
* {@code currentSegment(...)}. Il doit s'agir en général d'une
* des constantes {@link #SEG_MOVETO} ou {@link #SEG_LINETO}.
*/
private int type = SEG_MOVETO;
/**
* Entier indiquant quel sera le prochain item a retourner (début ou
* fin d'une graduation, début ou fin de l'axe, etc.). Il doit s'agir
* d'une des constantes {@link #AXIS_MOVETO}, {@link #AXIS_LINETO},
* {@link #TICK_MOVETO}, {@link #TICK_LINETO}, etc.
*/
private int nextType = AXIS_MOVETO;
/** Constante pour {@link #nextType}.*/ private static final int AXIS_MOVETO = 0;
/** Constante pour {@link #nextType}.*/ private static final int AXIS_LINETO = 1;
/** Constante pour {@link #nextType}.*/ private static final int TICK_MOVETO = 2;
/** Constante pour {@link #nextType}.*/ private static final int TICK_LINETO = 3;
/**
* Construit un itérateur.
*
* @param transform Transformation affine à appliquer sur les données. Il doit
* s'agir d'une transformation affine appropriée pour l'écriture de
* texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
* de la transformation affine créée par {@link Axis2D#createAffineTransform}.
*/
public TickPathIterator(final AffineTransform transform) {
super(null);
// 'refresh' est appelée par le constructeur parent.
this.transform=transform;
next();
}
/**
* Initialise cet itérateur. Cette méthode peut être appelée pour réutiliser un itérateur
* qui a déjà servit, plutôt que d'en construire un autre.
*
* @param transform Transformation affine à appliquer sur les données. Il doit
* s'agir d'une transformation affine appropriée pour l'écriture de
* texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
* de la transformation affine créée par {@link Axis2D#createAffineTransform}.
*/
final void init(final AffineTransform transform) {
refresh();
setFontRenderContext(null);
this.type = SEG_MOVETO;
this.nextType = AXIS_MOVETO;
this.transform = transform;
next();
}
/**
* Repositione l'itérateur au début de la graduation
* avec une nouvelle transformation affine.
*/
public void rewind(final AffineTransform transform) {
super.rewind();
// Keep 'fontContext'.
this.type = SEG_MOVETO;
this.nextType = AXIS_MOVETO;
this.transform = transform;
next();
}
/**
* Repositione l'itérateur au début de la graduation
* en conservant la transformation affine actuelle.
*/
@Override
public final void rewind() {
rewind(transform);
}
/**
* Return the winding rule for determining the insideness of the path.
*/
public int getWindingRule() {
return WIND_NON_ZERO;
}
/**
* Tests if the iteration is complete.
*/
public boolean isDone() {
return nextType==TICK_LINETO && !hasNext();
}
/**
* Returns the coordinates and type of the current path segment
* in the iteration. The return value is the path segment type:
* {@code SEG_MOVETO} or {@code SEG_LINETO}.
*/
public int currentSegment(final float[] coords) {
coords[0] = (float) point.x;
coords[1] = (float) point.y;
return type;
}
/**
* Returns the coordinates and type of the current path segment
* in the iteration. The return value is the path segment type:
* {@code SEG_MOVETO} or {@code SEG_LINETO}.
*/
public int currentSegment(final double[] coords) {
coords[0] = point.x;
coords[1] = point.y;
return type;
}
/**
* Moves the iterator to the next segment of the path forwards
* along the primary direction of traversal as long as there are
* more points in that direction.
*/
@Override
public void next() {
switch (nextType) {
default: { // Should not happen
throw new IllegalPathStateException(Integer.toString(nextType));
}
case AXIS_MOVETO: { // Premier point de l'axe
point.x = getX1();
point.y = getY1();
type = SEG_MOVETO;
nextType = AXIS_LINETO;
break;
}
case AXIS_LINETO: { // Fin de l'axe
point.x = getX2();
point.y = getY2();
type = SEG_LINETO;
nextType = TICK_MOVETO;
break;
}
case TICK_MOVETO: { // Premier point d'une graduation
currentTick(line);
point.x = line.x1;
point.y = line.y1;
type = SEG_MOVETO;
nextType = TICK_LINETO;
break;
}
case TICK_LINETO: { // Dernier point d'une graduation
point.x = line.x2;
point.y = line.y2;
type = SEG_LINETO;
nextType = TICK_MOVETO;
prepareLabel();
super.next();
break;
}
}
if (transform != null) {
transform.transform(point, point);
}
ensureValid();
}
/**
* Méthode appelée automatiquement par {@link #next} pour
* indiquer qu'il faudra se préparer à tracer une étiquette.
*/
protected void prepareLabel() {
}
}
/**
* Itérateur balayant l'axe et ses barres de graduations pour leur traçage.
* Cet itérateur balaye aussi les étiquettes de graduations.
*
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*/
private final class CompletePathIterator extends TickPathIterator {
/**
* Controle le remplacement des courbes par des droites. La valeur
* {@link java.lang.Double#NaN} indique qu'un tel remplacement n'a pas lieu.
*/
private final double flatness;
/**
* Chemin de l'étiquette {@link #label}.
*/
private PathIterator path;
/**
* Etiquette de graduation à tracer.
*/
private Shape label;
/**
* Rectangle englobant l'étiquette {@link #label} courante.
*/
private Rectangle2D labelBounds;
/**
* Valeur maximale de {@code labelBounds.getWidth()} trouvée jusqu'à maintenant.
*/
private double maxWidth=0;
/**
* Valeur maximale de {@code labelBounds.getHeight()} trouvée jusqu'à maintenant.
*/
private double maxHeight=0;
/**
* Prend la valeur {@code true} lorsque la légende de l'axe a été écrite.
*/
private boolean isDone;
/**
* Construit un itérateur.
*
* @param transform Transformation affine à appliquer sur les données. Il doit
* s'agir d'une transformation affine appropriée pour l'écriture de
* texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
* de la transformation affine créée par {@link Axis2D#createAffineTransform}.
* @param flatness Contrôle le remplacement des courbes par des droites. La valeur
* {@link java.lang.Double#NaN} indique qu'un tel remplacement ne doit pas être fait.
*/
public CompletePathIterator(final AffineTransform transform, final double flatness) {
super(transform);
this.flatness = flatness;
}
/**
* Retourne un itérateur balayant la forme géométrique spécifiée.
*/
private PathIterator getPathIterator(final Shape shape) {
return isNaN(flatness) ? shape.getPathIterator(transform)
: shape.getPathIterator(transform, flatness);
}
/**
* Lance une exception; cet itérateur n'est conçu pour n'être utilisé qu'une seule fois.
*/
@Override
public void rewind(final AffineTransform transform) {
throw new UnsupportedOperationException();
}
/**
* Tests if the iteration is complete.
*/
@Override
public boolean isDone() {
return (path!=null) ? path.isDone() : super.isDone();
}
/**
* Returns the coordinates and type of the current path segment in the iteration.
*/
@Override
public int currentSegment(final float[] coords) {
return (path!=null) ? path.currentSegment(coords) : super.currentSegment(coords);
}
/**
* Returns the coordinates and type of the current path segment in the iteration.
*/
@Override
public int currentSegment(final double[] coords) {
return (path!=null) ? path.currentSegment(coords) : super.currentSegment(coords);
}
/**
* Moves the iterator to the next segment of the path forwards along the primary
* direction of traversal as long as there are more points in that direction.
*/
@Override
public void next() {
if (path != null) {
path.next();
if (!path.isDone()) {
return;
}
path = null;
}
if (label != null) {
path = getPathIterator(label);
label = null;
if (path != null) {
if (!path.isDone()) {
return;
}
path=null;
}
}
if (!isDone) {
super.next();
if (isDone()) {
/*
* Quand tout le reste est terminé, prépare l'écriture de la légende de l'axe.
*/
isDone = true;
final String title = graduation.getTitle(true);
if (title != null) {
final GlyphVector glyphs;
glyphs = getTitleFont().createGlyphVector(getFontRenderContext(), title);
if (transform != null) {
transform = new AffineTransform(transform);
} else {
transform = new AffineTransform();
}
final Rectangle2D bounds = centerAxisLabel(glyphs.getVisualBounds(),
transform,
new XDimension2D.Double(maxWidth, maxHeight));
path = getPathIterator(glyphs.getOutline((float)bounds.getMinX(),
(float)bounds.getMaxY()));
}
}
}
}
/**
* Méthode appelée automatiquement par {@link #next} pour
* indiquer qu'il faudra se préparer à tracer une étiquette.
*/
@Override
protected void prepareLabel() {
if (isMajorTick()) {
final GlyphVector glyphs = currentLabelGlyphs();
final Rectangle2D bounds = currentLabelBounds();
if (glyphs!=null && bounds!=null) {
if (labelBounds==null || !labelBounds.intersects(bounds)) {
label = glyphs.getOutline((float)bounds.getMinX(), (float)bounds.getMaxY());
final double width = bounds.getWidth();
final double height = bounds.getHeight();
if (width > maxWidth) maxWidth =width;
if (height > maxHeight) maxHeight=height;
labelBounds=bounds;
}
}
}
}
}
}