/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003, Geotools Project Managment Committee (PMC)
 *    (C) 2003, Institut de Recherche pour le D?veloppement
 *
 *    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.geometry.jts;

// J2SE dependencies
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import org.geotools.referencing.operation.matrix.XAffineTransform;


/**
 * Apply an arbitrary {@link AffineTransform} on a {@link Shape}. This class is
 * used internally by {@link RenderedMarks}. It is designed for reuse with many
 * different affine transforms and shapes. This class is <strong>not</strong>
 * thread-safe.
 * 
 * @source $URL:
 *         http://svn.geotools.org/geotools/trunk/gt/module/render/src/org/geotools/renderer/lite/TransformedShape.java $
 * @version $Id: TransformedShape.java 27862 2007-11-12 19:51:19Z desruisseaux $
 * @author Martin Desruisseaux
 */
public final class TransformedShape extends AffineTransform implements Shape {
	/**
	 * The wrapped shape.
	 */
	public Shape shape;

	/**
	 * A temporary point.
	 */
	private final Point2D.Double point = new Point2D.Double();

	/**
	 * A temporary rectangle.
	 */
	private final Rectangle2D.Double rectangle = new Rectangle2D.Double();

	/**
	 * Construct a transformed shape initialized to the identity transform.
	 */
	public TransformedShape() {
	}

	/**
	 * Returns the 6 coefficients values.
	 */
	public void getMatrix(final float[] matrix, int offset) {
		matrix[offset] = (float) getScaleX(); // m00
		matrix[++offset] = (float) getShearY(); // m10
		matrix[++offset] = (float) getShearX(); // m01
		matrix[++offset] = (float) getScaleY(); // m11
		matrix[++offset] = (float) getTranslateX(); // m02
		matrix[++offset] = (float) getTranslateY(); // m12
	}

	/**
	 * Set the transform from a flat matrix.
	 * 
	 * @param matrix
	 *            The flat matrix.
	 * @param offset
	 *            The index of the first element to use in <code>matrix</code>.
	 */
	public void setTransform(final float[] matrix, int offset) {
		setTransform(matrix[offset], matrix[++offset], matrix[++offset],
				matrix[++offset], matrix[++offset], matrix[++offset]);
	}

	/**
	 * Set the transform from a flat matrix.
	 * 
	 * @param matrix
	 *            The flat matrix.
	 * 
	 */
	public void setTransform(final double[] matrix) {
		setTransform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4],
				matrix[5]);
	}

	/**
	 * Apply a uniform scale.
	 */
	public void scale(final double s) {
		scale(s, s);
	}

	/**
	 * Tests if the specified coordinates are inside the boundary of the
	 * <code>Shape</code>.
	 */
	public boolean contains(double x, double y) {
		point.x = x;
		point.y = y;
		return contains(point);
	}

	/**
	 * Tests if a specified {@link Point2D} is inside the boundary of the
	 * <code>Shape</code>.
	 */
	public boolean contains(final Point2D p) {
		try {
			return shape.contains(inverseTransform(p, point));
		} catch (NoninvertibleTransformException exception) {
			exceptionOccured(exception, "contains");
			return false;
		}
	}

	/**
	 * Tests if the interior of the <code>Shape</code> entirely contains the
	 * specified rectangular area.
	 */
	public boolean contains(double x, double y, double width, double height) {
		rectangle.x = x;
		rectangle.y = y;
		rectangle.width = width;
		rectangle.height = height;
		return contains(rectangle);
	}

	/**
	 * Tests if the interior of the <code>Shape</code> entirely contains the
	 * specified <code>Rectangle2D</code>. This method might conservatively
	 * return <code>false</code>.
	 */
	public boolean contains(final Rectangle2D r) {
		try {
			return shape.contains(XAffineTransform.inverseTransform(this, r,
					rectangle));
		} catch (NoninvertibleTransformException exception) {
			exceptionOccured(exception, "contains");
			return false;
		}
	}

	/**
	 * Tests if the interior of the <code>Shape</code> intersects the interior
	 * of a specified rectangular area.
	 */
	public boolean intersects(double x, double y, double width, double height) {
		rectangle.x = x;
		rectangle.y = y;
		rectangle.width = width;
		rectangle.height = height;
		return intersects(rectangle);
	}

	/**
	 * Tests if the interior of the <code>Shape</code> intersects the interior
	 * of a specified <code>Rectangle2D</code>. This method might
	 * conservatively return <code>true</code>.
	 */
	public boolean intersects(final Rectangle2D r) {
		try {
			return shape.intersects(XAffineTransform.inverseTransform(this, r,
					rectangle));
		} catch (NoninvertibleTransformException exception) {
			exceptionOccured(exception, "intersects");
			return false;
		}
	}

	/**
	 * Returns an integer {@link Rectangle} that completely encloses the
	 * <code>Shape</code>.
	 */
	public Rectangle getBounds() {
		final Rectangle rect = shape.getBounds();
		return (Rectangle) XAffineTransform.transform(this, rect, rect);
	}

	/**
	 * Returns a high precision and more accurate bounding box of the
	 * <code>Shape</code> than the <code>getBounds</code> method.
	 * 
	 * @todo REVISIT: tranform currently results in a new rectangle being
	 *        created, is this a memory overhead?
	 */
	public Rectangle2D getBounds2D() {
		final Rectangle2D rect = shape.getBounds2D();
		return XAffineTransform.transform(this, rect, null);
		// REVISIT: used to read (this,rect,rect) - this can result in an
		// unmidifiable geometry exception
	}

	/**
	 * Returns an iterator object that iterates along the <code>Shape</code>
	 * boundary and provides access to the geometry of the <code>Shape</code>
	 * outline.
	 */
	public PathIterator getPathIterator(AffineTransform at) {
		if (!isIdentity()) {
			if (at == null || at.isIdentity()) {
				return shape.getPathIterator(this);
			}
			at = new AffineTransform(at);
			at.concatenate(this);
		}
		return shape.getPathIterator(at);
	}

	/**
	 * Returns an iterator object that iterates along the <code>Shape</code>
	 * boundary and provides access to a flattened view of the
	 * <code>Shape</code> outline geometry.
	 */
	public PathIterator getPathIterator(AffineTransform at,
			final double flatness) {
		if (!isIdentity()) {
			if (at == null || at.isIdentity()) {
				return shape.getPathIterator(this, flatness);
			}
			at = new AffineTransform(at);
			at.concatenate(this);
		}
		return shape.getPathIterator(at, flatness);
	}

	/**
	 * Invoked when an inverse transform was required but the transform is not
	 * invertible. This error should not happen. However, even if it happen, it
	 * will not prevent the application to work since <code>contains(...)</code>
	 * method may conservatively return <code>false</code>. We will just log
	 * a warning message and continue.
	 */
	private static void exceptionOccured(
			final NoninvertibleTransformException exception, final String method) {
		final LogRecord record = new LogRecord(Level.WARNING, exception
				.getLocalizedMessage());
		record.setSourceClassName(TransformedShape.class.getName());
		record.setSourceMethodName(method);
		record.setThrown(exception);
		org.geotools.util.logging.Logging.getLogger("org.geotools.renderer.lite").log(record);
	}
}
