/* 
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. 
 * 
 * This file is available and licensed under the following license:
 * 
 * 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 Sun Microsystems 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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 solarsystem;

import javafx.animation.*;
import javafx.stage.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.effect.light.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import javafx.scene.layout.*;
import javafx.scene.text.*;
import javafx.util.*;
import java.lang.Math;


def earthOrbitTime = 2s;

def zoomSliderMax = 200;
def zoomSliderDefault = 5;
def zoomSlider = SwingSlider {maximum: zoomSliderMax, value: zoomSliderDefault};
var scaleFactor = bind (80.0 * Math.max(1, zoomSlider.value)) / zoomSliderMax;
// All dimensions are derived from scaleFactor, which is all we need to
// make the Zoom slider zoom.  We have to apply the bind keyword everywhere
// to ensure that changing the slider will change everything that depends on it.

// Pixel scales.  Planets are hugely magnified compared with their orbits
// and with the Sun.
def sunRadius = bind scaleFactor * 0.9;
def earthRadius = bind scaleFactor * 0.5;
def earthOrbitPixels = bind 10 * earthRadius;

def speedSliderMax = 300;
def speedSliderDefault = 100;
def speedSlider = SwingSlider {maximum: speedSliderMax, value: speedSliderDefault};
var speed = bind Math.max(0.00001, (speedSlider.value as Number) / speedSliderMax);

def sunColor = Color.YELLOW;
def planetDarkSideColor = Color.BLUE;
def planetLightSideColor = Color.LIGHTBLUE;
def orbitColor = Color.GREY;

def sunName = "Sun";

// If the eccentricity of the orbital ellipse is more than this, then we will
// make sure that the Sun is at the focus of the ellipse (and not its centre)
// and we will use Kepler's laws to ensure that the planet goes faster when
// it is near the Sun than when it is further away.
def eccentricityThreshold = 0.05;

class Planet {
    var name: String;
    var darkSideColor: Color = planetDarkSideColor;
    var brightSideColor: Color = planetLightSideColor;
    var orbit: Number;  // planet's distance from Sun at perihelion, Earth=1
    var day: Number;    // planet's day, Earth=1
    var year: Number;   // planet's year, Earth=0.997 (sidereal days)
    var radius: Number; // planet's radius, Earth=1
    var eccentricity: Number;
}

class PlanetarySystem extends CustomNode {
    var planets: Planet[];

    override function create(): Node {
        def maxOrbit = Sequences.max(for (planet in planets) planet.orbit) as Number;
        def maxOrbitPixels = earthOrbitPixels * maxOrbit;
        def padX = earthRadius * 1.5;
        def padY = earthRadius * 1.5;
        def sunCenterX = maxOrbitPixels + padX;
        def sunCenterY = maxOrbitPixels + padY;
        def height = 2 * sunCenterX;
        def width = 2 * sunCenterY;

        var nodes: Node[];

        // Spatter a few background stars around.
        for (i in [0..height*width/1000])
            insert star(Math.random() * width, Math.random() * height) into nodes;

        // Draw the Sun
        insert Circle {
            centerX: sunCenterX, centerY: sunCenterY
            radius: bind sunRadius
            fill: Color.YELLOW
            effect: Glow {level: 0.9}
       } into nodes;

       // Name the Sun
       insert Text {
	   content: sunName
	   fill: Color.BLACK
	   x: sunCenterX y: sunCenterY
	   font: bind Font {size: sunRadius / 3}
	   visible: bind sunRadius > 30
       } into nodes;

        for (planet in planets) {
            insert
                if (planet.eccentricity > eccentricityThreshold) {
                    EllipticalOrbitingPlanet {
                        sunCenterX: sunCenterX, sunCenterY: sunCenterY
                        planet: planet
                    }
                } else {
                    CircularOrbitingPlanet {
                        sunCenterX: sunCenterX, sunCenterY: sunCenterY
                        planet: planet
                    }
              } into nodes
        }
        return Group {content: nodes}
    }
}

function star(x: Number, y: Number): Node {
    return Rectangle {
        x: x, y: y, height: 1, width: 1, fill: Color.WHITE
    }
}

class CircularOrbitingPlanet extends CustomNode {
    var planet: Planet;
    var sunCenterX: Number;
    var sunCenterY: Number;

    def period = earthOrbitTime.mul(planet.year);
    var angle: Number;
    var timer: Timeline = Timeline {
        repeatCount: Timeline.INDEFINITE
        rate: bind speed
        keyFrames: [
            KeyFrame {time: 0s, values: [angle => 0]},
            KeyFrame {time: period, values: [angle => 360]}
        ]
    }
    // This animation will cause angle to go from 0 to 360 repeatedly,
    // and since the planet uses a Transform.rotate of that angle,
    // it will move around its orbit as the angle changes.
    // Because rate is bound to speed, which in turn is bound to the
    // Speed slider, the animation will go faster when you move the slider
    // to the right.

    override function create(): Node {
        timer.play();

        def orbit = bind planet.orbit * earthOrbitPixels;

        return Group {
            content: [
                // Draw orbit circle:
                Circle {
                    centerX: sunCenterX, centerY: sunCenterY
                    radius: bind orbit
                    strokeWidth: 2;
                    stroke: orbitColor;
                    fill: null
                },

                // Draw planet name, but not if the orbit is small:
                Text {
                    content: planet.name
                    fill: planet.brightSideColor
                    x: sunCenterX
                    y: bind sunCenterY - orbit * 0.85
                    font: bind Font {size: orbit / 10}
                    visible: bind (orbit > 60)
                },

                // Draw planet, with animation:
                Circle {
                    transforms: bind Transform.rotate(angle, sunCenterX, sunCenterY)
                    centerX: bind sunCenterX - orbit, centerY: sunCenterY
                    radius: bind planet.radius * earthRadius
                    fill: LinearGradient {
                        endX: 1.0, endY: 0.0
                        stops: [
                            Stop {offset: 0.0, color: planet.darkSideColor},
                            Stop {offset: 1.0, color: planet.brightSideColor}
                        ]
                    }
                    effect: Lighting {light: PointLight {x: sunCenterX, y: sunCenterY, z: 300}}
                }
            ]
        };
    }
}

class EllipticalOrbitingPlanet extends CustomNode {
    var planet: Planet;
    var sunCenterX: Number;
    var sunCenterY: Number;

    def period = earthOrbitTime.mul(planet.year);
    def nSubPeriods = 72;
    var angle: Number;
    var timer: Timeline;

    override function create(): Node {
        // The calculations are a lot less simple for an elliptical orbit
        // than for a circular one.  The code here applies Kepler's Laws
        // to compute the position of the planet at each of 72 equal slices
        // of its orbital period.  Then an animation moves the planet
        // from each of those slices to the next.
        def semiLatusRectum = planet.orbit * (1.0 + planet.eccentricity);
        def semiMajorAxis = planet.orbit / (1.0 - planet.eccentricity);
        def offsetFromCentre = semiMajorAxis - planet.orbit;
        def shiftPixels = bind offsetFromCentre * earthOrbitPixels;
        var frames: KeyFrame[];

        def eccFactor = Math.sqrt((1 + planet.eccentricity) / (1 - planet.eccentricity));
        for (t in [0..nSubPeriods-1]) {
            def tFraction = (t as Number) / nSubPeriods;
            def meanAnomaly = 2 * Math.PI * tFraction;
            def eccentricAnomaly = solveEccentricAnomaly(meanAnomaly);
            def theta = 2 * Math.atan(eccFactor * Math.tan(eccentricAnomaly / 2));
            def r = semiLatusRectum / (1 + planet.eccentricity * Math.cos(theta));
            // Now convert those polar coordinates into cartesian ones...
            def x = -offsetFromCentre - r * Math.cos(theta);
            def y = -r * Math.sin(theta);
            // ...and back into an angle relative to the centre of the ellipse
            def centreAngle = 180 + Math.toDegrees(Math.atan2(y, x));
            insert
                KeyFrame {time: period.mul(tFraction)
                          values: [angle => centreAngle]}
            into frames;
        }
        // We insert a keyframe for 360 degrees by hand, because the
        // algorithm would compute a value near 0 and we want to make sure that
        // the jump from 360 to 0 happens between the last keyframe and the
        // first keyframe of the next cycle, which follows immediately.  If
        // it happened between two keyframes that are not at the same time
        // then animation could interpolate any value between not-quite-360 and
        // not-quite-0, which would produce a glitch in the display.
        insert
            KeyFrame {time: period
                      values: [angle => 360]}
        into frames;
        timer = Timeline {
            repeatCount: Timeline.INDEFINITE, rate: bind speed, keyFrames: frames
        };
        timer.play();

        def orbit = bind planet.orbit * earthOrbitPixels;

        return Group {
            content: [
                // Draw orbit circle:
                Circle {
                    centerX: bind sunCenterX + shiftPixels, centerY: sunCenterY
                    radius: bind orbit
                    strokeWidth: 2;
                    stroke: orbitColor;
                    fill: null
                },

                Text {
                    content: planet.name
                    fill: planet.brightSideColor
                    x: sunCenterX + shiftPixels
                    y: bind sunCenterY - orbit * 0.85
                    font: bind Font {size: orbit / 10}
                    visible: bind (orbit > 60)
                },

                // Draw planet, with animation
                Circle {
                    transforms: bind Transform.rotate(angle, sunCenterX + shiftPixels, sunCenterY);
                    centerX: bind sunCenterX + shiftPixels - orbit, centerY: sunCenterY
                    radius: bind planet.radius * earthRadius
                    fill: LinearGradient {
                        endX: 1.0, endY: 0.0
                        stops: [
                            Stop {offset: 0.0, color: planet.darkSideColor},
                            Stop {offset: 1.0, color: planet.brightSideColor}
                        ]
                    }
                    effect: Lighting {light: PointLight {x: sunCenterX, y: sunCenterY, z: 300}}
                }
            ]
        };
    }

    // Solve for E in the equation M = E - e sin E, where M is meanAnomaly,
    // and e is planet.eccentricity.  The series here converges rapidly for
    // small e so 10 iterations is plenty.  See
    // http://en.wikipedia.org/wiki/Eccentric_anomaly
    function solveEccentricAnomaly(meanAnomaly: Number) {
        var e = meanAnomaly;
        for (i in [1..10])
            e = meanAnomaly + planet.eccentricity * Math.sin(e);
        return e;
    }
}

class NamedSlider extends CustomNode {
    var slider: SwingSlider;
    var name: String;
    var width: Integer;

    override public function create(): Node {
        return HBox {
            content: [
                Group {  // A Text object that is 80 pixels wide
                    content: [
                        Rectangle {
                            height: 1 width: 80 visible: false
                        },
                        Text {
                            content: name
                            font: Font {
                                size: 24
                            }
                            fill: Color.WHITE
                            // Shift the text down a bit to align better
                            // with the slider:
                            transforms: Transform.translate(0, 20)
                        }
                    ]
                },
                slider
            ]
        }
    }
}

// Wrap a Node so that it is translucent except when the mouse is inside it.
function translucentNode(n: Node): Node {
    var mouse: Boolean;
    return Group {
        opacity: bind if (mouse) 1 else 0.4
        content: [
            n,
            // Add an invisible rectangle that is a bit bigger than the
            // Node, so we don't go translucent when you move just outside,
            // or between the components of the Node.  This rectangle is how
            // we discover that the mouse has entered or exited.
            Rectangle {
                opacity: 0
                width:  bind n.boundsInParent.width  + 10
                height: bind n.boundsInParent.height + 10
                onMouseEntered: function(event) {mouse = true}
                onMouseExited:  function(event) {mouse = false}
            }
        ]
    }
}

def planets = [
    Planet{name: "Mercury" orbit: 0.3075 radius: 0.383 day: 58.65 year: 0.24
           brightSideColor: Color.LIGHTGREY darkSideColor: Color.DARKGREY
           eccentricity: 0.20563},
    Planet{name: "Venus"   orbit: 0.7184 radius: 0.95  day: 243   year: 0.615
           brightSideColor: Color.WHITE     darkSideColor: Color.BEIGE
           eccentricity: 0.0068},
    Planet{name: "Earth"   orbit: 0.9833 radius: 1     day: 0.997 year: 1
           brightSideColor: Color.LIGHTBLUE darkSideColor: Color.BLUE
           eccentricity: 0.01671},
    Planet{name: "Mars"    orbit: 1.3815 radius: 0.532 day: 1.025 year: 1.88
           brightSideColor: Color.ORANGE    darkSideColor: Color.DARKORANGE
           eccentricity: 0.093315},
    Planet{name: "Jupiter" orbit: 4.95   radius: 11.2  day: 0.41  year: 11.86
           brightSideColor: Color.LIGHTBLUE darkSideColor: Color.BLUE
           eccentricity: 0.048775},
    Planet{name: "Saturn"  orbit: 9.0481 radius: 9.45  day: 0.439 year: 29.66
           brightSideColor: Color.YELLOW    darkSideColor: Color.SLATEGREY
           eccentricity: 0.055723},
    Planet{name: "Uranus"  orbit: 18.376 radius: 4     day: 0.718 year: 84.323
           brightSideColor: Color.LIGHTCYAN darkSideColor: Color.CYAN
           eccentricity: 0.0444},
    Planet{name: "Neptune" orbit: 29.766 radius: 3.8   day: 0.671 year: 164.79
           brightSideColor: Color.BLUE      darkSideColor: Color.DARKBLUE
           eccentricity: 0.0112}
];

Stage {
    title: "SolarSystem";
    visible: true
    resizable: false
    height: 600 width: 600
    scene: Scene {
        content: [
            Group {
                content: [
                    PlanetarySystem {planets: planets},
                    translucentNode(VBox {
                        transforms: Transform.translate(10, 10);
                        content: [
                            NamedSlider {name: "Speed" slider: speedSlider},
                            NamedSlider {name: "Zoom" slider: zoomSlider}
                        ]
                    })
                ]
            }
        ]
        fill: Color.BLACK
    }
}