/*****************************************************************************
 * Java3D demonstration program to render the Lorentz "butterfly".
 * 
 * Copyright (c) 2009 Four Delta Consulting Ltd.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *       
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *       
 *     * Neither the name of Four Delta Consulting nor the names of its 
 *       contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY FOUR DELTA CONSULTING "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL FOUR DELTA CONSULTING BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ****************************************************************************/

package com.fourdelta.lorentz;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;

import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.LineArray;
import javax.media.j3d.LineAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
 * Java3D demonstration program to render the Lorentz "butterfly".
 */
public class LorentzApp extends JFrame {
	private static final long serialVersionUID = 2839740866350009392L;

	// Lorentz Equation constants
	private static final double 	A = 12.0;
	private static final double 	B = 20.0;
	private static final double 	C = 3.666;
	
	// rendering attributes
	private static final float 		LINE_WIDTH = 1.0f;
	private static final int 		ITER = 2000;
	private static final boolean 	ANTIALIAS = true;
	
	private LorentzApp() {
		super("Lorentz 3D");
		
		// set up the frame
		setLayout(new BorderLayout());
		setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		setPreferredSize(new Dimension(640, 480));
		
		// create a 3D canvas
        final GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
        final Canvas3D canvas = new Canvas3D(config);

        // add the canvas to the frame 
        add(canvas, BorderLayout.CENTER);

        // create the Java 3D universe
        final SimpleUniverse universe = createUniverse(canvas);

        // create the Java 3D scene & attach it to the universe  
        universe.addBranchGraph(createScene());

        // display the frame
		pack();
		setVisible(true);
	}

	///////////////////////////////////////////////////////////////////////////////////
	// Application entry point
	
	public static void main(final String[] args) {
		new LorentzApp();
	}

	///////////////////////////////////////////////////////////////////////////////////
	// Java3D universe creation

