/* * GeoTools - OpenSource mapping toolkit * http://geotools.org * (C) 2003-2006, Geotools Project Managment Committee (PMC) * (C) 2000, Institut de Recherche pour le Développement * (C) 1999, Pêches et Océans Canada * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.axis; // Graphics and geometry 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.Point2D; import java.awt.geom.Rectangle2D; // Other J2SE dependencies and extensions import java.util.Map; 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 javax.units.Unit; // OpenGIS dependencies import org.opengis.util.Cloneable; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystemAxis; // Geotools dependencies import org.geotools.resources.XMath; import org.geotools.resources.Utilities; 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 x1},{@linkplain #getY1 y1}) * to end point * ({@linkplain #getX2 x2},{@linkplain #getY2 y2}), * using a graduation from minimal value {@link Graduation#getMinimum} to maximal * value {@link Graduation#getMaximum}. * * Note the line's coordinates (x1,y1) and * (x2,y2) 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 * * @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 (sans 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 (sans * 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 x 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 x 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 y 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 y 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 (x,y) 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 (x,y) 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 XMath.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 x of starting point. * @param y1 Coordinate y of starting point * @param x2 Coordinate x of end point. * @param y2 Coordinate y 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. */ public java.awt.geom.PathIterator getPathIterator(final AffineTransform transform) { return getPathIterator(transform, java.lang.Double.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. */ public synchronized java.awt.geom.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 PathIterator(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. */ public String toString() { final StringBuffer buffer = new StringBuffer(Utilities.getShortClassName(this)); buffer.append("[\""); buffer.append(graduation.getTitle(true)); buffer.append("\"]"); return buffer.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: * *
* {@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()}; * } ** * 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: * *
* 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()); ** * @param bounds Un rectangle englobant les caractères à écrire. La position * (x,y) 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 = (double) x2 - (double) x1; double uy = (double) y2 - (double) y1; double ul = Math.sqrt(ux*ux + uy*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, new java.lang.Double(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 à la fin 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 */ private class TickPathIterator extends TickIterator implements java.awt.geom.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 pas 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 pas * é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 pas * 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 pas * 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. */ 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. */ 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 */ private final class PathIterator extends TickPathIterator { /** * Controle le remplacement des courbes par des droites. La valeur * {@link Double#NaN} indique qu'un tel remplacement n'a pas lieu. */ private final double flatness; /** * Chemin de l'étiquette {@link #label}. */ private java.awt.geom.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 pas * de la transformation affine créée par {@link Axis2D#createAffineTransform}. * @param flatness Contrôle le remplacement des courbes par des droites. La valeur * {@link Double#NaN} indique qu'un tel remplacement ne doit pas être fait. */ public PathIterator(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 java.awt.geom.PathIterator getPathIterator(final Shape shape) { return java.lang.Double.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. */ public void rewind(final AffineTransform transform) { throw new UnsupportedOperationException(); } /** * Tests if the iteration is complete. */ public boolean isDone() { return (path!=null) ? path.isDone() : super.isDone(); } /** * Returns the coordinates and type of the current path segment in the iteration. */ 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. */ 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. */ 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. */ 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; } } } } } }