#!/usr/local/bin/perl
# The Solar System, simulated on the web.
require "cgi-lib.pl";
require "flush.pl";
use GD;
$objectsFile = "/CHANGE/THIS/FILE/objects.dat";
$programUrl = "/CHANGE/THIS/URL/nph-sss.cgi";
$programDataPath = "/CHANGE/THIS/PATH/sss/";
$windowSize = 200;
$objectsTotal = 0;
$magnification = 1.0;
sub SCALEX {
local($x) = @_;
return ((($x - $minX)/($maxX - $minX)) * $windowSize);
}
sub UNSCALEX {
local($x) = @_;
return (($x * ($maxX - $minX) / $windowSize) + $minX);
}
sub SCALEY {
local($y) = @_;
return ((($y - $minY)/($maxY - $minY)) * $windowSize);
}
sub UNSCALEY {
local($y) = @_;
return (($y * ($maxY - $minY) / $windowSize) + $minY);
}
$PI = 3.141592653;
$centerFloating = 0;
$timeStep = 0.5;
$pathInfo = $ENV{'PATH_INFO'};
&ParseSessionId();
if ($pathInfo eq "/canvas.gif") {
&Canvas;
} elsif ($pathInfo eq "/form") {
&Form;
} elsif ($pathInfo eq "/zoomin") {
&ZoomIn;
} elsif ($pathInfo eq "/zoomout") {
&ZoomOut;
} elsif ($pathInfo eq "/timeplus") {
&TimePlus;
} elsif ($pathInfo eq "/timeminus") {
&TimeMinus;
} elsif ($pathInfo eq "/sizeplus") {
&SizePlus;
} elsif ($pathInfo eq "/sizeminus") {
&SizeMinus;
} elsif ($pathInfo eq "/uncenter") {
&UnCenter;
} elsif ($pathInfo eq "/nemesis") {
&Nemesis;
} elsif ($pathInfo eq "/select") {
&Select;
} elsif ($pathInfo eq "/resume") {
&Resume;
} else {
&Page(1);
}
sub Page {
local($firstTime) = @_;
local($i);
if ($firstTime) {
&SelectSessionId;
&ObjectsSetup;
&WriteSessionState;
}
print "HTTP/1.0 200 OK\n";
print "Pragma: no-cache\n";
print "Content-type: text/html\n\n";
print "
Solar System Simulator\n";
print "Solar System Simulator
\n";
print "Click on any planet to center the display\n";
print "on that object, or select one of the options below.\n";
print "Notes: zoom in to watch the inner solar system,\n";
print "or increase the time step many times to make the outer planets\n";
print "more interesting to watch.\n";
print "Large time steps will destabilize the moon.\n";
print "Even larger time steps may confuse the planets as well.
\n";
print "The stop button can be used to pause the simulation. Select\n";
print "Resume to resume the simulation after a stop.
";
# Imagemap link
print "
\n";
# Image
print "
\n";
print "
\n";
# Status display
if ($centerFloating) {
print "Centered on: ",
$objects[$centerFloatingObject]{'name'}, " ";
}
# Use printf for consistent numeric presentation
printf "Magnification: %f", $magnification;
printf " Time Step: %f days
", $timeStep;
# Various controls
print "Zoom In x2 ";
print "Zoom Out /2 ";
print "Time Step x2 ";
print "Time Step /2 ";
# This code works, but Netscape is very difficult to use
# if the window is any larger than 200x200, at least on
# my system. Feel free to remove the comment marks
# and see what happens.
# if ($windowSize < 1600) {
# print "Window Size x2 ";
# }
# if ($windowSize > 25) {
# print "Window Size /2 ";
# }
print "NEMESIS ";
print "Restart ";
print "Resume ";
if ($centerFloating) {
print "Uncenter";
}
print "
\n";
# A form, for skilled users
print "
Use this form if you wish to make several changes quickly.\n";
print "
\n";
print "\n";
return 0;
}
sub Canvas {
local($i, $j, $x, $y, $done, $labels, $trails,
$im, $black, $white, $blue);
print "HTTP/1.0 200 Document follows\r\n";
print "Content-type: multipart/x-mixed-replace;boundary=goober\n\n";
&flush(STDOUT);
while (!$done) {
# Re-read the session state, do the computations
# as quickly as possible, and write the state
# again.
&ReadSessionState;
for ($i=0; ($i < $objectsTotal); $i++) {
local($x, $y);
for ($j=0; ($j < $objectsTotal); $j++) {
if ($i != $j) {
local($dist, $pull);
$dist = &hypot(
$objects[$i]{'x'} -
$objects[$j]{'x'},
$objects[$i]{'y'} -
$objects[$j]{'y'});
if ($dist != 0.0) {
$pull=($gConstant *
$objects[$j]{'mass'} /
($dist * $dist));
$objects[$i]{'vx'} +=
(($objects[$j]{'x'} -
$objects[$i]{'x'}) /
$dist) * $pull *
$timeStep;
$objects[$i]{'vy'} +=
(($objects[$j]{'y'} -
$objects[$i]{'y'}) /
$dist) * $pull *
$timeStep;
}
}
}
}
for ($i=0; ($i < $objectsTotal); $i++) {
$objects[$i]{'x'} += ($objects[$i]{'vx'} * $timeStep);
$objects[$i]{'y'} += ($objects[$i]{'vy'} * $timeStep);
}
&WriteSessionState;
&AccountForCenter;
# Now build a GIF
$im = new GD::Image($windowSize, $windowSize);
$black = $im->colorAllocate(0, 0, 0);
$white = $im->colorAllocate(255, 255, 255);
$blue = $im->colorAllocate(192, 192, 255);
for ($i=0; ($i < $objectsTotal); $i++) {
local($val, $x, $y);
$val = &log10($objects[$i]{'radius'});
$x = &SCALEX($objects[$i]{'x'});
$y = SCALEY($objects[$i]{'y'});
$im->arc($x, $y, $val, $val, 0, 360, $white);
$im->string(gdTinyFont,
$x - (gdTinyFont->width *
length($objects[$i]{'name'}) / 2),
$y + $val,
$objects[$i]{'name'}, $blue);
}
sleep(1);
print "\n--goober\n";
print "Content-type: image/gif\n\n";
&flush(STDOUT);
print $im->gif;
&flush(STDOUT);
}
print "\n--goober--\n";
return 0;
}
sub Select {
local($x, $y);
&ReadSessionState;
$_ = $ENV{'QUERY_STRING'};
if (/(\d+),(\d+)/) {
$x = $1;
$y = $2;
&CenterClosest($x, $y);
&WriteSessionState;
return &Page(0);
} else {
print "text/html\n\n";
print "HTTP/1.0 200 Document follows\n";
print "Bad Click: ", $cgiQueryString, "
\n";
return 0;
}
}
sub UnCenter {
&ReadSessionState;
$centerFloating = 0;
&WriteSessionState;
return &Page(0);
}
sub ZoomIn {
&ReadSessionState;
$magnification *= 2.0;
&WriteSessionState;
return &Page(0);
}
sub ZoomOut {
&ReadSessionState;
$magnification /= 2.0;
&WriteSessionState;
return &Page(0);
}
sub TimePlus {
&ReadSessionState;
$timeStep *= 2.0;
&WriteSessionState;
return &Page(0);
}
sub TimeMinus {
&ReadSessionState;
$timeStep /= 2.0;
&WriteSessionState;
return &Page(0);
}
sub SizePlus {
&ReadSessionState;
if ($windowSize < 200) {
$windowSize *= 2.0;
}
&WriteSessionState;
return &Page(0);
}
sub SizeMinus {
&ReadSessionState;
if ($windowSize) {
$windowSize /= 2.0;
}
&WriteSessionState;
return &Page(0);
}
sub Resume {
&ReadSessionState;
return &Page(0);
}
sub Nemesis {
local($largestMass, $largestRadius, $i, $t);
$largestMass = 1.0;
$largestRadius = 1.0;
&ReadSessionState;
# Wreak havoc: introduce an object as
# large as the largest object in the
# system and send it whizzing through.
for ($i=0; ($i < $objectsTotal); $i++) {
if ((!$i) || ($objects[i]{'mass'} > $largestMass)) {
$largestMass = $objects[i]{'mass'};
}
if ((!$i) || ($objects[i]{'radius'} > $largestRadius)) {
$largestRadius = $objects[i]{'radius'};
}
}
$t = $objectsTotal;
# Start in the upper left corner of the simulation
$objects[$t]{'x'} = $minX;
$objects[$t]{'y'} = $minY;
# Fast enough to cross in 365 days
$objects[$t]{'vx'} = ($maxX - $minX) / 365.0;
$objects[$t]{'vy'} = ($maxY - $minY) / 365.0;
# Very big, very nasty
$objects[$t]{'mass'} = $largestMass * 2;
$objects[$t]{'radius'} = $largestRadius * 2;
$objects[$t]{'name'} = "NEMESIS";
$objectsTotal++;
&WriteSessionState;
return &Page(0);
}
sub SelectSessionId {
# Come up with a new, never-before-used session id.
$s = $programDataPath . "/id";
open(IN, $s);
$sessionId = ;
close(IN);
# Clean it up by casting it
$sessionId = int($sessionId);
open(OUT, ">" . $s);
print OUT $sessionId + 1, "\n";
close(OUT);
}
sub ParseSessionId {
$_ = $pathInfo;
if (/\/ID:(\d+)\/(.*)/) {
$sessionId = $1;
$pathInfo = "/" . $2;
}
}
sub CenterClosest {
local($x, $y) = @_;
local($smallestDist, $i, $centerX, $centerY);
$centerFloating = 1;
&AccountForCenter;
$centerX = &UNSCALEX($x);
$centerY = &UNSCALEY($y);
for ($i = 0; ($i < $objectsTotal); $i++) {
local($dist);
$dist = &hypot($objects[i]{'x'} -
$centerX, $objects[i]{'y'} - $centerY);
if ($dist < $smallestDist || (!$i)) {
$smallestDist = $dist;
$centerFloatingObject = $i;
}
}
}
sub AccountForCenter {
if ($centerFloating) {
$minX = $objects[$centerFloatingObject]{'x'} -
$width / 2.0 / $magnification;
$maxX = $objects[$centerFloatingObject]{'x'} +
$width / 2.0 / $magnification;
$minY = $objects[$centerFloatingObject]{'y'} -
$width / 2.0 / $magnification;
$maxY = $objects[$centerFloatingObject]{'y'} +
$width / 2.0 / $magnification;
} else {
local($cX, $cY);
$cX = ($minX + $maxX) / 2.0;
$cY = ($minY + $maxY) / 2.0;
$minX = $cX - $width / 2.0 / $magnification;
$maxX = $cX + $width / 2.0 / $magnification;
$minY = $cY - $width / 2.0 / $magnification;
$maxY = $cY + $width / 2.0 / $magnification;
}
}
sub Form {
local($center, $mag, $t);
&ReadSessionState;
&ReadParse(*input);
$mag = $input{'magnification'};
# Don't forbid negative numbers; they can actually be a lot of fun here
if ($mag != 0) {
$magnification = $mag;
}
$t = $input{'timestep'};
if ($t != 0) {
$timeStep = $t;
}
# Find out which object was centered on, if any.
$center = $input{'center'};
if ($center == -1) {
$centerFloating = 0;
} elsif (($center >= 0) && ($center < $objectsTotal)) {
$centerFloating = 1;
$centerFloatingObject = $center;
}
&WriteSessionState;
return &Page(0);
}
sub hypot {
local($s1, $s2) = @_;
return sqrt(($s1 * $s1) + ($s2 * $s2));
}
sub log10 {
local($n) = @_;
return log($n) / log(10);
}
sub ObjectsSetup {
local($f, $i);
$objectsTotal = 0;
open(IN, $objectsFile) || exit 1;
do {
$line = ;
$f = substr($line, 0, 1);
} while (($f eq "#") || ($f eq "\n"));
$gConstant = $line;
while($line = ) {
$f = substr($line, 0, 1);
if (($f eq "#") || ($f eq "\n")) {
next;
}
$i = $objectsTotal;
($objects[$i]{'name'},
$objects[$i]{'x'},
$objects[$i]{'y'},
$objects[$i]{'vx'},
$objects[$i]{'vy'},
$objects[$i]{'mass'},
$objects[$i]{'radius'}) = split(/\s+/, $line);
$objectsTotal++;
}
close(IN);
}
sub WriteSessionState {
local($sold, $snew, $i);
# Write to a different filename initially, then
# delete the old and rename the new at the
# end. This reduces the probability that an
# untimely kill signal will cause problems.
$sold = $programDataPath . "/" . $sessionId . ".sav";
$snew = $programDataPath . "/" . $sessionId . ".dtn";
open(OUT, ">" . $snew) || return;
# Because of the need for careful control of the
# output format, particularly exponential notation,
# printf is a good choice to use here.
printf OUT "%d\n", $objectsTotal;
printf OUT "%e\n", $gConstant;
printf OUT "%d\n", $windowSize;
printf OUT "%d\n", $centerFloating;
printf OUT "%d\n", $centerFloatingObject;
printf OUT "%e\n", $timeStep;
printf OUT "%e\n", $magnification;
for ($i=0; ($i < $objectsTotal); $i++) {
printf OUT "%e %e %e %e %e %e %s\n",
$objects[$i]{'x'},
$objects[$i]{'y'},
$objects[$i]{'vx'},
$objects[$i]{'vy'},
$objects[$i]{'mass'},
$objects[$i]{'radius'},
$objects[$i]{'name'};
}
close(OUT);
# OK, swap the files quickly.
unlink($sold);
rename($snew, $sold);
}
sub ReadSessionState {
local($i);
$maxX = 0;
$minX = 0;
$maxY = 0;
$minY = 0;
$s = $programDataPath . "/" . $sessionId . ".sav";
open(IN, $s) || return;
$objectsTotal = ;
$gConstant = ;
$windowSize = ;
$centerFloating = ;
$centerFloatingObject = ;
$timeStep = ;
$magnification = ;
for ($i = 0; ($i < $objectsTotal); $i++) {
$line = ;
($objects[$i]{'x'},
$objects[$i]{'y'},
$objects[$i]{'vx'},
$objects[$i]{'vy'},
$objects[$i]{'mass'},
$objects[$i]{'radius'},
$objects[$i]{'name'}) = split(/\s+/, $line);
if ($objects[$i]{'x'} < $minX || (!$objectsTotal)) {
$minX = $objects[$i]{'x'};
}
if ($objects[$i]{'x'} > $maxX || (!$objectsTotal)) {
$maxX = $objects[$i]{'x'};
}
if ($objects[$i]{'y'} < $minY || (!$objectsTotal)) {
$minY = $objects[$i]{'y'};
}
if ($objects[$i]{'y'} > $maxY || (!$objectsTotal)) {
$maxY = $objects[$i]{'y'};
}
}
close(IN);
if ((- $maxX) < $minX) {
$minX = - $maxX;
} else {
$maxX = - $minX;
}
if ((- $maxY) < $minY) {
$minY = - $maxY;
} else {
$maxY = - $minY;
}
if ($minX < $minY) {
$minY = $minX;
} else {
$minX = $minY;
}
if ($maxX > $maxY) {
$maxY = $maxX;
} else {
$maxX = $maxY;
}
$width = ($maxX - $minX);
}