/*
 * AffineTransformUtil.java
 *
 * Created on October 19, 2005, 10:46 AM
 *
 * Copyright (C) 2006-2007 United States Government as represented by 
 * Dataline, Inc.  
 * All Rights Reserved.
 *
 *
 * DEVELOPER POC
 * Michael W. Bishop
 * michael.bishop@je.jfcom.mil
 *
 *
 * GOVERNMENT POCs
 * Group Email: cdcie-info@je.jfcom.mil
 *
 * MAJ Jim Jackson
 * james.w.jackson@je.jfcom.mil
 * 757.203.5115 
 *
 * Boyd Fletcher
 * boyd.fletcher@je.jfcom.mil
 * 757.203.3290
 */
package mil.jfcom.cie.whiteboard.util;

import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;

import org.apache.batik.swing.JSVGCanvas;


/**
 * Class to handle working with AffineTransforms.
 *
 * @author Michael W. Bishop
 */
public class AffineTransformUtil {
    /** Creates a new instance of AffineTransformUtil */
    private AffineTransformUtil() {
    }

    /**
     * Get the rendering transform from the JSVGCanvas.
     *
     * @param jsvgCanvas An instance of JSVGCanvas to get the AffineTransform
     * from.
     * @param doInverse A boolean determining whether or not the returned
     * AffineTransform is an inverse.
     *
     * @return An AffineTransform based on the JSVGCanvas's rendering transform.
     */
    public static AffineTransform getAffineTransform(
        JSVGCanvas jsvgCanvas, boolean doInverse) {
        AffineTransform returnTransform = jsvgCanvas.getViewBoxTransform();

        if (doInverse) {
            try {
                returnTransform = returnTransform.createInverse();
            } catch (NoninvertibleTransformException ntE) {
                returnTransform = null;
            }
        }

        return returnTransform;
    }

    /**
     * Gets the rotation angle (radians) from the given AffineTransform.  Scale
     * values cannot be zero!
     *
     * This method:
     *
     * - "Unscales" the given transform by preconcatenating a scale by
     *   1 / scaleX and 1 / scaleY.  This is done in order to return a single
     *   rotation.
     *
     * - Finds the rotation in radians by calling Math.atan2(shearY, scaleX).
     *
     * @param inTransform An instance of AffineTransform.
     *
     * @return A double containing the rotation angle in radians.
     */
    public static double getRotationAngle(AffineTransform inTransform) {
        final AffineTransform cloneMatrix =
            (AffineTransform) inTransform.clone();
        double scaleX = 1.0d;
        double scaleY = 1.0d;
        final double newScaleX = inTransform.getScaleX();
        final double newScaleY = inTransform.getScaleY();

        if (newScaleX != 0.0) {
            scaleX = newScaleX;
        }

        if (newScaleY != 0.0) {
            scaleY = newScaleY;
        }

        // Pre-concatenate to the cloned matrix to undo scale values.
        cloneMatrix.preConcatenate(
            new AffineTransform(1.0 / scaleX, 0, 0, 1.0 / scaleY, 0, 0));

        // Find the rotation angle in radians.
        return Math.atan2(cloneMatrix.getShearY(), cloneMatrix.getScaleX());
    }

    /**
     * Gets the scale factors (pointX and pointY) from the given AffineTransform.  Scaling
     * is usually done around a center point or the origin.
     *
     * This method:
     *
     * - Reverts any rotation angle, rotating around the given (pointX,pointY) point.
     * - Returns the scaleX and scaleY factors of the result of "unrotating"
     *   the original matrix.
     *
     * @param inTransform An instance of AffineTransform.
     * @param pointX The pointX coordinate of the point to scale around.
     * @param pointY The pointY coordinate of the point to scale around.
     *
     * @return A double[] of {scaleX, scaleY}
     */
    public static double[] getScaleFactors(
        AffineTransform inTransform, double pointX, double pointY) {
        final AffineTransform cloneMatrix =
            (AffineTransform) inTransform.clone();
        AffineTransformUtil.revertRotationAngle(cloneMatrix, pointX, pointY);

        return new double[] { cloneMatrix.getScaleX(), cloneMatrix.getScaleY() };
    }

    /**
     * Transforms X/Y coordinates using the given AffineTransform.
     *
     * @param affineTransform The AffineTransform doing the transformation.
     * @param xValue An int with the x coordinate.
     * @param yValue An int with the y coordinate.
     *
     * @return A Point2D translated around the given coordinates.
     */
    public static Point2D convertPoint(
        AffineTransform affineTransform, int xValue, int yValue) {
        final Point2D inPoint = new Point2D.Double(xValue, yValue);

        return affineTransform.transform(inPoint, inPoint);
    }

    /**
     * Tests this class.
     *
     * @param args A String[] of command-line parameters.  Not needed for this
     * method.
     */
    public static void main(String[] args) {
        final AffineTransform affineTransform =
            new AffineTransform(1, 0, 0, 1, 0, 0);
        affineTransform.translate(10, 10);
        affineTransform.scale(3.0d, 8.0d);
        affineTransform.rotate((90.0d * Math.PI) / 180.0d);

        final double rotate =
            AffineTransformUtil.revertRotationAngle(affineTransform, 0, 0);
        System.out.println("Rotate: " + (180.0d / Math.PI * rotate));
        System.out.println("Transform: " + affineTransform);
    }

