/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2011-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2011-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.awt.geom.Point2D; import java.awt.geom.PathIterator; import java.awt.geom.IllegalPathStateException; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.geotoolkit.resources.Errors; import org.apache.sis.referencing.operation.transform.MathTransforms; import static java.lang.Math.*; import static org.apache.sis.math.MathFunctions.SQRT_2; /** * A path iterator which applies a map projection on the fly. This iterator extends * {@link Point2D.Double} for opportunist reasons only - users should not rely on this * implementation details. * <p> * The point inherited by this class is the location of the last point of the last * call to a {@code currentSegment(...)} method. This location has been transformed * by the {@linkplain #projection}. * * @author Rémi Maréchal (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.20 * @module */ @SuppressWarnings("serial") final class ProjectedPathIterator extends Point2D.Double implements PathIterator { /** * Tolerance factor for determining when to replace a curve by a straight line. */ private static final double TOLERANCE = 1E-5; /** * The original path iterator. */ private final PathIterator iterator; /** * The transform to apply on the values returned by the path iterator. * This transform should be the concatenation of the map projection * given to the {@link ProjectedShape} constructor, together with the * affine transform given to the {@code getPathIterator(...)} method. */ private final MathTransform2D projection; /** * A temporary buffer used when the {@link #currentSegment(float[])} method is invoked. * Note that this is the case when the shape is drawn with Java2D. */ private final double[] buffer; /** * The original ordinate values of the last iteration step, before transformation. * The values of the last iteration steps after transformation are (x,y). */ private transient double λ, φ; /** * The ordinate values of the last "move to" operation, both as original ordinates * (λ,φ) and projected ordinates (x,y). */ private transient double λ0, φ0, x0, y0; /** * Derivative of the previous iteration step, or {@code null} if none. */ private transient Matrix derivative; /** * Control points computed by {@link #transformSegment}. */ private transient double ctrlx1, ctrly1, ctrlx2, ctrly2; /** * {@code true} if the next call to the {@code currentSegment} shall return {@code SEG_CLOSE}. */ private transient boolean isClosing; /** * Creates a new path iterator which will apply the given transform on the * points returned by the given iterator. */ ProjectedPathIterator(final PathIterator iterator, final MathTransform2D projection) { this.iterator = iterator; this.projection = projection; this.buffer = new double[2]; } /** * Returns the winding rule specified by the original iterator. */ @Override public int getWindingRule() { return iterator.getWindingRule(); } /** * Returns {@code true} if there is no more points to iterate over. */ @Override public boolean isDone() { return !isClosing && iterator.isDone(); } /** * Moves the iterator to the next segment. */ @Override public void next() { if (!isClosing) { iterator.next(); } } /** * Transforms the given number of points in the given array. The last points is stored in the * (λ,φ) and (x,y) fields of this iterator, in order to be reused during the next iteration. */ private void transform(final double[] coords, final int n) throws TransformException { final int i0 = (n-1) << 1; final int i1 = i0 + 1; λ = coords[i0]; φ = coords[i1]; projection.transform(coords, 0, coords, 0, n); x = coords[i0]; y = coords[i1]; derivative = null; } /** * Same method than above, but for floating point values. The last point is transformed * in a special way in order to keep the {@code double} precision. */ private void transform(final float[] coords, int n) throws TransformException { if (--n != 0) { projection.transform(coords, 0, coords, 0, n); } final int i0 = n << 1; final int i1 = i0 + 1; x = λ = coords[i0]; y = φ = coords[i1]; final Point2D p = projection.transform(this, this); if (p != this) { // paranoiac check (should never happen) setLocation(p); } coords[i0] = (float) x; coords[i1] = (float) y; derivative = null; } /** * Returns the coordinates and type of the current path segment in the iteration. * This method handles line segments in a special way, by attempting to replace * them by a curve computed from the derivative at the two end points. */ @Override @SuppressWarnings("fallthrough") public int currentSegment(final double[] coords) { if (isClosing) { isClosing = false; return SEG_CLOSE; } int type = iterator.currentSegment(coords); try { switch (type) { case SEG_MOVETO: { λ0 = coords[0]; φ0 = coords[1]; transform(coords, 1); x0 = coords[0]; y0 = coords[1]; break; } case SEG_QUADTO: transform(coords, 2); break; case SEG_CUBICTO: transform(coords, 3); break; case SEG_CLOSE: { if (x0 == x && y0 == y) { break; } else { isClosing = true; coords[0] = λ0; coords[1] = φ0; } // Fall through } case SEG_LINETO: { final double x1 = x; final double y1 = y; final double Δλ = coords[0] - λ; final double Δφ = coords[1] - φ; Matrix D1 = derivative; if (D1 == null) { x = λ; y = φ; D1 = projection.derivative(this); } λ = coords[0]; φ = coords[1]; derivativeAndTransform(coords); type = transformSegment(Δλ, Δφ, x1, y1, D1); int i=0; switch (type) { case SEG_CUBICTO: coords[i++] = ctrlx1; coords[i++] = ctrly1; // Fallthrough case SEG_QUADTO: coords[i++] = ctrlx2; coords[i++] = ctrly2; // Fallthrough case SEG_LINETO: coords[i++] = x; coords[i++] = y; } break; } } } catch (TransformException cause) { IllegalPathStateException ex = new IllegalPathStateException(cause.getLocalizedMessage()); ex.initCause(cause); throw ex; } return type; } /** * A copy of the above method, adapted for {@code float} arrays. */ @Override @SuppressWarnings("fallthrough") public int currentSegment(final float[] coords) { if (isClosing) { isClosing = false; return SEG_CLOSE; } // Following block is for debugging purpose only. Set the condition to 'true' in order // to delegates to the method using full 'double' precision. This is sometime useful // for making sure that the results are the same. if (false) { final double[] buffer = new double[min(coords.length, 6)]; final int type = currentSegment(buffer); for (int i=0; i<buffer.length; i++) { coords[i] = (float) buffer[i]; } return type; } // Copy of currentSegment(double[]). Note that this copy contains many implicit casts // from 'float' to 'double'. So while the source code looks close to identical to the // above method, the compiled code is actually not the same. The part which is really // the same has been factorized out in the transformSegment(...) method. int type = iterator.currentSegment(coords); try { switch (type) { case SEG_MOVETO: { λ0 = coords[0]; φ0 = coords[1]; transform(coords, 1); x0 = coords[0]; y0 = coords[1]; break; } case SEG_QUADTO: transform(coords, 2); break; case SEG_CUBICTO: transform(coords, 3); break; case SEG_CLOSE: { if (x0 == x && y0 == y) { break; } else { isClosing = true; coords[0] = (float) λ0; coords[1] = (float) φ0; } // Fall through } case SEG_LINETO: { final double x1 = x; final double y1 = y; final double Δλ = coords[0] - λ; final double Δφ = coords[1] - φ; Matrix D1 = derivative; if (D1 == null) { x = λ; y = φ; D1 = projection.derivative(this); } buffer[0] = λ = coords[0]; buffer[1] = φ = coords[1]; derivativeAndTransform(buffer); type = transformSegment(Δλ, Δφ, x1, y1, D1); int i=0; switch (type) { case SEG_CUBICTO: coords[i++] = (float) ctrlx1; coords[i++] = (float) ctrly1; // Fallthrough case SEG_QUADTO: coords[i++] = (float) ctrlx2; coords[i++] = (float) ctrly2; // Fallthrough case SEG_LINETO: coords[i++] = (float) x; coords[i++] = (float) y; } break; } } } catch (TransformException cause) { IllegalPathStateException ex = new IllegalPathStateException(cause.getLocalizedMessage()); ex.initCause(cause); throw ex; } return type; } /** * Computes the normalized derivative at the given position and transforms that position. * The transformed position (if any) is stored in the ({@linkplain #x},{@linkplain #y}) * fields. This method tries to perform the two operations in a single step if possible. */ private void derivativeAndTransform(final double[] coords) throws TransformException { derivative = MathTransforms.derivativeAndTransform(projection, coords, 0, coords, 0); if (derivative == null) { throw new TransformException(Errors.format(Errors.Keys.CantComputeDerivative)); } x = coords[0]; y = coords[1]; } /** * Transforms a line segment from P1 to P2, which may result in a quadratic or cubic curve. * <p> * <b>Notation:</b> * <ul> * <li>The (λ,φ) variable names are ordinates in the source space.</li> * <li>The (x,y) variable names are ordinates in the target space.</li> * <li>The 1 and 2 suffixes denote the starting and ending points respectively.</li> * <li>The Δ prefix denote the difference between the target and the source points.</li> * </ul> */ private int transformSegment( final double Δλ, final double Δφ, final double x1, final double y1, final Matrix D1) { final double x2 = x; final double y2 = y; final Matrix D2 = derivative; // (Δx,Δy) is the vector from P1 to P2 in the (x,y) space. final double Δx = (x2 - x1); final double Δy = (y2 - y1); final double L12 = hypot(Δx, Δy); // (Δx1,Δy1) is the (Δx,Δy) vector that we would have if the derivative was D1 everywhere. // (Δx2,Δy2) is the (Δx,Δy) vector that we would have if the derivative was D2 everywhere. final double Δx1 = D1.getElement(0,0)*Δλ + D1.getElement(0,1)*Δφ; final double Δy1 = D1.getElement(1,0)*Δλ + D1.getElement(1,1)*Δφ; final double Δx2 = D2.getElement(0,0)*Δλ + D2.getElement(0,1)*Δφ; final double Δy2 = D2.getElement(1,0)*Δλ + D2.getElement(1,1)*Δφ; final double L1 = hypot(Δx1, Δy1); final double L2 = hypot(Δx2, Δy2); final double s1 = (Δx*Δx1 + Δy*Δy1) / (L12*L1); final double s2 = (Δx*Δx2 + Δy*Δy2) / (L12*L2); // Collinearity between two derivatives vectors and the segment. if (!(abs(abs(s1)-1) > TOLERANCE) && !(abs(abs(s2)-1) > TOLERANCE)) { // Use ! for catching NaN. return SEG_LINETO; } // Segment projection form circle bow, can be replaced by quadratique curve. if (s1 >= SQRT_2 / 2 && abs(s1-s2) <= TOLERANCE) { /* * Reference to Thomas W. Sederberg article : COMPUTER AIDED GEOMETRIC DESIGN * Computes α as a weight coefficient of the delta to add to the starting point. */ final double α = abs((Δy2*Δx - Δx2*Δy) / (Δy2*Δx1 - Δx2*Δy1)); ctrlx2 = x1 + Δx1*α; ctrly2 = y1 + Δy1*α; return SEG_QUADTO; } ctrlx1 = x1 + Δx1/3; ctrly1 = y1 + Δy1/3; ctrlx2 = x2 - Δx2/3; ctrly2 = y2 - Δy2/3; return SEG_CUBICTO; } }