	private SimpleUniverse createUniverse(final Canvas3D canvas) {
		final SimpleUniverse universe = new SimpleUniverse(canvas);

        // add mouse behaviour to the ViewingPlatform
        ViewingPlatform viewPlatform = universe.getViewingPlatform();

        // move the ViewPlatform back a bit
        viewPlatform.setNominalViewingTransform();

        // add orbit behaviour to the ViewingPlatform
        OrbitBehavior orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL);
        BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 100.0);
        orbit.setSchedulingBounds(bounds);
        viewPlatform.setViewPlatformBehavior(orbit);

        return universe;
	}
	

	///////////////////////////////////////////////////////////////////////////////////
	// Java3D scene creation
	
    private BranchGroup createScene()
    {
        // create the root of the branch graph
    	final BranchGroup root = new BranchGroup();

        // create a bounds for the background and lights
    	final BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 100.0);

    	// create some colors
    	final Color3f backColor = new Color3f(Color.BLACK);
    	final Color3f ambientColor = new Color3f(Color.WHITE);

    	// set up the background
    	final Background background = new Background(backColor);
        background.setApplicationBounds(bounds);
        root.addChild(background);

        // set up the ambient light
        final AmbientLight ambientLight = new AmbientLight(ambientColor);
        ambientLight.setInfluencingBounds(bounds);
        root.addChild(ambientLight);

        // create a global identity TransformGroup for the scene
        final TransformGroup masterTrans = new TransformGroup();
        root.addChild(masterTrans);
        
        // scale and translate the rendered object 
        final TransformGroup objectTrans = new TransformGroup();
        final Transform3D transform = new Transform3D();
        transform.setScale(0.03);
        transform.setTranslation(new Vector3d(0.0, 0.0, -0.5)); // translate to origin
        objectTrans.setTransform(transform);
        
        // create the equations
        final Shape3D equationObject = new Shape3D();
        equationObject.setCapability(Shape3D.ALLOW_APPEARANCE_READ);
        equationObject.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE);
        equationObject.setCapability(Shape3D.ALLOW_GEOMETRY_READ);
        equationObject.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE);
        equationObject.setGeometry(calculateGeometry());
        equationObject.setAppearance(createAppearance());

		// add the rendered object to its transform
        objectTrans.addChild(equationObject);
        
        // add the object transform group to the master transform
        masterTrans.addChild(objectTrans);
        
        // compile the scene graph.
        root.compile();

        return root;
    }
    
	///////////////////////////////////////////////////////////////////////////////////
	// Java3D object creation
	
	/*
	 * Calculate the geometry by iterating the solution of the equation set.
	 * 
	 * @return a LineArray of the geometry. 
	 */
    private LineArray calculateGeometry()
    {
    	final double step = 0.01f;

    	final float[] points = new float[ITER * 6];
    	final float[] colors = new float[ITER * 6];

        // initial conditions
        double[] current = { 0.0, 1.0, 0.0 };
        double[] derivs = derivatives(current);
        double time = step;
        
        final double period = ITER / 100.0;
        for (int index = 0; index < ITER; ++index)
        {
        	// stepwise integrate the equations to get the next values 
        	final double[] next = integrate(current, derivs, time, step);
            
        	// create a rainbow color sweep in RGB
        	final double red = ((1.0 + Math.sin(index * Math.PI / period)) / 2.0);
        	final double green = ((1.0 + Math.sin(index * Math.PI / period + 0.6667 * Math.PI)) / 2.0);
        	final double blue = ((1.0 + Math.sin(index * Math.PI / period + 1.3333 * Math.PI)) / 2.0);

        	// set the points for the line segment
            points[index * 6 + 0] = (float)current[0];
            points[index * 6 + 1] = (float)current[1];
            points[index * 6 + 2] = (float)current[2];
            points[index * 6 + 3] = (float)next[0];
            points[index * 6 + 4] = (float)next[1];
            points[index * 6 + 5] = (float)next[2];

            // set the start & end colours for the line segment
            colors[index * 6 + 0] = (float)red;
            colors[index * 6 + 1] = (float)green;
            colors[index * 6 + 2] = (float)blue;
            colors[index * 6 + 3] = (float)red;
            colors[index * 6 + 4] = (float)green;
            colors[index * 6 + 5] = (float)blue;

            current = next;
            time += step;
        }
        
        // build the line array representing the normals
        final LineArray line = new LineArray(ITER * 2, GeometryArray.COORDINATES | GeometryArray.COLOR_3);
        line.setCoordinates(0, points);
        line.setColors(0, colors);
        
        return line;
    }
    
    /**
     * Create the appearance for the rendered object.
     * 
     * @return an Appearance.
     */
	private Appearance createAppearance() {
		final Appearance appearance = new Appearance();
		LineAttributes attrs = new LineAttributes(LINE_WIDTH, LineAttributes.PATTERN_SOLID, true);
		attrs.setLineAntialiasingEnable(ANTIALIAS);
		appearance.setLineAttributes(attrs);
		return appearance;
	}
    
	///////////////////////////////////////////////////////////////////////////////////
	// Lorentz Equations

    /**
     * Get the derivatives of the Lorentz equation system w.r.t. time.
     * 
     * @param current the current values of the equation system.
     * @return the derivatives.
     */
	private double[] derivatives(double[] current) {
        final double x = current[0];
        final double y = current[1];
        final double z = current[2];
        
        return new double[] { 
            A * (y - x),       // dx / dt
            x * (B - z) - y,   // dy / dt
            x * y - C * z      // dz / dt
        };
	}

	///////////////////////////////////////////////////////////////////////////////////
	// Solver

	/**
     * Given values for the variables current[1..n] and their derivatives derivs[1..n] known at time t,
     * advance the solution over an interval step and return the incremented variables.
     * 
     * @param current current values at starting time
     * @param derivs derivatives at starting time
     * @param time current time value
     * @param step step size
     * @return resulting values at time + step
     */
	private double[] integrate(double[] current, double[] derivs, double time, double step) {
		// this is a simple Euler (straight line approximation)
        final int n = current.length;
        final double[] d = derivatives(current);
        final double[] result = new double[n];
        for (int index = 0; index < n; ++index) {
            result[index] = current[index] + d[index] * step; 
        }
        return result;
	}
}
