/***************************************************************************
* Copyright (C) 2007, Paul Lutus *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*
* SpaceApplet.java
*
* Created on February 12, 2007, 3:14 PM
*/
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;
public class SpaceApplet extends java.applet.Applet implements Runnable {
// m_fStandAlone will be true if applet is run in a frame
private boolean m_fStandAlone = false;
int defaultWidth = 640;
int defaultHeight = 480;
// a support function to launch an independent applet frame
public static void main(String args[])
{
SpaceApplet applet = new SpaceApplet();
GenericFrame frame = new GenericFrame(applet.appName);
frame.setSize(frame.getInsets().left + frame.getInsets().right + applet.defaultWidth,
frame.getInsets().top + frame.getInsets().bottom + applet.defaultHeight);
frame.add("Center", applet);
applet.m_fStandAlone = true;
applet.init();
applet.start();
frame.setVisible(true);
}
public String appName = "Space Applet 1.2";
public String copyright = "Copyright (C) 2007, Paul Lutus, released under the GPL";
private Thread m_space = null;
private int repaintMs = 5;
public int cometCount = 16;
public double darkEnergy;
public double timeStepHours;
public boolean darkEnergyMode = false;
private boolean running = true;
double toRad = Math.PI / 180;
private SpacePanel spacePanel;
public OrbitingData orbitData;
// initialize the applet
public void init() {
initComponents();
orbitData = new OrbitingData(this);
spacePanel = new SpacePanel(this);
add(spacePanel,java.awt.BorderLayout.CENTER);
if(m_fStandAlone) {
bodyControlPanel.remove(launchButton);
}
setup();
}
// Launch applet in frame from Web page
private void launchInFrame() {
if(!m_fStandAlone) {
runStopCheckbox.setSelected(false);
stop();
main(new String[2]);
}
}
// initial setup, also to reset the simulator on command
private void setup() {
stop();
orbitData.setup();
setTimeStep();
setCometCount();
setDarkEnergy();
startStop();
}
// some functions to read the user interface
private void setTimeStep() {
timeStepHours = 1;
try {
timeStepHours = Double.parseDouble(timeStepTextField.getText());
}
catch(Exception e) {
}
timeStepHours = (timeStepHours < 1)?1:timeStepHours;
}
private void setDarkEnergy() {
darkEnergy = 0;
darkEnergyMode = darkEnergyCheckBox.isSelected();
try {
darkEnergy = Double.parseDouble(darkEnergyTextField.getText());
}
catch(Exception e) {
}
}
private void setAnaglyphMode() {
spacePanel.anaglyphic = anaglyphCheckbox.isSelected();
}
private void setCometCount() {
cometCount = 16;
try {
cometCount = Integer.parseInt(cometsTextField.getText());
}
catch(Exception e) {
}
cometCount = (cometCount < 1)?1:cometCount;
orbitData.readComets(cometCount);
}
private void startStop() {
if(runStopCheckbox.isSelected()) {
start();
}
else {
stop();
}
}
// applet thread control
public void start()
{
if (m_space == null)
{
m_space = new Thread(this);
running = true;
m_space.start();
}
}
public void stop()
{
if (m_space != null)
{
running = false;
while (m_space.isAlive());
m_space = null;
}
}
public void run() {
while (running)
{
try
{
if(isVisible()) {
spacePanel.repaint();
getToolkit().sync();
Thread.sleep(repaintMs);
}
else {
Thread.sleep(200);
}
}
catch (InterruptedException e)
{
stop();
}
}
}
/** This method is called from within the init() method to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
private void initComponents() {//GEN-BEGIN:initComponents
controlPanel = new javax.swing.JPanel();
defaultPanel = new javax.swing.JPanel();
jLabel1 = new javax.swing.JLabel();
timeStepTextField = new javax.swing.JTextField();
runStopCheckbox = new javax.swing.JCheckBox();
anaglyphCheckbox = new javax.swing.JCheckBox();
resetButton = new javax.swing.JButton();
bodyControlPanel = new javax.swing.JPanel();
planetCheckBox = new javax.swing.JCheckBox();
cometCheckBox = new javax.swing.JCheckBox();
cometsTextField = new javax.swing.JTextField();
darkEnergyCheckBox = new javax.swing.JCheckBox();
darkEnergyTextField = new javax.swing.JTextField();
launchButton = new javax.swing.JButton();
setLayout(new java.awt.BorderLayout());
controlPanel.setLayout(new java.awt.BorderLayout());
controlPanel.setBackground(new java.awt.Color(255, 255, 204));
controlPanel.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(0, 0, 0)));
defaultPanel.setBackground(new java.awt.Color(255, 255, 204));
jLabel1.setText("Time Step (hours)");
defaultPanel.add(jLabel1);
timeStepTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
timeStepTextField.setText("64");
timeStepTextField.setMinimumSize(new java.awt.Dimension(30, 19));
timeStepTextField.setPreferredSize(new java.awt.Dimension(60, 19));
timeStepTextField.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyReleased(java.awt.event.KeyEvent evt) {
timeStepTextFieldKeyReleased(evt);
}
});
defaultPanel.add(timeStepTextField);
runStopCheckbox.setBackground(new java.awt.Color(255, 255, 204));
runStopCheckbox.setSelected(true);
runStopCheckbox.setText("Run/Stop");
runStopCheckbox.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
runStopCheckboxMouseClicked(evt);
}
});
defaultPanel.add(runStopCheckbox);
anaglyphCheckbox.setBackground(new java.awt.Color(255, 255, 204));
anaglyphCheckbox.setText("Anaglyphic");
anaglyphCheckbox.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
anaglyphCheckboxMouseClicked(evt);
}
});
defaultPanel.add(anaglyphCheckbox);
resetButton.setBackground(new java.awt.Color(255, 255, 204));
resetButton.setText("Reset");
resetButton.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
resetButtonMouseClicked(evt);
}
});
defaultPanel.add(resetButton);
controlPanel.add(defaultPanel, java.awt.BorderLayout.CENTER);
bodyControlPanel.setBackground(new java.awt.Color(255, 255, 204));
planetCheckBox.setBackground(new java.awt.Color(255, 255, 204));
planetCheckBox.setSelected(true);
planetCheckBox.setText("Planets");
bodyControlPanel.add(planetCheckBox);
cometCheckBox.setBackground(new java.awt.Color(255, 255, 204));
cometCheckBox.setSelected(true);
cometCheckBox.setText("Comets");
bodyControlPanel.add(cometCheckBox);
cometsTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
cometsTextField.setText("32");
cometsTextField.setPreferredSize(new java.awt.Dimension(60, 19));
cometsTextField.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyReleased(java.awt.event.KeyEvent evt) {
cometsTextFieldKeyReleased(evt);
}
});
bodyControlPanel.add(cometsTextField);
darkEnergyCheckBox.setBackground(new java.awt.Color(255, 255, 204));
darkEnergyCheckBox.setText("Dark Energy");
darkEnergyCheckBox.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
darkEnergyCheckBoxMouseClicked(evt);
}
});
bodyControlPanel.add(darkEnergyCheckBox);
darkEnergyTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
darkEnergyTextField.setText(".001");
darkEnergyTextField.setPreferredSize(new java.awt.Dimension(60, 19));
darkEnergyTextField.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyReleased(java.awt.event.KeyEvent evt) {
darkEnergyTextFieldKeyReleased(evt);
}
});
bodyControlPanel.add(darkEnergyTextField);
launchButton.setBackground(new java.awt.Color(255, 255, 204));
launchButton.setText("Separate");
launchButton.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
launchButtonMouseClicked(evt);
}
});
bodyControlPanel.add(launchButton);
controlPanel.add(bodyControlPanel, java.awt.BorderLayout.SOUTH);
add(controlPanel, java.awt.BorderLayout.SOUTH);
}//GEN-END:initComponents
private void launchButtonMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_launchButtonMouseClicked
launchInFrame();
}//GEN-LAST:event_launchButtonMouseClicked
private void timeStepTextFieldKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_timeStepTextFieldKeyReleased
// TODO add your handling code here:
setTimeStep();
}//GEN-LAST:event_timeStepTextFieldKeyReleased
private void cometsTextFieldKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_cometsTextFieldKeyReleased
// TODO add your handling code here:
setCometCount();
}//GEN-LAST:event_cometsTextFieldKeyReleased
private void darkEnergyTextFieldKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_darkEnergyTextFieldKeyReleased
// TODO add your handling code here:
setDarkEnergy();
}//GEN-LAST:event_darkEnergyTextFieldKeyReleased
private void darkEnergyCheckBoxMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_darkEnergyCheckBoxMouseClicked
setDarkEnergy();
}//GEN-LAST:event_darkEnergyCheckBoxMouseClicked
private void resetButtonMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_resetButtonMouseClicked
setup();
}//GEN-LAST:event_resetButtonMouseClicked
private void anaglyphCheckboxMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_anaglyphCheckboxMouseClicked
setAnaglyphMode();
}//GEN-LAST:event_anaglyphCheckboxMouseClicked
private void runStopCheckboxMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_runStopCheckboxMouseClicked
startStop();
}//GEN-LAST:event_runStopCheckboxMouseClicked
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JCheckBox anaglyphCheckbox;
private javax.swing.JPanel bodyControlPanel;
public javax.swing.JCheckBox cometCheckBox;
private javax.swing.JTextField cometsTextField;
private javax.swing.JPanel controlPanel;
private javax.swing.JCheckBox darkEnergyCheckBox;
private javax.swing.JTextField darkEnergyTextField;
private javax.swing.JPanel defaultPanel;
private javax.swing.JLabel jLabel1;
private javax.swing.JButton launchButton;
public javax.swing.JCheckBox planetCheckBox;
private javax.swing.JButton resetButton;
private javax.swing.JCheckBox runStopCheckbox;
private javax.swing.JTextField timeStepTextField;
// End of variables declaration//GEN-END:variables
}
// the animation panel class
class SpacePanel extends java.awt.Panel {
public boolean anaglyphic = false;
private PhysicsEngine engine = null;
private boolean initialized = false;
double rotation;
double anaglyphDepth = 0.1;
double drawingScale = 1e-12;
double sinVal,cosVal,oldAngle = 1e30;
SpaceApplet parent;
boolean busy = false;
Stack undrawData;
Image image = null;
Dimension old_size = null;
SpacePanel(SpaceApplet p) {
parent = p;
rotation = 20 * parent.toRad;
engine = new PhysicsEngine(parent);
setBackground(Color.black);
undrawData = new Stack();
initialized = true;
}
public void testRepaint() {
if(!busy) {
busy = true;
repaint();
getToolkit().sync();
busy = false;
}
else {
System.out.println("skipped frame");
}
}
public void paint(Graphics g) {
update(g);
}
public void update(Graphics g) {
double time_step_seconds = parent.timeStepHours * 3600; // convert to seconds
Dimension size = getSize();
// create background drawing buffer
if(image == null || old_size == null || !old_size.equals(size)) {
image = createImage(size.width,size.height);
Graphics bg = image.getGraphics();
bg.setColor(Color.black);
bg.fillRect(0,0,size.width,size.height);
old_size = size;
}
Graphics bg = image.getGraphics();
drawObjects(time_step_seconds,bg,size);
// transfer finished image to display
g.drawImage(image,0,0,this);
}
private void drawObjects(double time_step,Graphics g,Dimension size) {
double cx = size.width / 2;
double cy = size.height / 2;
int ovalSize = (int) (cx / 96);
ovalSize = (ovalSize < 4)?4:ovalSize;
undrawAll(g,ovalSize);
// draw "sun" oval (always)
g.setColor(Color.white);
drawOval(g,(int)cx,(int)cy,ovalSize * 2);
if(anaglyphic) {
g.setXORMode(Color.white);
}
else {
g.setPaintMode();
}
if(parent.planetCheckBox.isSelected()) {
// skip sun redraw here
drawSubset(1,time_step,cx,cy,ovalSize,g,size,parent.orbitData.planet_array);
}
if(parent.cometCheckBox.isSelected()) {
drawSubset(0,time_step,cx,cy,ovalSize,g,size,parent.orbitData.comet_array);
}
}
private void drawSubset(int first,double time_step,double cx, double cy,int ovalSize,Graphics g,Dimension size,OrbitingBody[] array) {
engine.processObjects(array,time_step);
for(int i = first;i < array.length;i++) {
OrbitingBody p = array[i];
Cart3 pp = scaleOrbitingBody(p,cx,cy,rotation);
// detect anaglyphic mode
if(!anaglyphic) {
g.setColor(p.color);
drawOval(g,(int)pp.x,(int)pp.y,ovalSize);
}
else {
// create two perspective views in different colors
int ax = (int) (pp.x + pp.z * anaglyphDepth);
int bx = (int) (pp.x - pp.z * anaglyphDepth);
g.setColor(Color.cyan);
drawOval(g,(int)ax,(int)pp.y,ovalSize);
g.setColor(Color.red);
drawOval(g,(int)bx,(int)pp.y,ovalSize);
}
}
}
private void drawOval(Graphics g, int x, int y,int ovalSize) {
Point p = new Point(x,y);
undrawData.push(p);
g.fillOval(x,y,ovalSize,ovalSize);
}
// rather than slowly erase the drawing buffer
// redraw the drawn objects in black (faster)
private void undrawAll(Graphics g,int ovalSize) {
g.setPaintMode();
g.setColor(Color.black);
while(!undrawData.empty()) {
Point p = (Point) undrawData.pop();
g.fillOval(p.x,p.y,ovalSize,ovalSize);
}
}
// scale and rotate coordinates
private Cart3 scaleOrbitingBody(OrbitingBody p,double cx,double cy,double a) {
Cart3 cp = new Cart3();
cp.x = (p.pos.x * drawingScale * cx) + cx;
if(a != oldAngle) {
sinVal = Math.sin(a);
cosVal = Math.cos(a);
oldAngle = a;
}
double py = p.pos.z * sinVal + p.pos.y * cosVal;
double pz = p.pos.z * cosVal + p.pos.y * sinVal;
cp.y = (py * drawingScale * cy) + cy;
cp.z = (pz * drawingScale * cy);
return cp;
}
public void toggleAnaglyphic() {
anaglyphic = !anaglyphic;
testRepaint();
}
};
// the physics engine class is responsible for
// computing gravitation as well as
// dark energy accelerations
class PhysicsEngine {
double G = 6.6742e-11; // universal gravitational constant
OrbitingBody sun;
SpaceApplet parent;
PhysicsEngine(SpaceApplet p) {
parent = p;
this.sun = parent.orbitData.planet_array[0];
}
void computeAcceleration(OrbitingBody pa,OrbitingBody pb,double dt, double darkEnergy)
{
// don't compute self-gravitation
if(pa != pb)
{
Cart3 radius = pa.pos.sub(pb.pos);
double sumsq = radius.sumsq();
double hypot = Math.sqrt(sumsq);
double acc = (darkEnergy - (G * pb.mass / sumsq)) * dt;
// next line assigns the computed acceleration to
// the x,y,z velocity components along the
// radius pa - pb without using trig functions
pa.vel.addTo(radius.div(hypot).mult(acc));
}
}
// perform all gravitational computations
public void processObjects(OrbitingBody[] array,double dt)
{
double darkEnergy = (parent.darkEnergyMode)?parent.darkEnergy:0.0;
// compute gravitation only wrt the sun, not wrt all other bodies
for(int i = 0;i < array.length;i++)
{
computeAcceleration(array[i],sun,dt,darkEnergy);
array[i].pos.addTo(array[i].vel.mult(dt));
}
}
};
// an orbiting body data class
class OrbitingBody {
String name;
double radius;
Cart3 pos;
Cart3 vel;
double mass;
Color color;
OrbitingBody(String name, double radius, Cart3 pos, Cart3 vel, double mass, Color color) {
this.name = name;
this.radius = radius;
this.pos = pos;
this.vel = vel;
this.mass = mass;
this.color = color;
}
};
// a source for orbiting objects
// including the entire solar system
// plus a random "comet" generator
class OrbitingData {
SpaceApplet parent;
public OrbitingBody[] planet_array = null;
public OrbitingBody[] comet_array = null;
public Color planet_colors[] =
{
Color.white,Color.yellow,Color.cyan,new Color(128,128,255),
Color.red,Color.green,Color.magenta,Color.blue
};
String data =
"Name,OrbitRad,BodyRad,Mass,OrbitVel\n"
+ "Sun,0,695000000,1.989E+030,0\n"
+ "Mercury,57900000000,2440000,3.33E+023,47900\n"
+ "Venus,108000000000,6050000,4.869E+024,35000\n"
+ "Earth,150000000000,6378140,5.976E+024,29800\n"
+ "Mars,227940000000,3397200,6.421E+023,24100\n"
+ "Jupiter,778330000000,71492000,1.9E+027,13100\n"
+ "Saturn,1429400000000,60268000,5.688E+026,9640\n"
+ "Uranus,2870990000000,25559000,8.686E+025,6810\n"
+ "Neptune,4504300000000,24746000,1.024E+026,5430\n"
// I guess Pluto isn't really a planet any more
+ "Pluto,5913520000000,1137000,1.27E+022,4740\n";
OrbitingData(SpaceApplet p) {
parent = p;
setup();
}
public void setup() {
readOrbitingBodies();
readComets(parent.cometCount);
}
private void readOrbitingBodies() {
Vector list = new Vector();
String planetStrings[] = data.split("\n");
double vals[] = new double[4];