0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
0005: * (C) 2000, Institut de Recherche pour le Développement
0006: * (C) 1999, Pêches et Océans Canada
0007: *
0008: * This library is free software; you can redistribute it and/or
0009: * modify it under the terms of the GNU Lesser General Public
0010: * License as published by the Free Software Foundation; either
0011: * version 2.1 of the License, or (at your option) any later version.
0012: *
0013: * This library is distributed in the hope that it will be useful,
0014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0016: * Lesser General Public License for more details.
0017: */
0018: package org.geotools.axis;
0019:
0020: // Graphics and geometry
0021: import java.awt.Font;
0022: import java.awt.Graphics2D;
0023: import java.awt.RenderingHints;
0024: import java.awt.Shape;
0025: import java.awt.font.FontRenderContext;
0026: import java.awt.font.GlyphVector;
0027: import java.awt.geom.AffineTransform;
0028: import java.awt.geom.Dimension2D;
0029: import java.awt.geom.IllegalPathStateException;
0030: import java.awt.geom.Line2D;
0031: import java.awt.geom.Point2D;
0032: import java.awt.geom.Rectangle2D;
0033:
0034: // Other J2SE dependencies and extensions
0035: import java.util.Map;
0036: import java.util.Locale;
0037: import java.util.Collections;
0038: import java.util.ConcurrentModificationException;
0039: import java.io.Serializable;
0040: import java.beans.PropertyChangeEvent;
0041: import java.beans.PropertyChangeListener;
0042: import javax.units.Unit;
0043:
0044: // OpenGIS dependencies
0045: import org.opengis.util.Cloneable;
0046: import org.opengis.referencing.IdentifiedObject;
0047: import org.opengis.referencing.cs.AxisDirection;
0048: import org.opengis.referencing.cs.CoordinateSystemAxis;
0049:
0050: // Geotools dependencies
0051: import org.geotools.resources.XMath;
0052: import org.geotools.resources.Utilities;
0053: import org.geotools.resources.geometry.XDimension2D;
0054: import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
0055: import org.geotools.referencing.operation.matrix.XAffineTransform;
0056:
0057: /**
0058: * An axis as a graduated line. {@code Axis2D} objets are really {@link Line2D}
0059: * objects with a {@link Graduation}. Because axis are {@link Line2D}, they can be
0060: * located anywhere in a widget with any orientation. Lines are drawn from starting
0061: * point
0062: * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
0063: * to end point
0064: * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}),
0065: * using a graduation from minimal value {@link Graduation#getMinimum} to maximal
0066: * value {@link Graduation#getMaximum}.
0067: *
0068: * Note the line's coordinates (<var>x<sub>1</sub></var>,<var>y<sub>1</sub></var>) and
0069: * (<var>x<sub>2</sub></var>,<var>y<sub>2</sub></var>) are completly independant of
0070: * graduation minimal and maximal values. Line's coordinates should be expressed in
0071: * some units convenient for rendering, as pixels or point (1/72 of inch). On the
0072: * opposite, graduation can have any arbitrary units, which is given by
0073: * {@link Graduation#getUnit}. The static method {@link #createAffineTransform} can
0074: * be used for mapping logical coordinates to pixels coordinates for an arbitrary
0075: * pair of {@code Axis2D} objects, which doesn't need to be perpendicular.
0076: *
0077: * @since 2.0
0078: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/axis/Axis2D.java $
0079: * @version $Id: Axis2D.java 22710 2006-11-12 18:04:54Z desruisseaux $
0080: * @author Martin Desruisseaux
0081: *
0082: * @see DefaultCoordinateSystemAxis
0083: * @see AxisDirection
0084: * @see Graduation
0085: */
0086: public class Axis2D extends Line2D implements Cloneable, Serializable {
0087: /**
0088: * Serial number for interoperability with different versions.
0089: */
0090: private static final long serialVersionUID = -8396436909942389360L;
0091:
0092: /**
0093: * Coordonnées des premier et dernier points de l'axe. Ces coordonnées
0094: * sont exprimées en "points" (1/72 de pouce), ce qui n'a rien à voir
0095: * avec les unités de {@link Graduation#getMinimum} et {@link Graduation#getMaximum}.
0096: */
0097: private float x1 = 8, y1 = 8, x2 = 648, y2 = 8;
0098:
0099: /**
0100: * Longueur des graduations, en points. Chaque graduations sera tracée à partir de
0101: * {@code [sub]TickStart} (généralement 0) jusqu'à {@code [sub]TickEnd}.
0102: * Par convention, des valeurs positives désignent l'intérieur du graphique et des
0103: * valeurs négatives l'extérieur.
0104: */
0105: private float tickStart = 0, tickEnd = 9, subTickStart = 0,
0106: subTickEnd = 5;
0107:
0108: /**
0109: * Indique dans quelle direction se trouve la graduation de l'axe. La valeur -1 indique
0110: * qu'il faudrait tourner l'axe dans le sens des aiguilles d'une montre pour qu'il soit
0111: * par-dessus sa graduation. La valeur +1 indique au contraire qu'il faudrait le tourner
0112: * dans le sens inverse des aiguilles d'une montre pour le même effet.
0113: */
0114: private byte relativeCCW = +1;
0115:
0116: /**
0117: * Modèle qui contient les minimum, maximum et la graduation de l'axe.
0118: */
0119: private final Graduation graduation;
0120:
0121: /**
0122: * The coordinate system axis object associated to this axis, or {@code null} if it has
0123: * not been created yet.
0124: */
0125: private transient DefaultCoordinateSystemAxis information;
0126:
0127: /**
0128: * Compte le nombre de modifications apportées à l'axe,
0129: * afin de détecter les changements faits pendant qu'un
0130: * itérateur balaye la graduation.
0131: */
0132: private transient int modCount;
0133:
0134: /**
0135: * Indique si {@link #getPathIterator} doit retourner {@link #iterator}.
0136: * Ce champ prend temporairement la valeur de {@code true} pendant
0137: * l'exécution de {@link #paint}.
0138: */
0139: private transient boolean isPainting;
0140:
0141: /**
0142: * Itérateur utilisé pour dessiner l'axe lors du dernier appel de
0143: * la méthode {@link #paint}. Cet itérateur sera réutilisé autant
0144: * que possible afin de diminuer le nombre d'objets créés lors de
0145: * chaque traçage.
0146: */
0147: private transient TickPathIterator iterator;
0148:
0149: /**
0150: * Coordonnées de la boîte englobant l'axe (<u>sans</u> ses étiquettes
0151: * de graduation) lors du dernier traçage par la méthode {@link #paint}.
0152: * Ces coordonnées sont indépendantes de {@link #lastContext} et ont été
0153: * obtenues sans transformation affine "utilisateur".
0154: */
0155: private transient Rectangle2D axisBounds;
0156:
0157: /**
0158: * Coordonnées de la boîte englobant les étiquettes de graduations (<u>sans</u>
0159: * le reste de l'axe) lors du dernier traçage par la méthode {@link #paint}. Ces
0160: * coordonnées ont été calculées en utilisant {@link #lastContext} mais ont été
0161: * obtenues sans transformation affine "utilisateur".
0162: */
0163: private transient Rectangle2D labelBounds;
0164:
0165: /**
0166: * Coordonnées de la boîte englobant la légende de l'axe lors du dernier traçage
0167: * par la méthode {@link #paint}. Ces coordonnées ont été calculées en utilisant
0168: * {@link #lastContext} mais ont été obtenues sans transformation affine "utilisateur".
0169: */
0170: private transient Rectangle2D legendBounds;
0171:
0172: /**
0173: * Dernier objet {@link FontRenderContext} a avoir été
0174: * utilisé lors du traçage par la méthode {@link #paint}.
0175: */
0176: private transient FontRenderContext lastContext;
0177:
0178: /**
0179: * Largeur et hauteur maximales des étiquettes de la graduation, ou
0180: * {@code null} si cette dimension n'a pas encore été déterminée.
0181: */
0182: private transient Dimension2D maximumSize;
0183:
0184: /**
0185: * A default font to use when no rendering hint were provided for the
0186: * {@link Graduation#TICK_LABEL_FONT} key. Cached here only for performance.
0187: */
0188: private transient Font defaultFont;
0189:
0190: /**
0191: * A set of rendering hints for this axis.
0192: */
0193: private RenderingHints hints;
0194:
0195: /**
0196: * Constructs an axis with a default {@link NumberGraduation}.
0197: */
0198: public Axis2D() {
0199: this (new NumberGraduation(null));
0200: }
0201:
0202: /**
0203: * Constructs an axis with the specified graduation.
0204: */
0205: public Axis2D(final Graduation graduation) {
0206: this .graduation = graduation;
0207: graduation
0208: .addPropertyChangeListener(new PropertyChangeListener() {
0209: public void propertyChange(
0210: final PropertyChangeEvent event) {
0211: synchronized (Axis2D.this ) {
0212: modCount++;
0213: clearCache();
0214: }
0215: }
0216: });
0217: }
0218:
0219: /**
0220: * Returns the axis's graduation.
0221: */
0222: public Graduation getGraduation() {
0223: return graduation;
0224: }
0225:
0226: /**
0227: * Returns the <var>x</var> coordinate of the start point. By convention,
0228: * this coordinate should be in pixels or points (1/72 of inch) for proper
0229: * positionning of ticks and labels.
0230: *
0231: * @see #getY1
0232: * @see #getX2
0233: * @see #setLine
0234: */
0235: public double getX1() {
0236: return x1;
0237: }
0238:
0239: /**
0240: * Returns the <var>x</var> coordinate of the end point. By convention,
0241: * this coordinate should be in pixels or points (1/72 of inch) for proper
0242: * positionning of ticks and labels.
0243: *
0244: * @see #getY2
0245: * @see #getX1
0246: * @see #setLine
0247: */
0248: public double getX2() {
0249: return x2;
0250: }
0251:
0252: /**
0253: * Returns the <var>y</var> coordinate of the start point. By convention,
0254: * this coordinate should be in pixels or points (1/72 of inch) for proper
0255: * positionning of ticks and labels.
0256: *
0257: * @see #getX1
0258: * @see #getY2
0259: * @see #setLine
0260: */
0261: public double getY1() {
0262: return y1;
0263: }
0264:
0265: /**
0266: * Returns the <var>y</var> coordinate of the end point. By convention,
0267: * this coordinate should be in pixels or points (1/72 of inch) for proper
0268: * positionning of ticks and labels.
0269: *
0270: * @see #getX2
0271: * @see #getY1
0272: * @see #setLine
0273: */
0274: public double getY2() {
0275: return y2;
0276: }
0277:
0278: /**
0279: * Returns the (<var>x</var>,<var>y</var>) coordinates of the start point.
0280: * By convention, those coordinates should be in pixels or points (1/72 of
0281: * inch) for proper positionning of ticks and labels.
0282: */
0283: public synchronized Point2D getP1() {
0284: return new Point2D.Float(x1, y1);
0285: }
0286:
0287: /**
0288: * Returns the (<var>x</var>,<var>y</var>) coordinates of the end point.
0289: * By convention, those coordinates should be in pixels or points (1/72 of
0290: * inch) for proper positionning of ticks and labels.
0291: */
0292: public synchronized Point2D getP2() {
0293: return new Point2D.Float(x2, y2);
0294: }
0295:
0296: /**
0297: * Returns the axis length. This is the distance between starting point (@link #getP1 P1})
0298: * and end point ({@link #getP2 P2}). This length is usually measured in pixels or points
0299: * (1/72 of inch).
0300: */
0301: public synchronized double getLength() {
0302: return XMath.hypot(getX1() - getX2(), getY1() - getY2());
0303: }
0304:
0305: /**
0306: * Returns a bounding box for this axis. The bounding box includes the axis's line
0307: * ({@link #getP1 P1}) to ({@link #getP2 P2}), the axis's ticks and all labels.
0308: *
0309: * @see #getX1
0310: * @see #getY1
0311: * @see #getX2
0312: * @see #getY2
0313: */
0314: public synchronized Rectangle2D getBounds2D() {
0315: if (axisBounds == null) {
0316: paint(null); // Force the computation of bounding box size.
0317: }
0318: final Rectangle2D bounds = (Rectangle2D) axisBounds.clone();
0319: if (labelBounds != null)
0320: bounds.add(labelBounds);
0321: if (legendBounds != null)
0322: bounds.add(legendBounds);
0323: return bounds;
0324: }
0325:
0326: /**
0327: * Sets the location of the endpoints of this {@code Axis2D} to the specified
0328: * coordinates. Coordinates should be in pixels (for screen rendering) or points
0329: * (for paper rendering). Using points units make it easy to render labels with
0330: * a raisonable font size, no matter the screen resolution or the axis graduation.
0331: *
0332: * @param x1 Coordinate <var>x</var> of starting point.
0333: * @param y1 Coordinate <var>y</var> of starting point
0334: * @param x2 Coordinate <var>x</var> of end point.
0335: * @param y2 Coordinate <var>y</var> of end point.
0336: * @throws IllegalArgumentException If a coordinate is {@code NaN} or infinite.
0337: *
0338: * @see #getX1
0339: * @see #getY1
0340: * @see #getX2
0341: * @see #getY2
0342: */
0343: public synchronized void setLine(final double x1, final double y1,
0344: final double x2, final double y2)
0345: throws IllegalArgumentException {
0346: final float fx1 = (float) x1;
0347: AbstractGraduation.ensureFinite("x1", fx1);
0348: final float fy1 = (float) y1;
0349: AbstractGraduation.ensureFinite("y1", fy1);
0350: final float fx2 = (float) x2;
0351: AbstractGraduation.ensureFinite("x2", fx2);
0352: final float fy2 = (float) y2;
0353: AbstractGraduation.ensureFinite("y2", fy2);
0354: modCount++; // Must be first
0355: this .x1 = fx1;
0356: this .y1 = fy1;
0357: this .x2 = fx2;
0358: this .y2 = fy2;
0359: clearCache();
0360: }
0361:
0362: /**
0363: * Returns {@code true} if the axis would have to rotate clockwise in order to
0364: * overlaps its graduation.
0365: */
0366: public boolean isLabelClockwise() {
0367: return relativeCCW < 0;
0368: }
0369:
0370: /**
0371: * Sets the label's locations relative to this axis. Value {@code true} means
0372: * that the axis would have to rotate clockwise in order to overlaps its graduation.
0373: * Value {@code false} means that the axis would have to rotate counter-clockwise
0374: * in order to overlaps its graduation.
0375: */
0376: public synchronized void setLabelClockwise(final boolean c) {
0377: modCount++; // Must be first
0378: relativeCCW = c ? (byte) -1 : (byte) +1;
0379: }
0380:
0381: /**
0382: * Returns a default font to use when no rendering hint were provided for
0383: * the {@link Graduation#TICK_LABEL_FONT} key.
0384: *
0385: * @return A default font (never {@code null}).
0386: */
0387: private synchronized Font getDefaultFont() {
0388: if (defaultFont == null) {
0389: defaultFont = new Font("SansSerif", Font.PLAIN, 9);
0390: }
0391: return defaultFont;
0392: }
0393:
0394: /**
0395: * Returns an iterator object that iterates along the {@code Axis2D} boundary
0396: * and provides access to the geometry of the shape outline. The shape includes
0397: * the axis line, graduation and labels. If an optional {@link AffineTransform}
0398: * is specified, the coordinates returned in the iteration are transformed accordingly.
0399: */
0400: public java.awt.geom.PathIterator getPathIterator(
0401: final AffineTransform transform) {
0402: return getPathIterator(transform, java.lang.Double.NaN);
0403: }
0404:
0405: /**
0406: * Returns an iterator object that iterates along the {@code Axis2D} boundary
0407: * and provides access to the geometry of the shape outline. The shape includes
0408: * the axis line, graduation and labels. If an optional {@link AffineTransform}
0409: * is specified, the coordinates returned in the iteration are transformed accordingly.
0410: */
0411: public synchronized java.awt.geom.PathIterator getPathIterator(
0412: final AffineTransform transform, final double flatness) {
0413: if (isPainting) {
0414: if (iterator != null) {
0415: iterator.rewind(transform);
0416: } else {
0417: iterator = new TickPathIterator(transform);
0418: }
0419: return iterator;
0420: }
0421: return new PathIterator(transform, flatness);
0422: }
0423:
0424: /**
0425: * Draw this axis in the specified graphics context. This method is equivalents
0426: * to {@code Graphics2D.draw(this)}. However, this method may be slightly
0427: * faster and produce better quality output.
0428: *
0429: * @param graphics The graphics context to use for drawing.
0430: */
0431: public synchronized void paint(final Graphics2D graphics) {
0432: if (!(getLength() > 0)) {
0433: return;
0434: }
0435: /*
0436: * Initialise l'itérateur en appelant 'init' (contrairement à 'getPathIterator'
0437: * qui n'appelle que 'rewind') pour des résultats plus rapides et plus constants.
0438: */
0439: if (iterator != null) {
0440: iterator.init(null);
0441: } else {
0442: iterator = new TickPathIterator(null);
0443: }
0444: final TickPathIterator iterator = this .iterator;
0445: final boolean sameContext;
0446: final Shape clip;
0447: if (graphics != null) {
0448: clip = graphics.getClip();
0449: iterator.setFontRenderContext(graphics
0450: .getFontRenderContext());
0451: iterator.setRenderingHint(graphics,
0452: Graduation.AXIS_TITLE_FONT);
0453: iterator.setRenderingHint(graphics,
0454: Graduation.TICK_LABEL_FONT);
0455: final FontRenderContext context = iterator
0456: .getFontRenderContext();
0457: sameContext = clip != null && context.equals(lastContext);
0458: } else {
0459: clip = null;
0460: sameContext = false;
0461: iterator.setFontRenderContext(null);
0462: }
0463: /*
0464: * Calcule (si ce n'était pas déjà fait) les coordonnées d'un rectangle qui englobe l'axe et
0465: * sa graduation (mais sans les étiquettes de graduation). Cette information nous permettra
0466: * de vérifier s'il est vraiment nécessaire de redessiner l'axe en vérifiant s'il intercepte
0467: * avec le "clip" du graphique.
0468: */
0469: if (axisBounds == null) {
0470: axisBounds = new Rectangle2D.Float(Math.min(x1, x2), Math
0471: .min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
0472: while (!iterator.isDone()) {
0473: axisBounds.add(iterator.point);
0474: iterator.next();
0475: }
0476: }
0477: /*
0478: * Dessine l'axe et ses barres de graduation (mais sans les étiquettes).
0479: */
0480: if (graphics != null) {
0481: if (clip == null || clip.intersects(axisBounds))
0482: try {
0483: isPainting = true;
0484: graphics.draw(this );
0485: } finally {
0486: isPainting = false;
0487: }
0488: }
0489: /*
0490: * Dessine les étiquettes de graduations. Ce bloc peut etre exécuté même si
0491: * 'graphics' est nul. Dans ce cas, les étiquettes ne seront pas dessinées
0492: * mais le calcul de l'espace qu'elles occupent sera quand même effectué.
0493: */
0494: if (!sameContext || labelBounds == null
0495: || clip.intersects(labelBounds) || maximumSize == null) {
0496: Rectangle2D lastLabelBounds = labelBounds = null;
0497: double maxWidth = 0;
0498: double maxHeight = 0;
0499: iterator.rewind();
0500: while (iterator.hasNext()) {
0501: if (iterator.isMajorTick()) {
0502: final GlyphVector glyphs = iterator
0503: .currentLabelGlyphs();
0504: final Rectangle2D bounds = iterator
0505: .currentLabelBounds();
0506: if (glyphs != null && bounds != null) {
0507: if (lastLabelBounds == null
0508: || !lastLabelBounds.intersects(bounds)) {
0509: if (graphics != null
0510: && (clip == null || clip
0511: .intersects(bounds))) {
0512: graphics.drawGlyphVector(glyphs,
0513: (float) bounds.getMinX(),
0514: (float) bounds.getMaxY());
0515: }
0516: lastLabelBounds = bounds;
0517: final double width = bounds.getWidth();
0518: final double height = bounds.getHeight();
0519: if (width > maxWidth)
0520: maxWidth = width;
0521: if (height > maxHeight)
0522: maxHeight = height;
0523: }
0524: if (labelBounds == null) {
0525: labelBounds = new Rectangle2D.Float();
0526: labelBounds.setRect(bounds);
0527: } else {
0528: labelBounds.add(bounds);
0529: }
0530: }
0531: }
0532: iterator.nextMajor();
0533: }
0534: maximumSize = new XDimension2D.Float((float) maxWidth,
0535: (float) maxHeight);
0536: }
0537: /*
0538: * Ecrit la légende de l'axe. Ce bloc peut etre exécuté même si
0539: * 'graphics' est nul. Dans ce cas, la légende ne sera pas écrite
0540: * mais le calcul de l'espace qu'elle occupe sera quand même effectué.
0541: */
0542: if (!sameContext || legendBounds == null
0543: || clip.intersects(legendBounds)) {
0544: final String title = graduation.getTitle(true);
0545: if (title != null) {
0546: final Font font = iterator.getTitleFont();
0547: final GlyphVector glyphs = font.createGlyphVector(
0548: iterator.getFontRenderContext(), title);
0549: final AffineTransform rotatedTr = new AffineTransform();
0550: final Rectangle2D bounds = iterator.centerAxisLabel(
0551: glyphs.getVisualBounds(), rotatedTr,
0552: maximumSize);
0553: if (graphics != null) {
0554: final AffineTransform currentTr = graphics
0555: .getTransform();
0556: try {
0557: graphics.transform(rotatedTr);
0558: graphics.drawGlyphVector(glyphs, (float) bounds
0559: .getMinX(), (float) bounds.getMaxY());
0560: } finally {
0561: graphics.setTransform(currentTr);
0562: }
0563: }
0564: legendBounds = XAffineTransform.transform(rotatedTr,
0565: bounds, bounds);
0566: }
0567: }
0568: lastContext = iterator.getFontRenderContext();
0569: }
0570:
0571: /**
0572: * Returns the value of a single preference for the rendering algorithms. Hint categories
0573: * include controls for label fonts and colors. Some of the keys and their associated values
0574: * are defined in the {@link Graduation} interface.
0575: *
0576: * @param key The key corresponding to the hint to get.
0577: * @return An object representing the value for the specified hint key, or {@code null}
0578: * if no value is associated to the specified key.
0579: *
0580: * @see Graduation#TICK_LABEL_FONT
0581: * @see Graduation#AXIS_TITLE_FONT
0582: */
0583: public synchronized Object getRenderingHint(
0584: final RenderingHints.Key key) {
0585: return (hints != null) ? hints.get(key) : null;
0586: }
0587:
0588: /**
0589: * Sets the value of a single preference for the rendering algorithms. Hint categories
0590: * include controls for label fonts and colors. Some of the keys and their associated
0591: * values are defined in the {@link Graduation} interface.
0592: *
0593: * @param key The key of the hint to be set.
0594: * @param value The value indicating preferences for the specified hint category.
0595: * A {@code null} value removes any hint for the specified key.
0596: *
0597: * @see Graduation#TICK_LABEL_FONT
0598: * @see Graduation#AXIS_TITLE_FONT
0599: */
0600: public synchronized void setRenderingHint(
0601: final RenderingHints.Key key, final Object value) {
0602: modCount++;
0603: if (value != null) {
0604: if (hints == null) {
0605: hints = new RenderingHints(key, value);
0606: clearCache();
0607: } else {
0608: if (!value.equals(hints.put(key, value))) {
0609: clearCache();
0610: }
0611: }
0612: } else if (hints != null) {
0613: if (hints.remove(key) != null) {
0614: clearCache();
0615: }
0616: if (hints.isEmpty()) {
0617: hints = null;
0618: }
0619: }
0620: }
0621:
0622: /**
0623: * Efface la cache interne. Cette méthode doit être appelée
0624: * chaque fois que des propriétés de l'axe ont changées.
0625: */
0626: private void clearCache() {
0627: axisBounds = null;
0628: labelBounds = null;
0629: legendBounds = null;
0630: maximumSize = null;
0631: information = null;
0632: }
0633:
0634: /**
0635: * Returns a string representation of this axis.
0636: */
0637: public String toString() {
0638: final StringBuffer buffer = new StringBuffer(Utilities
0639: .getShortClassName(this ));
0640: buffer.append("[\"");
0641: buffer.append(graduation.getTitle(true));
0642: buffer.append("\"]");
0643: return buffer.toString();
0644: }
0645:
0646: /**
0647: * Returns this axis name and direction. Information include a name (usually the
0648: * {@linkplain Graduation#getTitle graduation title}) and an direction. The direction is usually
0649: * {@linkplain AxisDirection#DISPLAY_UP up} or {@linkplain AxisDirection#DISPLAY_DOWN down}
0650: * for vertical axis, {@linkplain AxisDirection#DISPLAY_RIGHT right} or
0651: * {@linkplain AxisDirection#DISPLAY_LEFT left} for horizontal axis, or
0652: * {@linkplain AxisDirection#OTHER other} otherwise.
0653: */
0654: public synchronized DefaultCoordinateSystemAxis toCoordinateSystemAxis() {
0655: if (information == null) {
0656: String abbreviation = "z";
0657: AxisDirection direction = AxisDirection.OTHER;
0658: if (x1 == x2) {
0659: if (y1 < y2) {
0660: direction = AxisDirection.DISPLAY_UP;
0661: } else if (y1 > y2) {
0662: direction = AxisDirection.DISPLAY_DOWN;
0663: }
0664: abbreviation = "y";
0665: } else if (y1 == y2) {
0666: if (x1 < x2) {
0667: direction = AxisDirection.DISPLAY_RIGHT;
0668: } else if (x1 > x2) {
0669: direction = AxisDirection.DISPLAY_LEFT;
0670: }
0671: abbreviation = "x";
0672: }
0673: information = new DefaultCoordinateSystemAxis(Collections
0674: .singletonMap(IdentifiedObject.NAME_KEY, graduation
0675: .getTitle(false)), abbreviation, direction,
0676: graduation.getUnit());
0677: }
0678: return information;
0679: }
0680:
0681: /**
0682: * Creates an affine transform mapping logical to pixels coordinates for a pair
0683: * of axis. The affine transform will maps coordinates in the following way:
0684: *
0685: * <ul>
0686: * <li>For each input coordinates (<var>x</var>,<var>y</var>), the <var>x</var> and
0687: * <var>y</var> values are expressed in the same units than the {@code xAxis}
0688: * and {@code yAxis} graduations, respectively.</li>
0689: * <li>The output point is the pixel's coordinates for the (<var>x</var>,<var>y</var>)
0690: * values. Changing the <var>x</var> value move the pixel location in parallel with
0691: * the {@code xAxis}, which may or may not be horizontal. Changing the <var>y</var>
0692: * value move the pixel location in parallel with the {@code yAxis}, which may or
0693: * may not be vertical.</li>
0694: * </ul>
0695: *
0696: * @param xAxis The <var>x</var> axis. This axis doesn't have to be horizontal;
0697: * it can have any orientation, including vertical.
0698: * @param yAxis The <var>y</var> axis. This axis doesn't have to be vertical;
0699: * it can have any orientation, including horizontal.
0700: * @return An affine transform mapping logical to pixels coordinates.
0701: */
0702: public static AffineTransform createAffineTransform(
0703: final Axis2D xAxis, final Axis2D yAxis) {
0704: /* x
0705: * |
0706: * |\
0707: * | \ P Soit: X : l'axe des <var>x</var> du graphique.
0708: * | | Y : l'axe des <var>y</var> du graphique.
0709: * \ | P : un point à placer sur le graphique.
0710: * \| (Px,Py) : les composantes du point P selon les axes x et y.
0711: * \ y (Pi,Pj) : les composantes du point P en coordonnées "pixels".
0712: *
0713: * Désignons par <b>ex</b> et <b>ey</b> des vecteurs unitaires dans la direction de l'axe des
0714: * <var>x</var> et l'axe des <var>y</var> respectivement. Désignons par <b>i</b> et <b>j</b>
0715: * des vecteurs unitaires vers le droite et vers le haut de l'écran respectivement. On peut
0716: * décomposer les vecteurs unitaires <b>ex</b> et <b>ey</b> par:
0717: *
0718: * ex = exi*i + exj*j
0719: * ey = eyi*i + eyj*j
0720: * Donc, P = Px*ex + Py*ey = (Px*exi+Py*eyi)*i + (Px*exj + Py*eyj)*j
0721: *
0722: * Cette relation ne s'applique que si les deux systèmes de coordonnées (xy et ij) ont
0723: * la même origine. En pratique, ce ne sera pas le cas. Il faut donc compliquer un peu:
0724: *
0725: * Pi = (Px-Ox)*exi+(Py-Oy)*eyi + Oi où (Ox,Oy) sont les minimums des axes des x et y.
0726: * Pj = (Px-Ox)*exj+(Py-Oy)*eyj + Oj (Oi,Oj) est l'origine du système d'axe ij.
0727: *
0728: * [ Pi ] [ exi eyi Oi-(Ox*exi+Oy*eyi) ][ Px ] [ exi*Px + eyi*Py + Oi-(Ox*exi+Oy*oyi) ]
0729: * [ Pj ] = [ exj eyj Oj-(Ox*exj+Oy*eyj) ][ Py ] = [ exj*Px + eyj*Py + Oj-(Ox*exj+Oy*oyj) ]
0730: * [ 1 ] [ 0 0 1 ][ 1 ] [ 1 ]
0731: */
0732: synchronized (xAxis) {
0733: synchronized (yAxis) {
0734: final Graduation mx = xAxis.getGraduation();
0735: final Graduation my = yAxis.getGraduation();
0736: double ox = mx.getRange();
0737: double oy = my.getRange();
0738: double exi = ((double) xAxis.getX2() - (double) xAxis
0739: .getX1())
0740: / ox;
0741: double exj = ((double) xAxis.getY2() - (double) xAxis
0742: .getY1())
0743: / ox;
0744: double eyi = ((double) yAxis.getX2() - (double) yAxis
0745: .getX1())
0746: / oy;
0747: double eyj = ((double) yAxis.getY2() - (double) yAxis
0748: .getY1())
0749: / oy;
0750: ox = mx.getMinimum();
0751: oy = my.getMinimum();
0752: return new AffineTransform(exi, exj, eyi, eyj, xAxis.x1
0753: - (ox * exi + oy * eyi), yAxis.y1
0754: - (ox * exj + oy * eyj));
0755: }
0756: }
0757: }
0758:
0759: ////////////////////////////////////////////////////////////////////////////////////////////
0760: //////// ////////
0761: //////// TICK AND PATH ITERATORS ////////
0762: //////// ////////
0763: ////////////////////////////////////////////////////////////////////////////////////////////
0764:
0765: /**
0766: * Iterates along the graduation ticks and provides access to the graduation values. Each
0767: * {@code Axis2D.TickIterator} object traverses the graduation of the unclosing {@link Axis2D}
0768: * object independently from any other {@link TickIterator} objects in use at the same time.
0769: * If a change occurs in the underlying {@link Axis2D} object during the iteration, then
0770: * {@link #refresh} must be invoked in order to reset the iterator as if a new instance was
0771: * created. Except for {@link #refresh} method, using the iterator after a change in the
0772: * underlying {@link Axis2D} may thrown a {@link ConcurrentModificationException}.
0773: *
0774: * @since 2.0
0775: * @version $Id: Axis2D.java 22710 2006-11-12 18:04:54Z desruisseaux $
0776: * @author Martin Desruisseaux
0777: */
0778: public class TickIterator implements org.geotools.axis.TickIterator {
0779: /**
0780: * The underyling tick iterator.
0781: */
0782: private org.geotools.axis.TickIterator iterator;
0783:
0784: /**
0785: * A copy of {@link Axis2D#hints} rendering hints. A copy is required because some hints
0786: * (especially {@link Graduation#VISUAL_AXIS_LENGTH} and
0787: * {@link Graduation#VISUAL_TICK_SPACING}) are going to be overwriten. This set may also
0788: * contains additional hints provided by {@link Graphics2D} in the {@link Axis2D#paint}
0789: * method. This object will never be {@code null}.
0790: */
0791: private final RenderingHints hints;
0792:
0793: /**
0794: * {@code scaleX} and {@code scaleY} are used for scaling
0795: * logical coordinates to pixel coordinates. Those scale factors
0796: * <strong>must</strong> be the same than the one that appears in
0797: * {@link Axis2D#createAffineTransform}.
0798: */
0799: private double scaleX, scaleY;
0800:
0801: /**
0802: * {@code (tickX, tickY)} is a unitary vector perpendicular to the axis.
0803: */
0804: private double tickX, tickY;
0805:
0806: /**
0807: * The minimum value {@link Graduation#getMinimum}.
0808: * This value is copied here for faster access.
0809: */
0810: private double minimum;
0811:
0812: /**
0813: * Value returned by the last call to {@link #currentLabel}.
0814: * This value is cached here in order to avoid that
0815: * {@link #getGlyphVector} compute it again.
0816: */
0817: private transient String label;
0818:
0819: /**
0820: * Value returned by the last call to {@link #getGlyphVector}. This
0821: * value is cached here in order to avoir that {@link #getBounds}
0822: * compute it again.
0823: */
0824: private transient GlyphVector glyphs;
0825:
0826: /**
0827: * The font to use for rendering tick. If a rendering hint was provided for the
0828: * {@link Graduation#TICK_LABEL_FONT} key, then the value is used as the font.
0829: * Otherwise, a default font is created and used.
0830: */
0831: private transient Font font;
0832:
0833: /**
0834: * The font context from {@link Graphics2D#getFontContext},
0835: * or {@code null} for a default one.
0836: */
0837: private transient FontRenderContext fontContext;
0838:
0839: /**
0840: * Value of {@link Axis2D#modCount} when {@link #init} was
0841: * last invoked. This value is used in order to detect
0842: * changes to the underlying {@link Axis2D} during iteration.
0843: */
0844: private transient int modCount;
0845:
0846: /**
0847: * Construct an iterator.
0848: *
0849: * @param fontContext Information needed to correctly measure text, or
0850: * {@code null} if unknow. This object is usually given by
0851: * {@link Graphics2D#getFontRenderContext}.
0852: */
0853: public TickIterator(final FontRenderContext fontContext) {
0854: this .hints = new RenderingHints((Map) Axis2D.this .hints);
0855: this .fontContext = fontContext;
0856: refresh();
0857: }
0858:
0859: /**
0860: * Copy a rendering hints from the specified {@link Graphics2D}, providing that
0861: * it is not already defined.
0862: */
0863: final void setRenderingHint(final Graphics2D graphics,
0864: final RenderingHints.Key key) {
0865: if (hints.get(key) == null) {
0866: final Object value = graphics.getRenderingHint(key);
0867: if (value != null) {
0868: hints.put(key, value);
0869: }
0870: }
0871: }
0872:
0873: /**
0874: * Tests if the iterator has more ticks.
0875: */
0876: public boolean hasNext() {
0877: return iterator.hasNext();
0878: }
0879:
0880: /**
0881: * Tests if the current tick is a major one.
0882: *
0883: * @return {@code true} if current tick is a major tick,
0884: * or {@code false} if it is a minor tick.
0885: */
0886: public boolean isMajorTick() {
0887: return iterator.isMajorTick();
0888: }
0889:
0890: /**
0891: * Returns the position where to draw the current tick. The position is scaled
0892: * from the graduation's minimum to maximum. This is usually the same number
0893: * than {@link #currentValue}. The mean exception is for logarithmic graduation,
0894: * in which the tick position is not proportional to the tick value.
0895: */
0896: public double currentPosition() {
0897: return iterator.currentPosition();
0898: }
0899:
0900: /**
0901: * Returns the value for current tick. The current tick may be major or minor.
0902: */
0903: public double currentValue() {
0904: return iterator.currentValue();
0905: }
0906:
0907: /**
0908: * Returns the coordinates of the intersection point between current tick
0909: * and the underlying axis. Units are the same than axis start point
0910: * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
0911: * and end point
0912: * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
0913: * This is usually pixels.
0914: *
0915: * @param dest A destination point that stores the intersection coordinates,
0916: * or {@code null} to create a new {@link Point2D} object.
0917: * @return {@code dest}, or a new {@link Point2D} object if {@code dest} was null.
0918: */
0919: public Point2D currentPosition(final Point2D dest) {
0920: final double position = currentPosition() - minimum;
0921: final double x = position * scaleX + getX1();
0922: final double y = position * scaleY + getY1();
0923: ensureValid();
0924: if (dest != null) {
0925: dest.setLocation(x, y);
0926: return dest;
0927: }
0928: return new Point2D.Float((float) x, (float) y);
0929: }
0930:
0931: /**
0932: * Returns the coordinates of the current tick.
0933: * Units are the same than axis start point
0934: * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
0935: * and end point
0936: * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
0937: * This is usually pixels.
0938: *
0939: * @param dest A destination line that stores the current tick coordinates,
0940: * or {@code null} to create a new {@link Line2D} object.
0941: * @return {@code dest}, or a new {@link Line2D} object if {@code dest} was null.
0942: */
0943: public Line2D currentTick(final Line2D dest) {
0944: final boolean isMajorTick = isMajorTick();
0945: final double position = currentPosition() - minimum;
0946: final double x = position * scaleX + getX1();
0947: final double y = position * scaleY + getY1();
0948: final double s1 = isMajorTick ? tickStart : subTickStart;
0949: final double s2 = isMajorTick ? tickEnd : subTickEnd;
0950: final double x1 = x + tickX * s1;
0951: final double y1 = y + tickY * s1;
0952: final double x2 = x + tickX * s2;
0953: final double y2 = y + tickY * s2;
0954: ensureValid();
0955: if (dest != null) {
0956: dest.setLine(x1, y1, x2, y2);
0957: return dest;
0958: }
0959: return new Line2D.Float((float) x1, (float) y1, (float) x2,
0960: (float) y2);
0961: }
0962:
0963: /**
0964: * Returns the label for current tick. This method is usually invoked
0965: * only for major ticks, but may be invoked for minor ticks as well.
0966: * This method returns {@code null} if it can't produces a label
0967: * for current tick.
0968: */
0969: public String currentLabel() {
0970: if (label == null) {
0971: label = iterator.currentLabel();
0972: }
0973: return label;
0974: }
0975:
0976: /**
0977: * Returns the label for current tick as a glyphs vector. This method is used
0978: * together with {@link #currentLabelBounds} for labels rendering. <strong>Do
0979: * not change the returned {@link GlyphVector}</strong>, since the glyphs vector
0980: * is not cloned for performance raisons. This method returns {@code null}
0981: * if it can't produces a glyph vector for current tick.
0982: */
0983: public GlyphVector currentLabelGlyphs() {
0984: if (glyphs == null) {
0985: final String label = currentLabel();
0986: if (label != null) {
0987: glyphs = getTickFont().createGlyphVector(
0988: getFontRenderContext(), label);
0989: }
0990: }
0991: return glyphs;
0992: }
0993:
0994: /**
0995: * Returns a bounding vector for the current tick label.
0996: * Units are the same than axis start point
0997: * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>})
0998: * and end point
0999: * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}).
1000: * This is usually pixels. This method can be used as in the example below:
1001: *
1002: * <pre>
1003: * {@link Axis2D.TickIterator} iterator = axis.new {@link Axis2D.TickIterator TickIterator}(null};
1004: * while (iterator.{@link #hasNext() hasNext()}) {
1005: * {@link GlyphVector} glyphs = iterator.{@link Axis2D.TickIterator#currentLabelGlyphs() currentLabelGlyphs()};
1006: * {@link Rectangle2D} bounds = iterator.{@link Axis2D.TickIterator#currentLabelBounds() currentLabelBounds()};
1007: * graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(), (float)bounds.getMaxY());
1008: * iterator.{@link #next() next()};
1009: * }
1010: * </pre>
1011: *
1012: * This method returns {@code null} if it can't compute bounding box for current tick.
1013: */
1014: public Rectangle2D currentLabelBounds() {
1015: final GlyphVector glyphs = currentLabelGlyphs();
1016: if (glyphs == null) {
1017: return null;
1018: }
1019: final Rectangle2D bounds = glyphs.getVisualBounds();
1020: final double height = bounds.getHeight();
1021: final double width = bounds.getWidth();
1022: final double tickStart = (0.5 * height)
1023: - Math.min(Axis2D.this .tickStart, 0);
1024: final double position = currentPosition() - minimum;
1025: final double x = position * scaleX + getX1();
1026: final double y = position * scaleY + getY1();
1027: bounds.setRect(x - (1 + tickX) * (0.5 * width) - tickX
1028: * tickStart, y + (1 - tickY) * (0.5 * height)
1029: - tickY * tickStart - height, width, height);
1030: ensureValid();
1031: return bounds;
1032: }
1033:
1034: /**
1035: * Returns the font for tick labels. This is the font used for drawing the tick label
1036: * formatted by {@link TickIterator#currentLabel}.
1037: *
1038: * @return The font (never {@code null}).
1039: */
1040: private Font getTickFont() {
1041: if (font == null) {
1042: Object candidate = hints
1043: .get(Graduation.TICK_LABEL_FONT);
1044: if (candidate instanceof Font) {
1045: font = (Font) candidate;
1046: } else {
1047: font = getDefaultFont();
1048: }
1049: }
1050: return font;
1051: }
1052:
1053: /**
1054: * Returns the font for axis title. This is the font used for drawing the title
1055: * formatted by {@link Graduation#getTitle}.
1056: *
1057: * @return The font (never {@code null}).
1058: */
1059: final Font getTitleFont() {
1060: Object candidate = hints.get(Graduation.AXIS_TITLE_FONT);
1061: if (candidate instanceof Font) {
1062: return (Font) candidate;
1063: }
1064: final Font font = getTickFont();
1065: return font.deriveFont(Font.BOLD, font.getSize2D()
1066: * (12f / 9));
1067: }
1068:
1069: /**
1070: * Retourne un rectangle centré vis-à-vis l'axe. Les coordonnées de ce rectangle seront
1071: * les mêmes que celles de l'axe, habituellement des pixels ou des points (1/72 de pouce).
1072: * Cette méthode s'utilise typiquement comme suit:
1073: *
1074: * <pre>
1075: * Graphics2D graphics = ...
1076: * FontRenderContext fontContext = graphics.getFontRenderContext();
1077: * TickIterator iterator = axis.new TickIterator(graphics.getFontRenderContext());
1078: * Font font = iterator.getTitleFont();
1079: * String title = axis.getGraduation().getTitle(true);
1080: * GlyphVector glyphs = font.createGlyphVector(fontContext, title);
1081: * Rectangle2D bounds = centerAxisLabel(glyphs.getVisualBounds());
1082: * graphics.drawGlyphVector(glyphs, (float)bounds.getMinX(), (float)bounds.getMaxY());
1083: * </pre>
1084: *
1085: * @param bounds Un rectangle englobant les caractères à écrire. La position
1086: * (<var>x</var>,<var>y</var>) de ce rectangle est généralement
1087: * (mais pas obligatoirement) l'origine (0,0). Ce rectangle est
1088: * habituellement obtenu par un appel à
1089: * {@link Font#createGlyphVector(FontContext,String)}.
1090: * @param toRotate Si non-nul, transformation affine sur laquelle appliquer une rotation
1091: * égale à l'angle de l'axe. Cette méthode peut limiter la rotation aux
1092: * quadrants 1 et 2 afin de conserver une lecture agréable du texte.
1093: * @param maximumSize Largeur et hauteur maximales des étiquettes de graduation. Cette
1094: * information est utilisée pour écarter l'étiquette de l'axe suffisament
1095: * pour qu'elle n'écrase pas les étiquettes de graduation.
1096: * @return Le rectangle {@code bounds}, modifié pour être centré sur l'axe.
1097: */
1098: final Rectangle2D centerAxisLabel(final Rectangle2D bounds,
1099: final AffineTransform toRotate,
1100: final Dimension2D maximumSize) {
1101: final double height = bounds.getHeight();
1102: final double width = bounds.getWidth();
1103: final double tx = 0;
1104: final double ty = height
1105: + Math.abs(maximumSize.getWidth() * tickX)
1106: + Math.abs(maximumSize.getHeight() * tickY);
1107: final double x1 = getX1();
1108: final double y1 = getY1();
1109: final double x2 = getX2();
1110: final double y2 = getY2();
1111: /////////////////////////////////////
1112: //// Compute unit vector (ux,uy) ////
1113: /////////////////////////////////////
1114: double ux = (double) x2 - (double) x1;
1115: double uy = (double) y2 - (double) y1;
1116: double ul = Math.sqrt(ux * ux + uy * uy);
1117: ux /= ul;
1118: uy /= ul;
1119: //////////////////////////////////////////////
1120: //// Get the central position of the axis ////
1121: //////////////////////////////////////////////
1122: double x = 0.5 * (x1 + x2);
1123: double y = 0.5 * (y1 + y2);
1124: ////////////////////////////////////////
1125: //// Apply the parallel translation ////
1126: ////////////////////////////////////////
1127: x += ux * tx;
1128: y += uy * tx;
1129: ////////////////////////////////////
1130: //// Adjust sign of unit vector ////
1131: ////////////////////////////////////
1132: ux *= relativeCCW;
1133: uy *= relativeCCW;
1134: /////////////////////////////////////////////
1135: //// Apply the perpendicular translation ////
1136: /////////////////////////////////////////////
1137: x += uy * ty;
1138: y -= ux * ty;
1139: ///////////////////////////////////
1140: //// Offset the point for text ////
1141: ///////////////////////////////////
1142: final double anchorX = x;
1143: final double anchorY = y;
1144: if (toRotate == null) {
1145: y += 0.5 * height * (1 - ux);
1146: x -= 0.5 * width * (1 - uy);
1147: } else {
1148: if (ux < 0) {
1149: ux = -ux;
1150: uy = -uy;
1151: y += height;
1152: }
1153: x -= 0.5 * width;
1154: toRotate.rotate(Math.atan2(uy, ux), anchorX, anchorY);
1155: }
1156: bounds.setRect(x, y - height, width, height);
1157: ensureValid();
1158: return bounds;
1159: }
1160:
1161: /**
1162: * Moves the iterator to the next minor or major tick.
1163: */
1164: public void next() {
1165: this .label = null;
1166: this .glyphs = null;
1167: iterator.next();
1168: }
1169:
1170: /**
1171: * Moves the iterator to the next major tick. This move ignore any minor ticks
1172: * between current position and the next major tick.
1173: */
1174: public void nextMajor() {
1175: this .label = null;
1176: this .glyphs = null;
1177: iterator.nextMajor();
1178: }
1179:
1180: /**
1181: * Reset the iterator on its first tick. All other properties are left unchanged.
1182: */
1183: public void rewind() {
1184: this .label = null;
1185: this .glyphs = null;
1186: iterator.rewind();
1187: }
1188:
1189: /**
1190: * Reset the iterator on its first tick. If some axis properies have changed (e.g. minimum
1191: * and/or maximum values), then the new settings are taken in account. This {@link #refresh}
1192: * method help to reduce garbage-collection by constructing an {@code Axis2D.TickIterator}
1193: * object only once and reuse it for each axis's rendering.
1194: */
1195: public void refresh() {
1196: synchronized (Axis2D.this ) {
1197: this .label = null;
1198: this .glyphs = null;
1199: // Do NOT modify 'fontContext'.
1200:
1201: final Graduation graduation = getGraduation();
1202: final double dx = getX2() - getX1();
1203: final double dy = getY2() - getY1();
1204: final double range = graduation.getRange();
1205: final double length = Math.sqrt(dx * dx + dy * dy);
1206: hints.put(Graduation.VISUAL_AXIS_LENGTH,
1207: new java.lang.Double(length));
1208:
1209: this .scaleX = dx / range;
1210: this .scaleY = dy / range;
1211: this .tickX = -dy / length * relativeCCW;
1212: this .tickY = +dx / length * relativeCCW;
1213: this .minimum = graduation.getMinimum();
1214: this .iterator = graduation.getTickIterator(hints,
1215: iterator);
1216: this .modCount = Axis2D.this .modCount;
1217: }
1218: }
1219:
1220: /**
1221: * Returns the locale used for formatting tick labels.
1222: */
1223: public Locale getLocale() {
1224: return iterator.getLocale();
1225: }
1226:
1227: /**
1228: * Retourne le contexte utilisé pour dessiner les caractères.
1229: * Cette méthode ne retourne jamais {@code null}.
1230: */
1231: final FontRenderContext getFontRenderContext() {
1232: if (fontContext == null) {
1233: fontContext = new FontRenderContext(null, false, false);
1234: }
1235: return fontContext;
1236: }
1237:
1238: /**
1239: * Spécifie le contexte à utiliser pour dessiner les caractères,
1240: * ou {@code null} pour utiliser un contexte par défaut.
1241: */
1242: final void setFontRenderContext(final FontRenderContext context) {
1243: fontContext = context;
1244: }
1245:
1246: /**
1247: * Vérifie que l'axe n'a pas changé depuis le dernier appel de {@link #init}.
1248: * Cette méthode doit être appelée <u>à la fin</u> des méthodes de cette classe
1249: * qui lisent les champs de {@link Axis2D}.
1250: */
1251: final void ensureValid() {
1252: if (this .modCount != Axis2D.this .modCount) {
1253: throw new ConcurrentModificationException();
1254: }
1255: }
1256: }
1257:
1258: /**
1259: * Itérateur balayant l'axe et ses barres de graduations pour leur traçage. Cet itérateur ne
1260: * balaye pas les étiquettes de graduations. Puisque cet itérateur ne retourne que des droites
1261: * et jamais de courbes, il ne prend pas d'argument {@code flatness}.
1262: *
1263: * @version $Id: Axis2D.java 22710 2006-11-12 18:04:54Z desruisseaux $
1264: * @author Martin Desruisseaux
1265: */
1266: private class TickPathIterator extends TickIterator implements
1267: java.awt.geom.PathIterator {
1268: /**
1269: * Transformation affine à appliquer sur les données. Il doit s'agir d'une transformation
1270: * affine appropriée pour l'écriture de texte (généralement en pixels ou en points). Il ne
1271: * s'agit <u>pas</u> de la transformation affine créée par
1272: * {@link Axis2D#createAffineTransform}.
1273: */
1274: protected AffineTransform transform;
1275:
1276: /**
1277: * Coordonnées de la prochaine graduation à retourner par une des méthodes
1278: * {@code currentSegment(...)}. Ces coordonnées n'auront <u>pas</u>
1279: * été transformées selon la transformation affine {@link #transform}.
1280: */
1281: private final Line2D.Double line = new Line2D.Double();
1282:
1283: /**
1284: * Coordonnées du prochain point à retourner par une des méthodes
1285: * {@code currentSegment(...)}. Ces coordonnées auront été
1286: * transformées selon la transformation affine {@link #transform}.
1287: */
1288: private final Point2D.Double point = new Point2D.Double();
1289:
1290: /**
1291: * Type du prochain segment. Ce type est retourné par les méthodes
1292: * {@code currentSegment(...)}. Il doit s'agir en général d'une
1293: * des constantes {@link #SEG_MOVETO} ou {@link #SEG_LINETO}.
1294: */
1295: private int type = SEG_MOVETO;
1296:
1297: /**
1298: * Entier indiquant quel sera le prochain item a retourner (début ou
1299: * fin d'une graduation, début ou fin de l'axe, etc.). Il doit s'agir
1300: * d'une des constantes {@link #AXIS_MOVETO}, {@link #AXIS_LINETO},
1301: * {@link #TICK_MOVETO}, {@link #TICK_LINETO}, etc.
1302: */
1303: private int nextType = AXIS_MOVETO;
1304:
1305: /** Constante pour {@link #nextType}.*/
1306: private static final int AXIS_MOVETO = 0;
1307: /** Constante pour {@link #nextType}.*/
1308: private static final int AXIS_LINETO = 1;
1309: /** Constante pour {@link #nextType}.*/
1310: private static final int TICK_MOVETO = 2;
1311: /** Constante pour {@link #nextType}.*/
1312: private static final int TICK_LINETO = 3;
1313:
1314: /**
1315: * Construit un itérateur.
1316: *
1317: * @param transform Transformation affine à appliquer sur les données. Il doit
1318: * s'agir d'une transformation affine appropriée pour l'écriture de
1319: * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
1320: * de la transformation affine créée par {@link Axis2D#createAffineTransform}.
1321: */
1322: public TickPathIterator(final AffineTransform transform) {
1323: super (null);
1324: // 'refresh' est appelée par le constructeur parent.
1325: this .transform = transform;
1326: next();
1327: }
1328:
1329: /**
1330: * Initialise cet itérateur. Cette méthode peut être appelée pour réutiliser un itérateur
1331: * qui a déjà servit, plutôt que d'en construire un autre.
1332: *
1333: * @param transform Transformation affine à appliquer sur les données. Il doit
1334: * s'agir d'une transformation affine appropriée pour l'écriture de
1335: * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
1336: * de la transformation affine créée par {@link Axis2D#createAffineTransform}.
1337: */
1338: final void init(final AffineTransform transform) {
1339: refresh();
1340: setFontRenderContext(null);
1341: this .type = SEG_MOVETO;
1342: this .nextType = AXIS_MOVETO;
1343: this .transform = transform;
1344: next();
1345: }
1346:
1347: /**
1348: * Repositione l'itérateur au début de la graduation
1349: * avec une nouvelle transformation affine.
1350: */
1351: public void rewind(final AffineTransform transform) {
1352: super .rewind();
1353: // Keep 'fontContext'.
1354: this .type = SEG_MOVETO;
1355: this .nextType = AXIS_MOVETO;
1356: this .transform = transform;
1357: next();
1358: }
1359:
1360: /**
1361: * Repositione l'itérateur au début de la graduation
1362: * en conservant la transformation affine actuelle.
1363: */
1364: public final void rewind() {
1365: rewind(transform);
1366: }
1367:
1368: /**
1369: * Return the winding rule for determining the insideness of the path.
1370: */
1371: public int getWindingRule() {
1372: return WIND_NON_ZERO;
1373: }
1374:
1375: /**
1376: * Tests if the iteration is complete.
1377: */
1378: public boolean isDone() {
1379: return nextType == TICK_LINETO && !hasNext();
1380: }
1381:
1382: /**
1383: * Returns the coordinates and type of the current path segment
1384: * in the iteration. The return value is the path segment type:
1385: * {@code SEG_MOVETO} or {@code SEG_LINETO}.
1386: */
1387: public int currentSegment(final float[] coords) {
1388: coords[0] = (float) point.x;
1389: coords[1] = (float) point.y;
1390: return type;
1391: }
1392:
1393: /**
1394: * Returns the coordinates and type of the current path segment
1395: * in the iteration. The return value is the path segment type:
1396: * {@code SEG_MOVETO} or {@code SEG_LINETO}.
1397: */
1398: public int currentSegment(final double[] coords) {
1399: coords[0] = point.x;
1400: coords[1] = point.y;
1401: return type;
1402: }
1403:
1404: /**
1405: * Moves the iterator to the next segment of the path forwards
1406: * along the primary direction of traversal as long as there are
1407: * more points in that direction.
1408: */
1409: public void next() {
1410: switch (nextType) {
1411: default: { // Should not happen
1412: throw new IllegalPathStateException(Integer
1413: .toString(nextType));
1414: }
1415: case AXIS_MOVETO: { // Premier point de l'axe
1416: point.x = getX1();
1417: point.y = getY1();
1418: type = SEG_MOVETO;
1419: nextType = AXIS_LINETO;
1420: break;
1421: }
1422: case AXIS_LINETO: { // Fin de l'axe
1423: point.x = getX2();
1424: point.y = getY2();
1425: type = SEG_LINETO;
1426: nextType = TICK_MOVETO;
1427: break;
1428: }
1429: case TICK_MOVETO: { // Premier point d'une graduation
1430: currentTick(line);
1431: point.x = line.x1;
1432: point.y = line.y1;
1433: type = SEG_MOVETO;
1434: nextType = TICK_LINETO;
1435: break;
1436: }
1437: case TICK_LINETO: { // Dernier point d'une graduation
1438: point.x = line.x2;
1439: point.y = line.y2;
1440: type = SEG_LINETO;
1441: nextType = TICK_MOVETO;
1442: prepareLabel();
1443: super .next();
1444: break;
1445: }
1446: }
1447: if (transform != null) {
1448: transform.transform(point, point);
1449: }
1450: ensureValid();
1451: }
1452:
1453: /**
1454: * Méthode appelée automatiquement par {@link #next} pour
1455: * indiquer qu'il faudra se préparer à tracer une étiquette.
1456: */
1457: protected void prepareLabel() {
1458: }
1459: }
1460:
1461: /**
1462: * Itérateur balayant l'axe et ses barres de graduations pour leur traçage.
1463: * Cet itérateur balaye aussi les étiquettes de graduations.
1464: *
1465: * @version $Id: Axis2D.java 22710 2006-11-12 18:04:54Z desruisseaux $
1466: * @author Martin Desruisseaux
1467: */
1468: private final class PathIterator extends TickPathIterator {
1469: /**
1470: * Controle le remplacement des courbes par des droites. La valeur
1471: * {@link Double#NaN} indique qu'un tel remplacement n'a pas lieu.
1472: */
1473: private final double flatness;
1474:
1475: /**
1476: * Chemin de l'étiquette {@link #label}.
1477: */
1478: private java.awt.geom.PathIterator path;
1479:
1480: /**
1481: * Etiquette de graduation à tracer.
1482: */
1483: private Shape label;
1484:
1485: /**
1486: * Rectangle englobant l'étiquette {@link #label} courante.
1487: */
1488: private Rectangle2D labelBounds;
1489:
1490: /**
1491: * Valeur maximale de {@code labelBounds.getWidth()} trouvée jusqu'à maintenant.
1492: */
1493: private double maxWidth = 0;
1494:
1495: /**
1496: * Valeur maximale de {@code labelBounds.getHeight()} trouvée jusqu'à maintenant.
1497: */
1498: private double maxHeight = 0;
1499:
1500: /**
1501: * Prend la valeur {@code true} lorsque la légende de l'axe a été écrite.
1502: */
1503: private boolean isDone;
1504:
1505: /**
1506: * Construit un itérateur.
1507: *
1508: * @param transform Transformation affine à appliquer sur les données. Il doit
1509: * s'agir d'une transformation affine appropriée pour l'écriture de
1510: * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u>
1511: * de la transformation affine créée par {@link Axis2D#createAffineTransform}.
1512: * @param flatness Contrôle le remplacement des courbes par des droites. La valeur
1513: * {@link Double#NaN} indique qu'un tel remplacement ne doit pas être fait.
1514: */
1515: public PathIterator(final AffineTransform transform,
1516: final double flatness) {
1517: super (transform);
1518: this .flatness = flatness;
1519: }
1520:
1521: /**
1522: * Retourne un itérateur balayant la forme géométrique spécifiée.
1523: */
1524: private java.awt.geom.PathIterator getPathIterator(
1525: final Shape shape) {
1526: return java.lang.Double.isNaN(flatness) ? shape
1527: .getPathIterator(transform) : shape
1528: .getPathIterator(transform, flatness);
1529: }
1530:
1531: /**
1532: * Lance une exception; cet itérateur n'est conçu pour n'être utilisé qu'une seule fois.
1533: */
1534: public void rewind(final AffineTransform transform) {
1535: throw new UnsupportedOperationException();
1536: }
1537:
1538: /**
1539: * Tests if the iteration is complete.
1540: */
1541: public boolean isDone() {
1542: return (path != null) ? path.isDone() : super .isDone();
1543: }
1544:
1545: /**
1546: * Returns the coordinates and type of the current path segment in the iteration.
1547: */
1548: public int currentSegment(final float[] coords) {
1549: return (path != null) ? path.currentSegment(coords) : super
1550: .currentSegment(coords);
1551: }
1552:
1553: /**
1554: * Returns the coordinates and type of the current path segment in the iteration.
1555: */
1556: public int currentSegment(final double[] coords) {
1557: return (path != null) ? path.currentSegment(coords) : super
1558: .currentSegment(coords);
1559: }
1560:
1561: /**
1562: * Moves the iterator to the next segment of the path forwards along the primary
1563: * direction of traversal as long as there are more points in that direction.
1564: */
1565: public void next() {
1566: if (path != null) {
1567: path.next();
1568: if (!path.isDone()) {
1569: return;
1570: }
1571: path = null;
1572: }
1573: if (label != null) {
1574: path = getPathIterator(label);
1575: label = null;
1576: if (path != null) {
1577: if (!path.isDone()) {
1578: return;
1579: }
1580: path = null;
1581: }
1582: }
1583: if (!isDone) {
1584: super .next();
1585: if (isDone()) {
1586: /*
1587: * Quand tout le reste est terminé, prépare l'écriture de la légende de l'axe.
1588: */
1589: isDone = true;
1590: final String title = graduation.getTitle(true);
1591: if (title != null) {
1592: final GlyphVector glyphs;
1593: glyphs = getTitleFont().createGlyphVector(
1594: getFontRenderContext(), title);
1595: if (transform != null) {
1596: transform = new AffineTransform(transform);
1597: } else {
1598: transform = new AffineTransform();
1599: }
1600: final Rectangle2D bounds = centerAxisLabel(
1601: glyphs.getVisualBounds(), transform,
1602: new XDimension2D.Double(maxWidth,
1603: maxHeight));
1604: path = getPathIterator(glyphs.getOutline(
1605: (float) bounds.getMinX(),
1606: (float) bounds.getMaxY()));
1607: }
1608: }
1609: }
1610: }
1611:
1612: /**
1613: * Méthode appelée automatiquement par {@link #next} pour
1614: * indiquer qu'il faudra se préparer à tracer une étiquette.
1615: */
1616: protected void prepareLabel() {
1617: if (isMajorTick()) {
1618: final GlyphVector glyphs = currentLabelGlyphs();
1619: final Rectangle2D bounds = currentLabelBounds();
1620: if (glyphs != null && bounds != null) {
1621: if (labelBounds == null
1622: || !labelBounds.intersects(bounds)) {
1623: label = glyphs.getOutline((float) bounds
1624: .getMinX(), (float) bounds.getMaxY());
1625: final double width = bounds.getWidth();
1626: final double height = bounds.getHeight();
1627: if (width > maxWidth)
1628: maxWidth = width;
1629: if (height > maxHeight)
1630: maxHeight = height;
1631: labelBounds = bounds;
1632: }
1633: }
1634: }
1635: }
1636: }
1637: }
|