/***************************************************************************
* Copyright (C) 2013, 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.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.5";
public String copyright = "Copyright (C) 2013, 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
@Override
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
@Override
public void start() {
if (m_space == null) {
m_space = new Thread(this);
running = true;
m_space.start();
}
}
@Override
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.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void 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.setBackground(new java.awt.Color(255, 255, 204));
controlPanel.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));
controlPanel.setLayout(new java.awt.BorderLayout());
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("1e-9");
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);
}// </editor-fold>//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;
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();
}
public void testRepaint() {
if (!busy) {
busy = true;
repaint();
getToolkit().sync();
busy = false;
} else {
System.out.println("skipped frame");
}
}
@Override
public void paint(Graphics g) {
update(g);
}
@Override
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 UpdatePosition(OrbitingBody pa, OrbitingBody pb, double dt, double darkEnergy) {
// don't compute self-gravitation
if (pa != pb) {
// this trig-free method does this:
// 1. vel += radius * -G * mass * dt * radius.abs()^-3
// 2. pos += vel * dt
Cart3 radius = pa.pos.sub(pb.pos);
pa.vel.addTo(radius.mult(dt * (darkEnergy - G * pb.mass * radius.invSumCube())));
pa.pos.addTo(pa.vel.mult(dt));
}
}
// 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 (OrbitingBody ob : array) {
UpdatePosition(ob, sun, dt, darkEnergy);
}
}
};
// 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() {
ArrayList list = new ArrayList();
String planetStrings[] = data.split("\n");
double vals[] = new double[4];
for (int i = 1; i < planetStrings.length; i++) {
String fields[] = planetStrings[i].split(",");
for (int j = 1; j < fields.length; j++) {
vals[j - 1] = Double.parseDouble(fields[j]);
}
Cart3 pos = new Cart3(-vals[0], 0, 0);
Cart3 vel = new Cart3(0, 0, vals[3]);
Color c = planet_colors[(i - 1) % planet_colors.length];
OrbitingBody planet = new OrbitingBody(fields[0], vals[1], pos, vel, vals[2], c);
list.add(planet);
}
planet_array = (OrbitingBody[]) list.toArray(new OrbitingBody[list.size()]);
}
public void readComets(int count) {
ArrayList list = new ArrayList();
Random r = new Random();
//r.setSeed(0);
for (int i = 0; i < count; i++) {
String name = "comet" + i;
double ca = r.nextDouble() * 360; // angle in x-z plane
double cr = (r.nextDouble() * 100000) + 100000; // distance from sun
cr *= 4e6;
Cart3 pos = new Cart3(cr * Math.sin(ca * parent.toRad), 0, cr * Math.cos(ca * parent.toRad));
// comet initial velocity
double v = ((r.nextDouble() * 200) + 100) * 50.0;
v = (i % 2 == 1) ? -v : v;
Cart3 vel = new Cart3(0, v, 0);
Color c = planet_colors[i % planet_colors.length];
OrbitingBody comet = new OrbitingBody(name, 1e3, pos, vel, 1e9, c);
list.add(comet);
}
comet_array = (OrbitingBody[]) list.toArray(new OrbitingBody[list.size()]);
}
};
// a convenience class for handling 3D Cartesian vectors
class Cart3 {
double x = 0, y = 0, z = 0;
Cart3() {
}
Cart3(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
Cart3(Cart3 o) {
x = o.x;
y = o.y;
z = o.z;
}
public Cart3 sub(Cart3 a) {
return new Cart3(x - a.x, y - a.y, z - a.z);
}
public Cart3 mult(double m) {
return new Cart3(x * m, y * m, z * m);
}
public Cart3 addTo(Cart3 a) {
x += a.x;
y += a.y;
z += a.z;
return this;
}
public double invSumCube() {
return Math.pow(x*x+y*y+z*z,-1.5);
}
public double abs() {
return Math.sqrt(x * x + y * y + z * z);
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
};
// a generic frame class for stand-alone operation
class GenericFrame extends Frame {
public GenericFrame(String str) {
super(str);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
}
}