Skip to content

Instantly share code, notes, and snippets.

@zed

zed/input.txt Secret

Created November 25, 2012 17:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zed/a011754fa77737e099b7 to your computer and use it in GitHub Desktop.
Save zed/a011754fa77737e099b7 to your computer and use it in GitHub Desktop.
solar system animation using quickdraw in Python
RootObject: Sun
Object: Sun
Satellites: Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Ceres,Pluto,Haumea,Makemake,Eris
Radius: 20890260
Orbital Radius: 0
Object: Miranda
Orbital Radius: 5822550
Radius: 23500
Period: 1.413
Object: Ariel
Orbital Radius: 8595000
Radius: 60000
Period: 2.520379
Object: Umbriel
Orbital Radius: 11983500
Radius: 60000
Period: 4.144177
Object: Titania
Orbital Radius: 19575000
Radius: 75000
Period: 8.7058
Object: Oberon
Orbital Radius: 26235000
Radius: 75000
Period: 13.463
Object: Uranus
Orbital Radius: 453572956
Radius: 2555900
Period: 30799
Satellites: Puck,Miranda,Ariel,Umbriel,Titania,Oberon
Object: Neptune
Orbital Radius: 550000000
Radius: 2476400
Period: 60190
Satellites: Triton
Object: Triton
Orbital Radius: 40000000
Radius: 135300
Period: -5.8
Object: Mercury
Orbital Radius: 38001200
Period: 87.9691
Radius: 243900.7
Object: Venus
Orbital Radius: 57477000
Period: 224.698
Radius: 605100.8
Object: Earth
Orbital Radius: 77098290
Period: 365.256363004
Radius: 637100.0
Satellites: Moon
Object: Moon
Orbital Radius: 18128500
Radius: 173700.10
Period: 27.321582
Object: Mars
Orbital Radius: 106669000
Period: 686.971
Radius: 339600.2
Satellites: Phobos,Deimos
Object: Phobos
Orbital Radius: 3623500.6
Radius: 200000
Period: 0.31891023
Object: Deimos
Orbital Radius: 8346000
Period: 1.26244
Radius: 200000.2
Object: Jupiter
Orbital Radius: 210573600
Period: 4332.59
Radius: 7149200
Satellites: Io,Europa,Ganymede,Callisto
Object: Ceres
Orbital Radius: 130995855
Period: 1679.67
Radius: 48700
Object: Io
Orbital Radius: 22000000
Period: 1.7691377186
Radius: 182100.3
Object: Europa
Orbital Radius: 36486200
Period: 3.551181
Radius: 156000.8
Object: Ganymede
Orbital Radius: 47160000
Period: 7.15455296
Radius: 263400
Object: Callisto
Orbital Radius: 69700000
Period: 16.6890184
Radius: 241000
Object: Saturn
Orbital Radius: 353572956
Period: 10759.22
Radius: 6026800
Satellites: Mimas,Enceladus,Tethys,Dione,Rhea,Titan,Iapetus
Object: Mimas
Orbital Radius: 8433396
Radius: 20600
Period: 0.9
Object: Enceladus
Orbital Radius: 10706000
Radius: 25000
Period: 1.4
Object: Tethys
Orbital Radius: 13706000
Radius: 50000
Period: 1.9
Object: Dione
Orbital Radius: 17106000
Radius: 56000
Period: 2.7
Object: Rhea
Orbital Radius: 24000000
Radius: 75000
Period: 4.5
Object: Titan
Orbital Radius: 50706000
Radius: 257600
Period: 15.945
Object: Iapetus
Radius: 75000
Orbital Radius: 72285891
Period: 79
#!/usr/bin/env python
"""
See
http://stackoverflow.com/questions/13520435/python3-recursion-animation-in-quickdraw
"""
from __future__ import division
import logging
from itertools import groupby
from math import pi as PI, sin, cos
from timeit import default_timer as timer # performance counter
OrderedDict = dict
try:
from collections import OrderedDict
except ImportError:
pass
import quickdraw as qd
DAY = 86400 # day in seconds
class Clock(object):
def __init__(self):
self._time = timer()
def tick(self, fps):
"""Limit number of calls to fps calls per second."""
endtime = self._time + 1 / fps
t = timer()
while t < endtime: # busy loop
t = timer()
elapsed = t - self._time
self._time = t
return elapsed
class Body(object):
def __init__(self, name, radius, orbital_radius, period, satellites):
self.name = name
self.radius = radius
self.orbital_radius = orbital_radius
self.period = period * DAY
self.satellites = satellites
self.phi = 0
self.scr_radius = self.radius
self.scr_orbital_radius = self.orbital_radius
@classmethod
def from_file(cls, filename_or_file):
cls.bodies = {}
should_close = not hasattr(filename_or_file, 'read') # filename given
file = open(filename_or_file) if should_close else filename_or_file
try:
# find root object name
for line in file:
key, sep, value = line.partition(":")
if sep and key == "RootObject":
root_object_name = value.strip()
break
else:
raise ValueError("can't find root object name ")
# parse objects (bodies)
def start_object(line, start=[None]):
if line.startswith('Object:'): # start new object
start[0] = not start[0]
return start[0]
for _, object_ in groupby(file, key=start_object):
d = dict(line.partition(':')[::2] for line in object_)
if 'Object' not in d:
continue # skip empty group
body = Body(
name=d['Object'].strip(),
radius=float(d['Radius']),
orbital_radius=float(d.get('Orbital Radius') or 0),
period=float(d.get('Period') or .0),
satellites=d.get('Satellites', '').split(",") or [])
cls.bodies[body.name] = body
finally:
if should_close:
file.close()
# fix satellites
root = cls.bodies[root_object_name]
root.remove_undefined_satellites()
return root
def remove_undefined_satellites(self):
for i, sputnik in enumerate(self.satellites):
if not isinstance(sputnik, Body):
sputnik = Body.bodies.get(sputnik.strip())
if sputnik is not None:
self.satellites[i] = sputnik
sputnik.remove_undefined_satellites()
# remove non-bodies
self.satellites[:] = [body for body in self.satellites
if isinstance(body, Body)]
def asdict(self):
return OrderedDict([
('Object', self.name),
('Radius', self.radius),
('Orbital Radius', self.orbital_radius),
('Period', self.period),
('Satellites', [body.asdict() for body in self.satellites]),
])
def update(self, time_):
"""Update x, y physical coordinates recursively."""
if self.period:
self.rotate(time_ * 2 * PI / self.period)
for body in self.satellites:
body.update(time_)
def rotate(self, phi):
"""Rotate phi radians anticlock-wise."""
self.phi += phi
self.phi %= 2 * PI
def scale(self, f):
# convert from physical scale to screen scale
self.scr_radius = f * self.radius
self.scr_orbital_radius = f * self.orbital_radius
for body in self.satellites:
body.scale(f)
def render(self, graphics, x0, y0, _level=1):
"""Draw the body recursively.
x0, y0 - screen coordinates of a central body (parent)
"""
# note: x0, y0 could be replaced by moving a group (begingroup, ..)
# compute screen coordinates
x = self.scr_orbital_radius * cos(self.phi)
y = -self.scr_orbital_radius * sin(self.phi)
# draw orbit
graphics.color(*[100] * 4)
graphics.circle(x0, y0, self.scr_orbital_radius)
# draw itself
if self.name == "Earth":
graphics.color(10, 255, 10)
else:
graphics.color(255 / _level, 255 / _level, 0,
255 - 10 * _level)
graphics.fillcircle(x + x0, y + y0, max(1, self.scr_radius))
# draw satellites
for body in self.satellites:
body.render(graphics, x + x0, y + y0, _level + 1)
class Screen(object):
def __init__(self):
self.width, self.height = 800, 600
def scale(self, maxdist):
return min(self.height, self.width) / maxdist / 2
def max_distance(body):
if body.satellites:
maxr = max(body.radius, max(map(max_distance, body.satellites)))
else:
maxr = body.radius
return body.orbital_radius + maxr
def main():
logging.basicConfig() # to see debug output; set level=logging.DEBUG
screen = Screen()
clock = Clock()
body = Body.from_file("input.txt")
maxdist = max_distance(body)
body.scale(screen.scale(maxdist))
qd.flush(False) # don't update screen immediately
qd.windowevents(True)
qd.render('on')
elapsed = 0
while True:
for e in qd.get_events():
if e.type is qd.WindowResized:
screen.width, screen.height = e.value
body.scale(screen.scale(maxdist))
# draw background
qd.color(0, 0, 0)
qd.clear()
# draw bodies recursively
body.update(1000000 * elapsed)
body.render(qd, screen.width // 2, screen.height // 2)
qd.refresh() # put on screen
elapsed = clock.tick(fps=60) # no more than given frames per second
main()
"""Convert function calls into corresponding quickdraw commands.
It uses http://pages.cpsc.ucalgary.ca/QuickDraw/quickdraw.jar
>>> import quickdraw as qd
>>> qd.help()
>>> qd.color(255, 0, 0)
>>> qd.fillcircle(100, 100, 50)
"""
# Implementation is based on
# https://github.com/amoffat/sh/blob/master/sh.py
import logging
import sys
from collections import deque, namedtuple
from subprocess import Popen, PIPE
from threading import Thread
debug = logging.getLogger('quickdraw').debug
class Error(RuntimeError):
pass
Event = namedtuple('Event', "type value")
class WindowResized(Event):
__slots__ = ()
def __new__(cls, value):
return Event.__new__(cls, WindowResized,
[int(n) for n in value.split(",")])
class WindowStateChanged(Event):
__slots__ = ()
def __new__(cls, value):
return Event.__new__(cls, WindowStateChanged, value.strip())
WindowStateChangedClosed = WindowStateChanged('Closed')
class EventType:
WindowResized = WindowResized
WindowStateChanged = WindowStateChanged
class Module(object):
"""Allow to import arbitrary quickdraw commands."""
def __init__(self, module):
self._module = module
def __getattribute__(self, name):
if name == '_module':
return object.__getattribute__(self, name)
elif ((name.startswith("__") and name.endswith("__")) or
name in "get_events WindowResized WindowResized".split()
):
return getattr(self._module, name)
else: # quickdraw command
return getattr(get_quickdraw(), 'command_' + name)
def __setattr__(self, name, value):
obj = self if name == '_module' else self._module
object.__setattr__(obj, name, value)
class Quickdraw(object):
"""quickdraw subprocess."""
def __init__(self, quickdraw_cmd=("java", "-jar", "quickdraw.jar")):
self._process = Popen(quickdraw_cmd,
universal_newlines=True,
stdin=PIPE, stdout=PIPE, stderr=PIPE,
bufsize=1) # line-buffered
if sys.platform.startswith('java'): # jython support
self._events = deque()
else:
self._events = deque(maxlen=100) # limit event history size
start_daemon_thread(target=self._read_stdout)
self._error = None
start_daemon_thread(target=self._read_stderr)
def __getattr__(self, name):
if not name.startswith('command_'):
raise AttributeError(name)
cmd = self._make_command(name[len('command_'):])
setattr(self, name, cmd)
return cmd
def _make_command(self, name):
def command(*args):
self._check_error()
cmd = name + " " + " ".join(quote(str(obj)) for obj in args)
debug(cmd)
try:
self._process.stdin.write(cmd)
self._process.stdin.write("\n")
except EnvironmentError:
if self._process.poll() is not None:
self.quit()
else:
raise
command.__name__ = name
return command
def get_events(self):
"""Yield all events occurred since previous call."""
self._check_error()
while self._events:
yield self._events.popleft()
self._check_error()
def quit(self):
for stream in "stdin stdout stderr".split():
s = getattr(self._process, stream)
if s is not None:
try:
s.close()
except EnvironmentError:
pass
setattr(self._process, stream, None)
sys.exit()
def _read_stdout(self):
try:
for line in iter(self._process.stdout.readline, ''):
debug(line)
type_, sep, value = line.partition(':')
if sep:
event_type = getattr(EventType, type_, None)
if event_type:
event = event_type(value)
if event == WindowStateChangedClosed:
self._error = 'quit'
self._events.append(event)
continue
sys.stdout.write(line)
finally:
self._process.stdout.close()
def _read_stderr(self):
self._error = self._process.stderr.read(1)
def _check_error(self):
if not self._error:
return
if self._error == 'quit':
self.quit()
c, self._error = self._error, None
message = c + self._process.stderr.readline()
raise Error(message)
def get_events():
return get_quickdraw().get_events()
def start_daemon_thread(**thread_args):
t = Thread(**thread_args)
t.daemon = True
t.start()
def quote(s):
"""Quote string for quickdraw."""
s = s.replace('\\', '\\\\').replace('"', r'\"')
return '"' + s + '"' if any(c.isspace() for c in s) else s
_quickdraw = None
def get_quickdraw():
"""Return global quickdraw instance."""
global _quickdraw
if _quickdraw is None:
_quickdraw = Quickdraw()
return _quickdraw
if __name__ == "__main__":
import doctest
sys.exit(doctest.testmod().failed)
else:
sys.modules[__name__] = Module(sys.modules[__name__])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment