/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package mil.jfcom.cie.whiteboard;

import java.awt.BorderLayout;
import java.awt.geom.AffineTransform;
import java.text.MessageFormat;
import javax.swing.JFrame;
import javax.swing.JLabel;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.parser.AWTTransformProducer;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.util.SVGConstants;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Element;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGLocatable;
import org.w3c.dom.svg.SVGRect;

/**
 * Test case against Batik 1.7 release.
 * 
 * - Rotates a "rect" element 360 degrees without any graphical artifacts.
 * - Rotates a "text" element 350 degrees.  Graphical artifacts are shown from
 *   90 to 270 degrees.
 * 
 * @author Michael W. Bishop
 */
public class TestElementTransform {
    public SVGDocument svgDocument = null;
    public Element textElement = null;
    public Element rectElement = null;
    public JSVGCanvas jsvgCanvas = null;
    public JLabel degreeLabel = new JLabel("0");
    protected static final String MATRIX_STRING =
        "matrix({0}, {1}, {2}, {3}, {4}, {5})";
    
    public void createCanvas() {
        jsvgCanvas = new JSVGCanvas(null, true, false);
        jsvgCanvas.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC);
        jsvgCanvas.setDoubleBuffered(true);
        jsvgCanvas.setDoubleBufferedRendering(true);
    }
    
    public void initializeDoc() {
        final DOMImplementation domImpl =
            SVGDOMImplementation.getDOMImplementation();
        svgDocument =
            (SVGDocument) domImpl.createDocument(
                SVGConstants.SVG_NAMESPACE_URI,
                SVGConstants.SVG_SVG_TAG,
                null);
        svgDocument.getDocumentElement().setAttribute(
            SVGConstants.SVG_WIDTH_ATTRIBUTE,
            "640");
        svgDocument.getDocumentElement().setAttribute(
            SVGConstants.SVG_HEIGHT_ATTRIBUTE,
            "480");
        svgDocument.getDocumentElement().setAttribute(
            SVGConstants.SVG_VIEW_BOX_ATTRIBUTE,
            "0 0 640 480");
        textElement =
            svgDocument.createElementNS(
                SVGConstants.SVG_NAMESPACE_URI,
                SVGConstants.SVG_TEXT_TAG);
        textElement.setAttribute(
            SVGConstants.SVG_STYLE_ATTRIBUTE,
            "font-family: sans-serif; font-weight: normal; font-style: normal; font-size: 64");
        textElement.setAttribute(SVGConstants.SVG_X_ATTRIBUTE, "128");
        textElement.setAttribute(SVGConstants.SVG_Y_ATTRIBUTE, "220");
        textElement.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "black");
        textElement.setAttribute(SVGConstants.SVG_FILL_OPACITY_ATTRIBUTE, "1");
        textElement.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)");
        textElement.setTextContent("Transform Test");
        svgDocument.getDocumentElement().appendChild(textElement);
        
        rectElement = svgDocument.createElementNS(
                SVGConstants.SVG_NAMESPACE_URI,
                SVGConstants.SVG_RECT_TAG);
        rectElement.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "black");
        rectElement.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "4");
        rectElement.setAttribute(SVGConstants.SVG_X_ATTRIBUTE, "128");
        rectElement.setAttribute(SVGConstants.SVG_Y_ATTRIBUTE, "325");
        rectElement.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "100");
        rectElement.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "50");
        svgDocument.getDocumentElement().appendChild(rectElement);
    }
    
    public void loop(final Element rotateElement) throws InterruptedException {
        Thread.sleep(2000);
        final SVGRect boundingBox = ((SVGLocatable) rotateElement).getBBox(); 
        final double centerX = boundingBox.getX() + boundingBox.getWidth() * 0.5d;
        final double centerY = boundingBox.getY() + boundingBox.getHeight() * 0.5d;
        for (int count = 0; count < 360; count++) {
            final AffineTransform transform =
                    AWTTransformProducer.createAffineTransform(
                    rotateElement.getAttribute(
                    SVGConstants.SVG_TRANSFORM_ATTRIBUTE));
            final AffineTransform rotated = TestElementTransform.rotate(
                    transform, Math.toRadians(count), centerX, centerY);
            jsvgCanvas.getUpdateManager().getUpdateRunnableQueue().invokeLater(
                    new Runnable() {
                    public void run () {
                        final double[] matrixValues = new double[6];
                        rotated.getMatrix(matrixValues);

                        final Object[] args =
                            {
                                String.valueOf(matrixValues[0]), String.valueOf(
                                matrixValues[1]), String.valueOf(matrixValues[2]),
                                String.valueOf(matrixValues[3]), String.valueOf(
                                matrixValues[4]), String.valueOf(matrixValues[5])
                            };

                        rotateElement.setAttribute(
                        SVGConstants.SVG_TRANSFORM_ATTRIBUTE,
                        MessageFormat.format(MATRIX_STRING, args));
                    }
            });
            degreeLabel.setText(String.valueOf(count));
            Thread.sleep(20);
        }
    }
    
    /**
     * 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());
    }
    
    public static void main(String[] args) throws InterruptedException {
        final TestElementTransform main = new TestElementTransform();
        main.initializeDoc();

        final JFrame jFrame = new JFrame("Text Tooltip Test");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.getContentPane().setLayout(new BorderLayout());
        main.createCanvas();
        main.jsvgCanvas.setDocument(main.svgDocument);
        jFrame.getContentPane().add(main.jsvgCanvas, BorderLayout.CENTER);
        jFrame.getContentPane().add(main.degreeLabel, BorderLayout.SOUTH);
        jFrame.pack();
        jFrame.setVisible(true);
        main.loop(main.rectElement);
        main.loop(main.textElement);
    }
    
    /**
     * 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) {
        TestElementTransform.revertRotationAngle(inTransform, pointX, pointY);
        inTransform.rotate(radians, pointX, pointY);

        return inTransform;
    }
    
    /**
     * 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 =
            TestElementTransform.getRotationAngle(inTransform);

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

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