    /**
     * Removes the rotation angle on the given AffineTransform.  This method:
     *
     * - Calls getRotationAngle (see documentation).
     * - Rotates by negative the value returned around the point at (pointX,
     *   pointY).
     *
     * @param inTransform An instance of AffineTransform.
     * @param pointX The pointX coordinate of the point to rotate around.
     * @param pointY The pointY coordinate of the point to rotate around.
     *
     * @return A double (radians) of the rotation angle.
     */
    public static double revertRotationAngle(
        AffineTransform inTransform, double pointX, double pointY) {
        final double radians =
            AffineTransformUtil.getRotationAngle(inTransform);

        // Undo the rotation on the original matrix (radians).
        inTransform.rotate(-radians, pointX, pointY);

        // Return the value in radians.
        return radians;
    }

    /**
     * Rotate the given AffineTransform around the given point, the given number
     * of degrees (radians).  This method:
     *
     * - Calls revertRotationAngle, undoing any existing rotation.
     * - Rotates by radians radians around the point at (pointX,pointY).
     *
     * @param inTransform An instance of AffineTransform.
     * @param radians The degrees in radians to rotate.
     * @param pointX The X coordinate of the point to rotate around.
     * @param pointY The Y coordinate of the point to rotate around.
     *
     * @return A rotated instance of AffineTransform.
     */
    public static AffineTransform rotate(
        AffineTransform inTransform, double radians, double pointX,
        double pointY) {
        AffineTransformUtil.revertRotationAngle(inTransform, pointX, pointY);
        inTransform.rotate(radians, pointX, pointY);

        return inTransform;
    }

    /**
     * Scale the given AffineTransform around the given point by the given
     * scale factors.  This method:
     *
     * - Calls revertRotationAngle to remove the current rotation (t).
     * - Scales by 1 / scaleX and 1 / scaleY.  (Undoes any current scale)
     * - Untranslates according to old scale factor around (pointX, pointY).
     * - Translates to (pointX, pointY) based on old scale * new scale (scaleX and scaleY).
     * - Scales based on old scale * new scale.
     * - Reapplies the rotation (t).
     *
     * @param inTransform An instance of AffineTransform.
     * @param scaleX A double representing the scale multiple in the X plane.
     * @param scaleY A double representing the scale multiple in the Y plane.
     * @param pointX The X coordinate of the point to scale around.
     * @param pointY The Y coordinate of the point to scale around.
     *
     * @return A scaled instance of AffineTransform.
     */
    public static AffineTransform scale(
        AffineTransform inTransform, double scaleX, double scaleY, double pointX,
        double pointY) {
        final double radians =
            AffineTransformUtil.revertRotationAngle(
                inTransform, pointX, pointY);
        final double currentScaleX = inTransform.getScaleX();
        final double currentScaleY = inTransform.getScaleY();
        final double newScaleX = currentScaleX * scaleX;
        final double newScaleY = currentScaleY * scaleY;
        final double reverseTranslateX = -pointX * (currentScaleX - 1.0d);
        final double reverseTranslateY = -pointY * (currentScaleY - 1.0d);
        double scaleXValue = 1.0d;
        double scaleYValue = 1.0d;

        if (currentScaleX != 0.0d) {
            scaleXValue = currentScaleX;
        }

        if (currentScaleY != 0.0d) {
            scaleYValue = currentScaleY;
        }

        if ((currentScaleY != 0.0d) && (currentScaleX != 0.0d)) {
            inTransform.scale(1.0d / scaleXValue, 1.0d / scaleYValue);
        }

        inTransform.translate(-reverseTranslateX, -reverseTranslateY);

        final double translateX = -pointX * (newScaleX - 1.0d);
        final double translateY = -pointY * (newScaleY - 1.0d);
        inTransform.translate(translateX, translateY);
        inTransform.scale(newScaleX, newScaleY);
        inTransform.rotate(radians, pointX, pointY);

        return inTransform;
    }

    /**
     * Translate the given AffineTransform around the given point by the given
     * amounts.  pointX and pointY are needed to undo any scaling if present.
     * This method:
     *
     * - Calls revertRotationAngle to remove the current rotation.
     * - Scales by 1 / scaleX and 1 / scaleY.
     * - Untranslates according to scale factor around (pointX, pointY).
     * - Translates to (transX, transY).
     * - Retranslates according to scale factor around (pointX, pointY).
     * - Rescales according to scale factor.
     * - Reapplies the rotation.
     *
     * @param inTransform An instance of AffineTransform.
     * @param transX A double representing the translation in the X plane.
     * @param transY A double representing the translation in the Y plane.
     * @param pointX The X coordinate of the point to scale around.
     * @param pointY The Y coordinate of the point to scale around.
     *
     * @return A translated instance of AffineTransform.
     */
    public static AffineTransform translate(
        AffineTransform inTransform, double transX, double transY, double pointX,
        double pointY) {
        final double radians =
            AffineTransformUtil.revertRotationAngle(
                inTransform, pointX, pointY);
        final double currentScaleX = inTransform.getScaleX();
        final double currentScaleY = inTransform.getScaleY();
        final double reverseTranslateX = -pointX * (currentScaleX - 1.0d);
        final double reverseTranslateY = -pointY * (currentScaleY - 1.0d);

        if ((currentScaleY != 0.0d) && (currentScaleX != 0.0d)) {
            inTransform.scale(1.0d / currentScaleX, 1.0d / currentScaleY);
        }

        inTransform.translate(-reverseTranslateX, -reverseTranslateY);
        inTransform.translate(transX, transY);
        inTransform.translate(reverseTranslateX, reverseTranslateY);
        inTransform.scale(currentScaleX, currentScaleY);
        inTransform.rotate(radians, pointX, pointY);

        return inTransform;
    }
}
