/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 1998-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.shape; import java.io.Serializable; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.util.NoSuchElementException; import static java.lang.Double.doubleToLongBits; /** * Arrow oriented toward positives <var>x</var> values (0° arithmetic). This shape doesn't * have direct support for rotation. To rotate the arrow toward an other direction, use * {@link AffineTransform}. See also the example documented in the {@link TransformedShape} * class. * <p> * The following picture shows the default {@code Arrow2D} appearance. The relative size * of the tail can be modified by {@link #setTailProportion}. * * <center><img src="doc-files/Arrow2D.png"></center> * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 1.0 * @module */ public class Arrow2D extends RectangularShape implements Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = 5093131349056679731L; /** * Minimal <var>x</var> et <var>y</var> coordinate values. */ private double minX, minY; /** * Longueur de la flèche. Cette longueur est mesurée horizontalement (selon * l'axe des <var>x</var>) de la queue jusqu'à la pointe de la flèche. */ private double length; /** * Largeur de la flèche. Cette largeur est mesurée verticalement (selon l'axe * des <var>y</var>) le long de la partie la plus large de cette flèche. */ private double thickness; /** * The arrow's thickness at the tail ({@code x == minX}), as a proportion of the * {@linkplain #thickness maximal thickness}. Should be a factor between 0 and 1. */ private double sy0 = 0; /** * The arrow's thickness at the base ({@code x == minX+sx*length}), as a proportion of * the {@linkplain #thickness maximal thickness}. Should be a factor between 0 and 1. */ private double sy1 = 1.0 / 3; /** * The base position, as a factor of the total length. Should be a factor between 0 and 1. */ private double sx = 2.0 / 3; /** * Creates a new arrow with a null surface. */ public Arrow2D() { } /** * Creates new arrow in the specified {@linkplain #setFrame(double,double,double,double) frame}. * * @param x Minimal <var>x</var> value. * @param y Minimal <var>y</var> value. * @param width The length in <var>x</var> direction. * @param height The length in <var>y</var> direction. */ public Arrow2D(final double x, final double y, final double width, final double height) { this.minX = x; this.minY = y; this.length = width; this.thickness = height; } /** * Sets the tail width and height, relative to the arrow width and height. * All given number must be in the [0 … 1] range. * * @param sx The position where the arrow's head starts, relative to the total * arrow's {@linkplain #getWidth width}. * @param sy1 The height of the arrow's tail at the position where the head start * (<var>sx</var>), relative to the arrow's {@linkplain #getHeight height}. * @param sy0 The height of the arrow's tail at the leftmore position, relative to the * arrow's {@linkplain #getHeight height}. */ public void setTailProportion(double sx, double sy1, double sy0) { this.sy1 = Math.max(0, Math.min(1, sy1)); this.sy0 = Math.max(0, Math.min(1, sy0)); this.sx = Math.max(0, Math.min(1, sx )); } /** * Returns the length of the arrow's tail. This length is measured along the <var>x</var> * axis. * * @return The length of the arrow's tail. */ public double getTailLength() { return sx * length; } /** * Returns the minimal <var>x</var> coordinate of the smallest * {@linkplain #getBounds2D bounding box} that contains fully this arrow. */ @Override public double getX() { return minX; } /** * Returns the minimal <var>y</var> coordinate of the smallest * {@linkplain #getBounds2D bounding box} that contains fully this arrow. */ @Override public double getY() { return minY; } /** * Returns the width of the smallest {@linkplain #getBounds2D bounding box} that contains * fully this arrow. */ @Override public double getWidth() { return length; } /** * Returns the height of the smallest {@linkplain #getBounds2D bounding box} that contains * fully this arrow. */ @Override public double getHeight() { return thickness; } /** * Returns the arrow height at the given <var>x</var> ordinate. If the given position * is not between {@code getMinX()} and {@code getMaxX()}, then this method returns 0. * * @param x Ordinate <var>x</var> where to get the arrow height. * @return The arrow height at the given <var>x</var> ordinate, as a number * between 0 and {@code getHeight()}. */ public double getHeight(double x) { x = (x-minX) / (sx*length); if (x<0 || x>1) { return 0; } else if (x <= 1) { return (sy0 + (sy1 - sy0)*x)*thickness; } else { return (x-1) * sx / (1-sx) * thickness; } } /** * Determines whether the arrow is empty. */ @Override public boolean isEmpty() { return !(length>0 && thickness>0); // Uses '!' in order to catch NaN. } /** * Sets the location and size of the framing rectangle of this arrow to the specified * rectangular values. * * @param x The minimal <var>x</var> value. * @param y The minimal <var>y</var> value. * @param width The length in <var>x</var> direction. * @param height The length in <var>y</var> direction. */ @Override public void setFrame(final double x, final double y, final double width, final double height) { this.minX = x; this.minY = y; this.length = width; this.thickness = height; } /** * Returns the bounding box for this arrow. This is the smallest rectangle that * contains fully this arrow. */ @Override public Rectangle2D getBounds2D() { return new Rectangle2D.Double(minX, minY, length, thickness); } /** * Tests if the specified point is inside this shape. * * @param x The <var>x</var> coordinate to test. * @param y The <var>y</var> coordinate to test. */ @Override public boolean contains(final double x, double y) { if (x < minX) { return false; } final double base = minX + sx*length; if (x <= base) { /* * Point dans la queue. Vérifie s'il se trouve dans le triangle... */ double yMaxAtX = 0.5*thickness; y -= (minY + yMaxAtX); yMaxAtX *= sy0 + (sy1-sy0) * ((x-minX) / (base-minX)); return (Math.abs(y) <= yMaxAtX); } else { /* * Point dans la pointe. Vérifie s'il se trouve dans le triangle. */ final double maxX = minX + length; if (x > maxX) { return false; } double yMaxAtX = 0.5*thickness; y -= (minY + yMaxAtX); yMaxAtX *= (maxX-x) / (maxX-base); return Math.abs(y) <= yMaxAtX; } } /** * Tests if the interior of this arrow entirely contains the specified rectangle. * * @param x The minimal <var>x</var> value. * @param y The minimal <var>y</var> value. * @param width The rectangle width. * @param height The rectangle height. * @return {@code true} if the interior of this arrow contains the rectangle. */ @Override public boolean contains(final double x, final double y, final double width, final double height) { return contains(x , y ) && contains(x+width, y ) && contains(x+width, y+height) && contains(x , y+height); } /** * Tests if the interior of this arrow intersects the interior of the specified rectangle. * * @param x The minimal <var>x</var> value. * @param y The minimal <var>y</var> value. * @param width The rectangle width. * @param height The rectangle height. * @return {@code true} if the interior of this arrow intersects the interior of the rectangle. */ @Override public boolean intersects(final double x, final double y, final double width, final double height) { final double right = x + width; final double maxX = minX + length; if (x <= maxX && right >= minX) { final double top = y + height; final double maxY = minY + thickness; if (y <= maxY && top >= minY) { /* * The rectangle intersects this arrow's bounding box. Now, check if a * rectangle corner is outside the arrow (while in the bounding box). * If such a case is found, returns false. */ final double base = minX + length*sx; if (x > base) { double yMaxAtX = 0.5*thickness; final double centerY = minY + yMaxAtX; if (y >= centerY) { yMaxAtX *= (maxX-x)/(maxX-base); if (!(y-centerY <= yMaxAtX)) { return false; } } else if (top <= centerY) { yMaxAtX *= (maxX-x) / (maxX-base); if (!(centerY-top <= yMaxAtX)) { return false; } } } else if (right < base) { double yMaxAtX = 0.5*thickness; final double centerY = minY + yMaxAtX; if (y >= centerY) { yMaxAtX *= sy0 + (sy1-sy0)*((x-minX) / (base-minX)); if (!(y-centerY <= yMaxAtX)) { return false; } } else if (top <= centerY) { yMaxAtX *= sy0 + (sy1-sy0)*((x-minX) / (base-minX)); if (!(centerY-top <= yMaxAtX)) { return false; } } } return true; } } return false; } /** * Returns an iterator for this arrow. Because this shape is made only of straight * segments, this method ignores the {@code flatness} argument and delegates to * <code>{@linkplain #getPathIterator(AffineTransform) getPathIterator}(at)</code> * * @param at An optional affine transform to apply, or {@code null} if none. */ @Override public PathIterator getPathIterator(final AffineTransform at, final double flatness) { return getPathIterator(at); } /** * Returns an iterator for this arrow. * * @param at An optional affine transform to apply, or {@code null} if none. */ @Override public PathIterator getPathIterator(final AffineTransform at) { return new Iterator(at); } /** * The iterator for the enclosing arrow shape. */ private class Iterator implements PathIterator { /** * An optional affine transform to apply, or {@code null} if none. */ private final AffineTransform at; /** * Frequently used constants computed once for ever at construction time. */ private final double halfBottom0, halfBottom1, center, halfTop1, halfTop0, base; /** * Indicates which arrow edge will be the next one to be returned. */ private int code; /** * Creates an iterator for the enclosing arrow shape. * * @param at An optional affine transform to apply, or {@code null} if none. */ Iterator(final AffineTransform at) { this.at = at; final double halfheight = 0.5*thickness; halfBottom0 = minY + halfheight * (1-sy0); halfBottom1 = minY + halfheight * (1-sy1); center = minY + halfheight; halfTop1 = minY + halfheight * (1+sy1); halfTop0 = minY + halfheight * (1+sy0); base = minX + sx*length; } /** * Returns the winding rule, which is always {@link #WIND_EVEN_ODD}. */ @Override public int getWindingRule() { return WIND_EVEN_ODD; } /** * Move to the next segment. */ @Override public void next() { code++; } /** * Returns the coordinates for the current segment. */ @Override public int currentSegment(final float[] coords) { switch (code) { case 0: coords[0]=(float) minX; coords[1]=(float) halfBottom0; break; case 1: coords[0]=(float) base; coords[1]=(float) halfBottom1; break; case 2: coords[0]=(float) base; coords[1]=(float) minY; break; case 3: coords[0]=(float) (minX+length); coords[1]=(float) center; break; case 4: coords[0]=(float) base; coords[1]=(float) (minY+thickness); break; case 5: coords[0]=(float) base; coords[1]=(float) halfTop1; break; case 6: coords[0]=(float) minX; coords[1]=(float) halfTop0; break; case 7: coords[0]=(float) minX; coords[1]=(float) halfBottom0; break; case 8: return SEG_CLOSE; default: throw new NoSuchElementException(); } if (at!=null) { at.transform(coords, 0, coords, 0, 1); } return (code == 0) ? SEG_MOVETO : SEG_LINETO; } /** * Returns the coordinates for the current segment. */ @Override public int currentSegment(final double[] coords) { switch (code) { case 0: coords[0]=minX; coords[1]=halfBottom0; break; case 1: coords[0]=base; coords[1]=halfBottom1; break; case 2: coords[0]=base; coords[1]=minY; break; case 3: coords[0]=minX+length; coords[1]=center; break; case 4: coords[0]=base; coords[1]=minY+thickness; break; case 5: coords[0]=base; coords[1]=halfTop1; break; case 6: coords[0]=minX; coords[1]=halfTop0; break; case 7: coords[0]=minX; coords[1]=halfBottom0; break; case 8: return SEG_CLOSE; default: throw new NoSuchElementException(); } if (at!=null) { at.transform(coords, 0, coords, 0, 1); } return (code==0) ? SEG_MOVETO : SEG_LINETO; } /** * Returns {@code true} if there is no more point to iterate. */ @Override public boolean isDone() { return code > 8; } } /** * Compares this arrow with the specified object for equality. * * @param object The object to compare with this arrow for equality. * @return {@code true} if the given object is equal to this arrow. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (object != null && getClass() == object.getClass()) { final Arrow2D cast = (Arrow2D) object; return doubleToLongBits(thickness) == doubleToLongBits(cast.thickness) && doubleToLongBits(length ) == doubleToLongBits(cast.length ) && doubleToLongBits(minX ) == doubleToLongBits(cast.minX ) && doubleToLongBits(minY ) == doubleToLongBits(cast.minY ) && doubleToLongBits(sx ) == doubleToLongBits(cast.sx ) && doubleToLongBits(sy0 ) == doubleToLongBits(cast.sy1 ) && doubleToLongBits(sy1 ) == doubleToLongBits(cast.sy0 ); } else { return false; } } /** * Returns a hash value for this arrow. */ @Override public int hashCode() { final long code = doubleToLongBits(thickness) + 31* (doubleToLongBits(length ) + 31* (doubleToLongBits(minX ) + 31* (doubleToLongBits(minY ) + 31* (doubleToLongBits(sx ) + 31* (doubleToLongBits(sy0 ) + 31* (doubleToLongBits(sy1))))))); return (int) code + (int) (code >>> 32); } }