/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 1999-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.display.axis; import java.awt.Font; import java.awt.Shape; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.font.GlyphVector; import java.awt.geom.PathIterator; import java.awt.geom.AffineTransform; import java.awt.font.FontRenderContext; import java.awt.geom.IllegalPathStateException; import java.io.Serializable; import java.util.Map; import java.util.Collections; import java.util.ConcurrentModificationException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import static java.lang.Double.isNaN; import static java.lang.Double.NaN; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.cs.AxisDirection; import org.geotoolkit.util.Cloneable; import org.apache.sis.util.Classes; import org.geotoolkit.display.shape.DoubleDimension2D; import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis; import org.apache.sis.referencing.operation.matrix.AffineTransforms2D; /** * An axis as a graduated line. {@code Axis2D} objets are really {@link Line2D} objects with a * {@link Graduation}. Because axis are {@link Line2D}, they can be located anywhere in a widget * with any orientation. Lines are drawn from starting point * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>}) * to end point * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}), * using a graduation from minimal value {@link Graduation#getMinimum} to maximal * value {@link Graduation#getMaximum}. * <p> * Note the line's coordinates (<var>x<sub>1</sub></var>,<var>y<sub>1</sub></var>) and * (<var>x<sub>2</sub></var>,<var>y<sub>2</sub></var>) are completely independent 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 * {@link #createAffineTransform createAffineTransform} static method 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. * * @author Martin Desruisseaux (MPO, IRD) * * @see DefaultCoordinateSystemAxis * @see AxisDirection * @see Graduation * * @since 1.0 * @module */ public class Axis2D extends Line2D implements Cloneable, Serializable { /** * Serial number for inter-operability 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 double 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 double tickStart=0, tickEnd=9, subTickStart=0, subTickEnd=5; /** * Indique dans quelle direction se trouve la graduation de l'axe. La valeur -1 indique * qu'il faudrait tourner l'axe dans le sens des aiguilles d'une montre pour qu'il soit * par-dessus sa graduation. La valeur +1 indique au contraire qu'il faudrait le tourner * dans le sens inverse des aiguilles d'une montre pour le même effet. */ private byte relativeCCW = +1; /** * Modèle qui contient les minimum, maximum et la graduation de l'axe. */ private final Graduation graduation; /** * The coordinate system axis object associated to this axis, or {@code null} if it has * not been created yet. */ private transient DefaultCoordinateSystemAxis information; /** * Compte le nombre de modifications apportées à l'axe, afin de détecter * les changements faits pendant qu'un itérateur balaye la graduation. */ private transient int modCount; /** * Indique si {@link #getPathIterator} doit retourner {@link #iterator}. Ce champ prend * temporairement la valeur de {@code true} pendant l'exécution de {@link #paint}. */ private transient boolean isPainting; /** * Itérateur utilisé pour dessiner l'axe lors du dernier appel de * la méthode {@link #paint}. Cet itérateur sera réutilisé autant * que possible afin de diminuer le nombre d'objets créés lors de * chaque traçage. */ private transient TickPathIterator iterator; /** * Coordonnées de la boîte englobant l'axe (<u>sans</u> ses étiquettes * de graduation) lors du dernier traçage par la méthode {@link #paint}. * Ces coordonnées sont indépendantes de {@link #lastContext} et ont été * obtenues sans transformation affine "utilisateur". */ private transient Rectangle2D axisBounds; /** * Coordonnées de la boîte englobant les étiquettes de graduations (<u>sans</u> * le reste de l'axe) lors du dernier traçage par la méthode {@link #paint}. Ces * coordonnées ont été calculées en utilisant {@link #lastContext} mais ont été * obtenues sans transformation affine "utilisateur". */ private transient Rectangle2D labelBounds; /** * Coordonnées de la boîte englobant la légende de l'axe lors du dernier traçage * par la méthode {@link #paint}. Ces coordonnées ont été calculées en utilisant * {@link #lastContext} mais ont été obtenues sans transformation affine "utilisateur". */ private transient Rectangle2D legendBounds; /** * Dernier objet {@link FontRenderContext} a avoir été * utilisé lors du traçage par la méthode {@link #paint}. */ private transient FontRenderContext lastContext; /** * Largeur et hauteur maximales des étiquettes de la graduation, ou * {@code null} si cette dimension n'a pas encore été déterminée. */ private transient Dimension2D maximumSize; /** * A default font to use when no rendering hint were provided for the * {@link Graduation#TICK_LABEL_FONT} key. Cached here only for performance. */ private transient Font defaultFont; /** * A set of rendering hints for this axis. */ private RenderingHints hints; /** * Constructs an axis with a default {@link NumberGraduation}. */ public Axis2D() { this(new NumberGraduation(null)); } /** * Constructs an axis with the specified graduation. * * @param graduation The graduation to be given to this axis. */ public Axis2D(final Graduation graduation) { this.graduation = graduation; graduation.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent event) { synchronized (Axis2D.this) { modCount++; clearCache(); } } }); } /** * Returns the axis's graduation. * * @return The graduation used by this axis. */ public Graduation getGraduation() { return graduation; } /** * Returns the <var>x</var> coordinate of the start point. By convention, * this coordinate should be in pixels or points (1/72 of inch) for proper * positionning of ticks and labels. * * @see #getY1 * @see #getX2 * @see #setLine */ @Override public double getX1() { return x1; } /** * Returns the <var>x</var> coordinate of the end point. By convention, * this coordinate should be in pixels or points (1/72 of inch) for proper * positionning of ticks and labels. * * @see #getY2 * @see #getX1 * @see #setLine */ @Override public double getX2() { return x2; } /** * Returns the <var>y</var> coordinate of the start point. By convention, * this coordinate should be in pixels or points (1/72 of inch) for proper * positionning of ticks and labels. * * @see #getX1 * @see #getY2 * @see #setLine */ @Override public double getY1() { return y1; } /** * Returns the <var>y</var> coordinate of the end point. By convention, * this coordinate should be in pixels or points (1/72 of inch) for proper * positionning of ticks and labels. * * @see #getX2 * @see #getY1 * @see #setLine */ @Override public double getY2() { return y2; } /** * Returns the (<var>x</var>,<var>y</var>) coordinates of the start point. * By convention, those coordinates should be in pixels or points (1/72 of * inch) for proper positionning of ticks and labels. */ @Override public synchronized Point2D getP1() { return new Point2D.Double(x1,y1); } /** * Returns the (<var>x</var>,<var>y</var>) coordinates of the end point. * By convention, those coordinates should be in pixels or points (1/72 of * inch) for proper positionning of ticks and labels. */ @Override public synchronized Point2D getP2() { return new Point2D.Double(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). * * @return The axis length in display units. */ public synchronized double getLength() { return Math.hypot(x1 - x2, y1 - y2); } /** * 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 */ @Override 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 reasonable font size, no matter the screen resolution or the axis graduation. * * @param x1 Coordinate <var>x</var> of starting point. * @param y1 Coordinate <var>y</var> of starting point * @param x2 Coordinate <var>x</var> of end point. * @param y2 Coordinate <var>y</var> of end point. * @throws IllegalArgumentException If a coordinate is {@code NaN} or infinite. * * @see #getX1 * @see #getY1 * @see #getX2 * @see #getY2 */ @Override public synchronized void setLine(final double x1, final double y1, final double x2, final double y2) throws IllegalArgumentException { AbstractGraduation.ensureFinite("x1", x1); AbstractGraduation.ensureFinite("y1", y1); AbstractGraduation.ensureFinite("x2", x2); AbstractGraduation.ensureFinite("y2", y2); modCount++; // Must be first this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; clearCache(); } /** * Returns {@code true} if the axis would have to rotate clockwise in order to * overlaps its graduation. * * @return {@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. * * @param c {@code true} if the axis would have to rotate 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. * * @param transform The transform to apply on the coordinates to be iterated. */ @Override public PathIterator getPathIterator(final AffineTransform transform) { return getPathIterator(transform, NaN); } /** * Returns an iterator object that iterates along the {@code Axis2D} boundary * and provides access to the geometry of the shape outline. The shape includes * the axis line, graduation and labels. If an optional {@link AffineTransform} * is specified, the coordinates returned in the iteration are transformed accordingly. * * @param transform The transform to apply on the coordinates to be iterated. * @param flatness Control the subdivisions of curves by straight lines. */ @Override public synchronized PathIterator getPathIterator(final AffineTransform transform, final double flatness) { if (isPainting) { TickPathIterator iterator = this.iterator; if (iterator != null) { iterator.rewind(transform); } else { this.iterator = iterator = new TickPathIterator(transform); } return iterator; } return new CompletePathIterator(transform, flatness); } /** * Draw this axis in the specified graphics context. This method is equivalents * to {@code Graphics2D.draw(this)}. However, this method may be slightly * faster and produce better quality output. * * @param graphics The graphics context to use for drawing. */ public synchronized void paint(final Graphics2D graphics) { if (!(getLength() > 0)) { return; } /* * Initialise l'itérateur en appelant 'init' (contrairement à 'getPathIterator' * qui n'appelle que 'rewind') pour des résultats plus rapides et plus constants. */ TickPathIterator iterator = this.iterator; if (iterator != null) { iterator.init(null); } else { this.iterator = iterator = new TickPathIterator(null); } 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.Double(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.isTickDone()) { 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 DoubleDimension2D(maxWidth, 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 = AffineTransforms2D.transform(rotatedTr, bounds, bounds); } } lastContext = iterator.getFontRenderContext(); } /** * Returns the value of a single preference for the rendering algorithms. Hint categories * include controls for label fonts and colors. Some of the keys and their associated values * are defined in the {@link Graduation} interface. * * @param key The key corresponding to the hint to get. * @return An object representing the value for the specified hint key, or {@code null} * if no value is associated to the specified key. * * @see Graduation#TICK_LABEL_FONT * @see Graduation#AXIS_TITLE_FONT */ public synchronized Object getRenderingHint(final RenderingHints.Key key) { return (hints != null) ? hints.get(key) : null; } /** * Sets the value of a single preference for the rendering algorithms. Hint categories * include controls for label fonts and colors. Some of the keys and their associated * values are defined in the {@link Graduation} interface. * * @param key The key of the hint to be set. * @param value The value indicating preferences for the specified hint category. * A {@code null} value removes any hint for the specified key. * * @see Graduation#TICK_LABEL_FONT * @see Graduation#AXIS_TITLE_FONT */ public synchronized void setRenderingHint(final RenderingHints.Key key, final Object value) { modCount++; if (value != null) { if (hints == null) { hints = new RenderingHints(key, value); clearCache(); } else if (!value.equals(hints.put(key, value))) { clearCache(); } } else if (hints != null) { if (hints.remove(key) != null) { clearCache(); } if (hints.isEmpty()) { hints = null; } } } /** * Efface la cache interne. Cette méthode doit être appelée * chaque fois que des propriétés de l'axe ont changées. */ private void clearCache() { axisBounds = null; labelBounds = null; legendBounds = null; maximumSize = null; information = null; } /** * Returns a string representation of this axis. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)); return buffer.append("[\"").append(graduation.getTitle(true)).append("\"]").toString(); } /** * Returns this axis name and direction. Information include a name (usually the * {@linkplain Graduation#getTitle graduation title}) and an direction. The direction is usually * {@linkplain AxisDirection#DISPLAY_UP up} or {@linkplain AxisDirection#DISPLAY_DOWN down} * for vertical axis, {@linkplain AxisDirection#DISPLAY_RIGHT right} or * {@linkplain AxisDirection#DISPLAY_LEFT left} for horizontal axis, or * {@linkplain AxisDirection#OTHER other} otherwise. * * @return Axis name and direction. */ public synchronized DefaultCoordinateSystemAxis toCoordinateSystemAxis() { if (information == null) { String abbreviation = "z"; AxisDirection direction = AxisDirection.OTHER; if (x1 == x2) { if (y1 < y2) { direction = AxisDirection.DISPLAY_DOWN; } else if (y1 > y2) { direction = AxisDirection.DISPLAY_UP; } 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 axes. The affine transform will maps coordinates in the following way: * <p> * <ul> * <li>For each input coordinates (<var>x</var>,<var>y</var>), the <var>x</var> and * <var>y</var> values are expressed in the same units than the {@code xAxis} * and {@code yAxis} graduations, respectively.</li> * <li>The output point is the pixel's coordinates for the (<var>x</var>,<var>y</var>) * values. Changing the <var>x</var> value move the pixel location in parallel with * the {@code xAxis}, which may or may not be horizontal. Changing the <var>y</var> * value move the pixel location in parallel with the {@code yAxis}, which may or * may not be vertical.</li> * </ul> * * @param xAxis The <var>x</var> axis. This axis doesn't have to be horizontal; * it can have any orientation, including vertical. * @param yAxis The <var>y</var> axis. This axis doesn't have to be vertical; * it can have any orientation, including horizontal. * @return An affine transform mapping logical to pixels coordinates. */ public static AffineTransform createAffineTransform(final Axis2D xAxis, final Axis2D yAxis) { /* x * │ * │\ * │ \ P Soit: X : l'axe des <var>x</var> du graphique. * │ │ Y : l'axe des <var>y</var> du graphique. * \ │ P : un point à placer sur le graphique. * \│ (Px,Py) : les composantes du point P selon les axes x et y. * \ y (Pi,Pj) : les composantes du point P en coordonnées "pixels". * * Désignons par <b>ex</b> et <b>ey</b> des vecteurs unitaires dans la direction de l'axe des * <var>x</var> et l'axe des <var>y</var> respectivement. Désignons par <b>i</b> et <b>j</b> * des vecteurs unitaires vers le droite et vers le haut de l'écran respectivement. On peut * décomposer les vecteurs unitaires <b>ex</b> et <b>ey</b> par: * * ex = exi*i + exj*j * ey = eyi*i + eyj*j * Donc, P = Px*ex + Py*ey = (Px*exi+Py*eyi)*i + (Px*exj + Py*eyj)*j * * Cette relation ne s'applique que si les deux systèmes de coordonnées (xy et ij) ont * la même origine. En pratique, ce ne sera pas le cas. Il faut donc compliquer un peu: * * Pi = (Px-Ox)*exi+(Py-Oy)*eyi + Oi où (Ox,Oy) sont les minimums des axes des x et y. * Pj = (Px-Ox)*exj+(Py-Oy)*eyj + Oj (Oi,Oj) est l'origine du système d'axe ij. * * ┌ ┐ ┌ ┐┌ ┐ ┌ ┐ * │ Pi │ │ exi eyi Oi-(Ox*exi+Oy*eyi) ││ Px │ │ exi*Px + eyi*Py + Oi-(Ox*exi+Oy*oyi) │ * │ Pj │ = │ exj eyj Oj-(Ox*exj+Oy*eyj) ││ Py │ = │ exj*Px + eyj*Py + Oj-(Ox*exj+Oy*oyj) │ * │ 1 │ │ 0 0 1 ││ 1 │ │ 1 │ * └ ┘ └ ┘└ ┘ └ ┘ */ final double px, ox, exi, exj; synchronized (xAxis) { final Graduation mx = xAxis.getGraduation(); final double range = mx.getSpan(); px = xAxis.getX1(); exi = (xAxis.getX2() - px) / range; exj = (xAxis.getY2() - xAxis.getY1()) / range; ox = mx.getMinimum(); } final double py, oy, eyi, eyj; synchronized (yAxis) { final Graduation my = yAxis.getGraduation(); final double range = my.getSpan(); py = yAxis.getY1(); eyi = (yAxis.getX2() - yAxis.getX1()) / range; eyj = (yAxis.getY2() - py) / range; oy = my.getMinimum(); } return new AffineTransform(exi, exj, eyi, eyj, px - (ox*exi + oy*eyi), py - (ox*exj + oy*eyj)); } //////////////////////////////////////////////////////////////////////////////////////////// //////// //////// //////// TICK AND PATH ITERATORS //////// //////// //////// //////////////////////////////////////////////////////////////////////////////////////////// /** * Iterates along the graduation ticks and provides access to the graduation values. Each * {@code Axis2D.TickIterator} object traverses the graduation of the unclosing {@link Axis2D} * object independently from any other {@link TickIterator} objects in use at the same time. * If a change occurs in the underlying {@link Axis2D} object during the iteration, then * {@link #refresh} must be invoked in order to reset the iterator as if a new instance was * created. Except for {@link #refresh} method, using the iterator after a change in the * underlying {@link Axis2D} may thrown a {@link ConcurrentModificationException}. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 2.0 * @module */ public class TickIterator implements org.geotoolkit.display.axis.TickIterator { /** * The underyling tick iterator. */ @SuppressWarnings("hiding") private org.geotoolkit.display.axis.TickIterator iterator; /** * A copy of {@link Axis2D#hints}. A copy is required because some hints (especially * {@link Graduation#VISUAL_AXIS_LENGTH} and {@link Graduation#VISUAL_TICK_SPACING}) * are going to be overwritten. This map may also contains additional hints provided * by {@link Graphics2D} in the {@link Axis2D#paint} method. This object will never * be {@code null}. */ @SuppressWarnings("hiding") private final RenderingHints hints; /** * {@code scaleX} and {@code scaleY} are used for scaling logical coordinates to pixel * coordinates. Those scale factors <strong>must</strong> be the same than the one that * appears in {@link Axis2D#createAffineTransform}. */ private double scaleX, scaleY; /** * {@code (tickX, tickY)} is a unitary vector perpendicular to the axis. */ private double tickX, tickY; /** * The minimum value {@link Graduation#getMinimum}. * This value is copied here for faster access. */ private double minimum; /** * Value returned by the last call to {@link #currentLabel}. This value is * cached here in order to avoid that {@link #getGlyphVector} computes it again. */ private transient String label; /** * Value returned by the last call to {@link #getGlyphVector}. This value is * cached here in order to avoid that {@link #getBounds} computes it again. */ private transient GlyphVector glyphs; /** * The font to use for rendering tick. If a rendering hint was provided for the * {@link Graduation#TICK_LABEL_FONT} key, then the value is used as the font. * Otherwise, a default font is created and used. */ private transient Font font; /** * The font context from {@link Graphics2D#getFontContext}, * or {@code null} for a default one. */ private transient FontRenderContext fontContext; /** * Value of {@link Axis2D#modCount} when {@link #init} was last invoked. This value is * used in order to detect changes to the underlying {@link Axis2D} during iteration. */ @SuppressWarnings("hiding") private transient int modCount; /** * Constructs an iterator. * * @param fontContext Information needed to correctly measure text, or * {@code null} if unknown. This object is usually given by * {@link Graphics2D#getFontRenderContext}. */ @SuppressWarnings({"unchecked","rawtypes"}) public TickIterator(final FontRenderContext fontContext) { /* * The unsafe cast below is a workaround for the mismatch between the generic types * in the RenderingHints class declaration and in the constructor signature. It is * safe since our API allows only Key objects to be put in the map. We can not use * directly putAll(Axis2D.this.hints) instead because the Axis2D hints may be null. */ this.hints = new RenderingHints((Map) Axis2D.this.hints); this.fontContext = fontContext; refresh(); } /** * Copies a rendering hints from the specified {@link Graphics2D}, providing that * it is not already defined. */ final void setRenderingHint(final Graphics2D graphics, final RenderingHints.Key key) { if (hints.get(key) == null) { final Object value = graphics.getRenderingHint(key); if (value != null) { hints.put(key, value); } } } /** * {@inheritDoc} */ @Override public boolean isDone() { return iterator.isDone(); } /** * {@inheritDoc} */ @Override public boolean isMajorTick() { return iterator.isMajorTick(); } /** * {@inheritDoc} */ @Override public double currentPosition() { return iterator.currentPosition(); } /** * {@inheritDoc} */ @Override public double currentValue() { return iterator.currentValue(); } /** * Returns the coordinates of the intersection point between current tick * and the underlying axis. Units are the same than axis start point * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>}) * and end point * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}). * This is usually pixels. * * @param dest A destination point that stores the intersection coordinates, * or {@code null} to create a new {@link Point2D} object. * @return {@code dest}, or a new {@link Point2D} object if {@code dest} was null. */ public Point2D currentPosition(final Point2D dest) { final double position = currentPosition() - minimum; final double x = position*scaleX + getX1(); final double y = position*scaleY + getY1(); ensureValid(); if (dest != null) { dest.setLocation(x,y); return dest; } return new Point2D.Double(x, y); } /** * Returns the coordinates of the current tick. * Units are the same than axis start point * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>}) * and end point * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}). * This is usually pixels. * * @param dest A destination line that stores the current tick coordinates, * or {@code null} to create a new {@link Line2D} object. * @return {@code dest}, or a new {@link Line2D} object if {@code dest} was null. */ public Line2D currentTick(final Line2D dest) { final boolean isMajorTick = isMajorTick(); final double position = currentPosition() - minimum; final double x = position*scaleX + getX1(); final double y = position*scaleY + getY1(); final double s1 = isMajorTick ? tickStart : subTickStart; final double s2 = isMajorTick ? tickEnd : subTickEnd; final double x1 = x+tickX*s1; final double y1 = y+tickY*s1; final double x2 = x+tickX*s2; final double y2 = y+tickY*s2; ensureValid(); if (dest != null) { dest.setLine(x1, y1, x2, y2); return dest; } return new Line2D.Double(x1, y1, x2, y2); } /** * {@inheritDoc} */ @Override public String currentLabel() { if (label == null) { label = iterator.currentLabel(); } return label; } /** * Returns the label for current tick as a glyphs vector. This method is used * together with {@link #currentLabelBounds} for labels rendering. <strong>Do * not change the returned {@link GlyphVector}</strong>, since the glyphs vector * is not cloned for performance raisons. This method returns {@code null} * if it can't produces a glyph vector for current tick. * * @return The label for the current tick as a glyphs vector. */ public GlyphVector currentLabelGlyphs() { if (glyphs == null) { final String label = currentLabel(); if (label != null) { glyphs = getTickFont().createGlyphVector(getFontRenderContext(), label); } } return glyphs; } /** * Returns a bounding box for the current tick label. Units are the same than axis start point * ({@linkplain #getX1 <var>x<sub>1</sub></var>},{@linkplain #getY1 <var>y<sub>1</sub></var>}) * and end point * ({@linkplain #getX2 <var>x<sub>2</sub></var>},{@linkplain #getY2 <var>y<sub>2</sub></var>}). * This is usually pixels. This method can be used as in the example below: * * {@preformat java * Axis2D.TickIterator iterator = axis.new TickIterator(null); * while (iterator.hasNext()) { * GlyphVector glyphs = iterator.currentLabelGlyphs(); * Rectangle2D bounds = iterator.currentLabelBounds(); * graphics.drawGlyphVector(glyphs, (float) bounds.getMinX(), (float) bounds.getMaxY()); * iterator.next(); * } * } * * This method returns {@code null} if it can't compute bounding box for current tick. * * @return A bounding box for the current tick label. */ 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: * * {@preformat java * Graphics2D graphics = ... * FontRenderContext fc = graphics.getFontRenderContext(); * TickIterator it = axis.new TickIterator(fc); * Font font = it.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 (<var>x</var>, * <var>y</var>) de ce rectangle est généralement (mais pas obligatoirement) * l'origine (0,0). Ce rectangle est habituellement obtenu par un appel à * {@link Font#createGlyphVector(FontContext,String)}. * @param toRotate * Si non-nul, transformation affine sur laquelle appliquer une rotation égale à * l'angle de l'axe. Cette méthode peut limiter la rotation aux quadrants 1 et 2 * afin de conserver une lecture agréable du texte. * @param maximumSize * Largeur et hauteur maximales des étiquettes de graduation. Cette information * est utilisée pour écarter l'étiquette de l'axe suffisament pour qu'elle * n'écrase pas les étiquettes de graduation. * @return * Le rectangle {@code bounds}, modifié pour être centré sur l'axe. */ final Rectangle2D centerAxisLabel(final Rectangle2D bounds, final AffineTransform toRotate, final Dimension2D maximumSize) { final double height = bounds.getHeight(); final double width = bounds.getWidth(); final double tx = 0; final double ty = height + Math.abs(maximumSize.getWidth()*tickX) + Math.abs(maximumSize.getHeight()*tickY); final double x1 = getX1(); final double y1 = getY1(); final double x2 = getX2(); final double y2 = getY2(); ///////////////////////////////////// //// Compute unit vector (ux,uy) //// ///////////////////////////////////// double ux = x2 - x1; double uy = y2 - y1; double ul = Math.hypot(ux, uy); ux /= ul; uy /= ul; ////////////////////////////////////////////// //// Get the central position of the axis //// ////////////////////////////////////////////// double x = 0.5 * (x1+x2); double y = 0.5 * (y1+y2); //////////////////////////////////////// //// Apply the parallel translation //// //////////////////////////////////////// x += ux*tx; y += uy*tx; //////////////////////////////////// //// Adjust sign of unit vector //// //////////////////////////////////// ux *= relativeCCW; uy *= relativeCCW; ///////////////////////////////////////////// //// Apply the perpendicular translation //// ///////////////////////////////////////////// x += uy*ty; y -= ux*ty; /////////////////////////////////// //// Offset the point for text //// /////////////////////////////////// final double anchorX = x; final double anchorY = y; if (toRotate == null) { y += 0.5*height * (1-ux); x -= 0.5*width * (1-uy); } else { if (ux < 0) { ux = -ux; uy = -uy; y += height; } x -= 0.5*width; toRotate.rotate(Math.atan2(uy,ux), anchorX, anchorY); } bounds.setRect(x, y-height, width, height); ensureValid(); return bounds; } /** * Moves the iterator to the next minor or major tick. */ @Override 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. */ @Override public void nextMajor() { this.label = null; this.glyphs = null; iterator.nextMajor(); } /** * Reset the iterator on its first tick. All other properties are left unchanged. */ @Override 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.getSpan(); final double length = Math.hypot(dx, dy); hints.put(Graduation.VISUAL_AXIS_LENGTH, length); this.scaleX = dx/range; this.scaleY = dy/range; this.tickX = -dy/length*relativeCCW; this.tickY = +dx/length*relativeCCW; this.minimum = graduation.getMinimum(); this.iterator = graduation.getTickIterator(hints, iterator); this.modCount = Axis2D.this.modCount; } } /** * Retourne le contexte utilisé pour dessiner les caractères. * Cette méthode ne retourne jamais {@code null}. */ final FontRenderContext getFontRenderContext() { if (fontContext == null) { fontContext = new FontRenderContext(null, false, false); } return fontContext; } /** * Spécifie le contexte à utiliser pour dessiner les caractères, * ou {@code null} pour utiliser un contexte par défaut. */ final void setFontRenderContext(final FontRenderContext context) { fontContext = context; } /** * Vérifie que l'axe n'a pas changé depuis le dernier appel de {@link #init}. * Cette méthode doit être appelée <u>à la fin</u> des méthodes de cette classe * qui lisent les champs de {@link Axis2D}. */ final void ensureValid() { if (this.modCount != Axis2D.this.modCount) { throw new ConcurrentModificationException(); } } } /** * Itérateur balayant l'axe et ses barres de graduations pour leur traçage. Cet itérateur ne * balaye pas les étiquettes de graduations. Puisque cet itérateur ne retourne que des droites * et jamais de courbes, il ne prend pas d'argument {@code flatness}. * <p> * <strong>WARNING:</strong> There is a clash in the semantic of {@link #isDone} between the * {@link PathIterator} contract and the {@link TickIterator} contract. Since this object is * used only for iterating over the shape outline, not over tick, it should be of no concern * to the user. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 2.0 * @module */ private class TickPathIterator extends TickIterator implements PathIterator { /** * Transformation affine à appliquer sur les données. Il doit s'agir d'une transformation * affine appropriée pour l'écriture de texte (généralement en pixels ou en points). Il ne * s'agit <u>pas</u> de la transformation affine créée par * {@link Axis2D#createAffineTransform}. */ protected AffineTransform transform; /** * Coordonnées de la prochaine graduation à retourner par une des méthodes * {@code currentSegment(...)}. Ces coordonnées n'auront <u>pas</u> * été transformées selon la transformation affine {@link #transform}. */ private final Line2D.Double line = new Line2D.Double(); /** * Coordonnées du prochain point à retourner par une des méthodes * {@code currentSegment(...)}. Ces coordonnées auront été * transformées selon la transformation affine {@link #transform}. */ private final Point2D.Double point = new Point2D.Double(); /** * Type du prochain segment. Ce type est retourné par les méthodes * {@code currentSegment(...)}. Il doit s'agir en général d'une * des constantes {@link #SEG_MOVETO} ou {@link #SEG_LINETO}. */ private int type = SEG_MOVETO; /** * Entier indiquant quel sera le prochain item a retourner (début ou * fin d'une graduation, début ou fin de l'axe, etc.). Il doit s'agir * d'une des constantes {@link #AXIS_MOVETO}, {@link #AXIS_LINETO}, * {@link #TICK_MOVETO}, {@link #TICK_LINETO}, etc. */ private int nextType = AXIS_MOVETO; /** Constante pour {@link #nextType}.*/ private static final int AXIS_MOVETO = 0; /** Constante pour {@link #nextType}.*/ private static final int AXIS_LINETO = 1; /** Constante pour {@link #nextType}.*/ private static final int TICK_MOVETO = 2; /** Constante pour {@link #nextType}.*/ private static final int TICK_LINETO = 3; /** * Construit un itérateur. * * @param transform Transformation affine à appliquer sur les données. Il doit * s'agir d'une transformation affine appropriée pour l'écriture de * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u> * de la transformation affine créée par {@link Axis2D#createAffineTransform}. */ public TickPathIterator(final AffineTransform transform) { super(null); // 'refresh' est appelée par le constructeur parent. this.transform = transform; next(); } /** * Initialise cet itérateur. Cette méthode peut être appelée pour réutiliser un itérateur * qui a déjà servit, plutôt que d'en construire un autre. * * @param transform Transformation affine à appliquer sur les données. Il doit * s'agir d'une transformation affine appropriée pour l'écriture de * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u> * de la transformation affine créée par {@link Axis2D#createAffineTransform}. */ final void init(final AffineTransform transform) { refresh(); setFontRenderContext(null); this.type = SEG_MOVETO; this.nextType = AXIS_MOVETO; this.transform = transform; next(); } /** * Repositione l'itérateur au début de la graduation * avec une nouvelle transformation affine. */ public void rewind(final AffineTransform transform) { super.rewind(); // Keep 'fontContext'. this.type = SEG_MOVETO; this.nextType = AXIS_MOVETO; this.transform = transform; next(); } /** * Repositione l'itérateur au début de la graduation * en conservant la transformation affine actuelle. */ @Override public final void rewind() { rewind(transform); } /** * Return the winding rule for determining the insideness of the path. */ @Override public final int getWindingRule() { return WIND_NON_ZERO; } /** * Returns {@code true} if the underlying tick iterator is done. * This method is defined as a workaround for the clash between * {@link PathIterator#isDone} and {@link TickIterator#isDone}. */ final boolean isTickDone() { return super.isDone(); } /** * Tests if the iteration is complete. This is the {@link PathIterator#isDone()} * method which is defined here. In order to query {@link TickIterator#isDone()}, * invoke {@link #isTickDone()} instead. */ @Override public boolean isDone() { return (nextType == TICK_LINETO) && super.isDone(); } /** * 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}. */ @Override 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}. */ @Override public int currentSegment(final double[] coords) { coords[0] = point.x; coords[1] = point.y; return type; } /** * Moves the iterator to the next segment of the path forwards * along the primary direction of traversal as long as there are * more points in that direction. */ @Override public void next() { switch (nextType) { default: { // Should not happen throw new IllegalPathStateException(Integer.toString(nextType)); } case AXIS_MOVETO: { // Premier point de l'axe point.x = getX1(); point.y = getY1(); type = SEG_MOVETO; nextType = AXIS_LINETO; break; } case AXIS_LINETO: { // Fin de l'axe point.x = getX2(); point.y = getY2(); type = SEG_LINETO; nextType = TICK_MOVETO; break; } case TICK_MOVETO: { // Premier point d'une graduation currentTick(line); point.x = line.x1; point.y = line.y1; type = SEG_MOVETO; nextType = TICK_LINETO; break; } case TICK_LINETO: { // Dernier point d'une graduation point.x = line.x2; point.y = line.y2; type = SEG_LINETO; nextType = TICK_MOVETO; prepareLabel(); super.next(); break; } } if (transform != null) { transform.transform(point, point); } ensureValid(); } /** * Méthode appelée automatiquement par {@link #next} pour * indiquer qu'il faudra se préparer à tracer une étiquette. */ protected void prepareLabel() { } } /** * Itérateur balayant l'axe et ses barres de graduations pour leur traçage. * Cet itérateur balaye aussi les étiquettes de graduations. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 2.0 * @module */ private final class CompletePathIterator extends TickPathIterator { /** * Controle le remplacement des courbes par des droites. La valeur * {@link java.lang.Double#NaN} indique qu'un tel remplacement n'a pas lieu. */ private final double flatness; /** * Chemin de l'étiquette {@link #label}. */ private PathIterator path; /** * Etiquette de graduation à tracer. */ private Shape label; /** * Rectangle englobant l'étiquette {@link #label} courante. */ @SuppressWarnings("hiding") private Rectangle2D labelBounds; /** * Valeur maximale de {@code labelBounds.getWidth()} trouvée jusqu'à maintenant. */ private double maxWidth = 0; /** * Valeur maximale de {@code labelBounds.getHeight()} trouvée jusqu'à maintenant. */ private double maxHeight = 0; /** * Prend la valeur {@code true} lorsque la légende de l'axe a été écrite. */ private boolean isDone; /** * Construit un itérateur. * * @param transform Transformation affine à appliquer sur les données. Il doit * s'agir d'une transformation affine appropriée pour l'écriture de * texte (généralement en pixels ou en points). Il ne s'agit <u>pas</u> * de la transformation affine créée par {@link Axis2D#createAffineTransform}. * @param flatness Contrôle le remplacement des courbes par des droites. La valeur * {@link java.lang.Double#NaN} indique qu'un tel remplacement ne doit pas être fait. */ public CompletePathIterator(final AffineTransform transform, final double flatness) { super(transform); this.flatness = flatness; } /** * Retourne un itérateur balayant la forme géométrique spécifiée. */ private PathIterator getPathIterator(final Shape shape) { return isNaN(flatness) ? shape.getPathIterator(transform) : shape.getPathIterator(transform, flatness); } /** * Lance une exception; cet itérateur n'est conçu pour n'être utilisé qu'une seule fois. */ @Override public void rewind(final AffineTransform transform) { throw new UnsupportedOperationException(); } /** * Tests if the iteration is complete. */ @Override public boolean isDone() { return (path != null) ? path.isDone() : super.isDone(); } /** * Returns the coordinates and type of the current path segment in the iteration. */ @Override public int currentSegment(final float[] coords) { return (path != null) ? path.currentSegment(coords) : super.currentSegment(coords); } /** * Returns the coordinates and type of the current path segment in the iteration. */ @Override public int currentSegment(final double[] coords) { return (path != null) ? path.currentSegment(coords) : super.currentSegment(coords); } /** * Moves the iterator to the next segment of the path forwards along the primary * direction of traversal as long as there are more points in that direction. */ @Override public void next() { if (path != null) { path.next(); if (!path.isDone()) { return; } path = null; } if (label != null) { path = getPathIterator(label); label = null; if (path != null) { if (!path.isDone()) { return; } path = null; } } if (!isDone) { super.next(); if (isDone()) { /* * Quand tout le reste est terminé, prépare l'écriture de la légende de l'axe. */ isDone = true; final String title = graduation.getTitle(true); if (title != null) { final GlyphVector glyphs; glyphs = getTitleFont().createGlyphVector(getFontRenderContext(), title); if (transform != null) { transform = new AffineTransform(transform); } else { transform = new AffineTransform(); } final Rectangle2D bounds = centerAxisLabel(glyphs.getVisualBounds(), transform, new DoubleDimension2D(maxWidth, maxHeight)); path = getPathIterator(glyphs.getOutline((float) bounds.getMinX(), (float) bounds.getMaxY())); } } } } /** * Méthode appelée automatiquement par {@link #next} pour * indiquer qu'il faudra se préparer à tracer une étiquette. */ @Override protected void prepareLabel() { if (isMajorTick()) { final GlyphVector glyphs = currentLabelGlyphs(); final Rectangle2D bounds = currentLabelBounds(); if (glyphs!=null && bounds!=null) { if (labelBounds==null || !labelBounds.intersects(bounds)) { label = glyphs.getOutline((float) bounds.getMinX(), (float) bounds.getMaxY()); final double width = bounds.getWidth(); final double height = bounds.getHeight(); if (width > maxWidth) maxWidth =width; if (height > maxHeight) maxHeight=height; labelBounds=bounds; } } } } } }