#!/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 ""; print "
\n"; print " Magnification
\n"; print " Time Step (days)
\n"; print "Center Display On:
"; print "\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); }