forked from akkana/scripts
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathepicycles.py
executable file
·214 lines (165 loc) · 6.95 KB
/
epicycles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env python3
# Plot all the planets' orbits, as viewed from a point that
# floats above the Earth's north ecliptic pole and moves with
# the Earth, to demonstrate phenomena like epicycles and the
# Venus pentagram. Idea from Galen Gisler's planetarium show.
#
# Copyright 2020 by Akkana Peck: Share and enjoy under the GPLv2 or later.
# Weird GLib bug: GLib.timeout_add takes an integer number
# of milliseconds, but if you pass it a float, it sets the timeout
# but then doesn't call draw() automatically after configure(),
# resulting in a transparent window since the black background
# doesn't get drawn. I guess it's some mysterious GTK/Cairo bug.
# Of course I could require an integer timeout when parsing arguments,
# but it's such an amusingly weird bug that I've left it as a float.
import ephem
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
import cairo
import math
import argparse
import sys
from datetime import datetime
planets = [
ephem.Mercury(),
ephem.Venus(),
ephem.Mars(),
ephem.Jupiter(),
ephem.Saturn(),
ephem.Uranus(),
ephem.Neptune(),
ephem.Pluto()
]
# For some reason, Gdk's "green" is not the 0 255 0 from rgb.txt, but
# comes out (red=0, green=32896, blue=0). But it parses green1 correctly.
planet_color_names = [ 'yellow', 'white', 'red', 'cyan', 'violet',
'green1', 'blue', 'purple' ]
# Cairo drawing wants color components to go from 0 to 1; GTK uses 0 to 65535.
# The good thing about standards is that there are so many of them.
def color_to_triplet(c):
return c.red / 65535, c.green / 65535, c.blue / 65535
class EclipticPoleWindow(Gtk.Window):
def __init__(self, auscale, timestep, time_increment=5, start_time=None):
"""time_increment is in days.
start_time is a an ephem.Date object.
"""
super().__init__()
self.auscale = auscale
self.timestep = timestep
self.stepping = True
if start_time:
self.time = start_time
else:
self.time = ephem.Date(datetime.now())
self.time_increment = ephem.hour * time_increment * 24
# Paths for each planet. Must save the full path
# since we might need to redraw if the window gets covered.
self.planet_paths = [ [] for i in range(len(planets)) ]
# Set up colors
self.bg_color = Gdk.color_parse('black')
self.planet_colors = [ Gdk.color_parse(c) for c in planet_color_names ]
self.line_width = 3
self.drawing_area = Gtk.DrawingArea()
self.set_default_size(1024, 768)
self.add(self.drawing_area)
GLib.timeout_add(self.timestep, self.idle_cb)
self.drawing_area.connect('draw', self.draw)
self.drawing_area.connect('configure-event', self.configure)
self.connect("destroy", Gtk.main_quit)
self.connect("key-press-event", self.key_press)
# GLib.idle_add(self.idle_cb)
self.show_all()
def draw(self, widget, ctx):
# print("Draw")
if not ctx:
print("Draw with no cairo context")
ctx = widget.get_window().cairo_create()
ctx.set_source_rgb(self.bg_color.red, self.bg_color.green,
self.bg_color.blue)
ctx.rectangle(0, 0, self.width, self.height)
ctx.fill()
# This makes no sense: specified line width has to be one less here
# than it does in idle_cb to result in the same line width.
ctx.set_line_width(self.line_width-1)
for i, path in enumerate(self.planet_paths):
if not path:
continue
ctx.set_source_rgb(*color_to_triplet(self.planet_colors[i]))
self.planet_segment(ctx, *path[0], False)
for pair in path[1:]:
self.planet_segment(ctx, *pair, True)
ctx.stroke()
def idle_cb(self):
"""Calculate and draw the next position of each planet.
"""
if not self.width:
print("idle: skipping")
return True
# self.time += ephem.date(self.time + self.time_increment)
self.time += self.time_increment
ctx = self.drawing_area.get_window().cairo_create()
for i, p in enumerate(planets):
p.compute(self.time)
ra = p.ra
dist = p.earth_distance
if self.planet_paths[i]:
ctx.set_source_rgb(*color_to_triplet(self.planet_colors[i]))
ctx.new_path()
self.planet_segment(ctx, *self.planet_paths[i][-1], False)
self.planet_segment(ctx, ra, dist, True)
ctx.stroke()
ctx.close_path()
self.planet_paths[i].append((ra, dist))
# Returning True reschedules the timeout.
return self.stepping
def planet_segment(self, ctx, ra, dist, drawp):
"""Draw (if drawp) or move to the appropriate place on the screen
for the given ra and dist coordinates.
"""
x = dist * self.dist_scale * math.cos(ra) + self.halfwidth
y = dist * self.dist_scale * math.sin(ra) + self.halfheight
if drawp:
ctx.line_to(x, y)
else:
ctx.move_to(x, y)
def configure(self, widget, event):
"""Window size change: reset the scale factors."""
# print("configure")
self.width, self.height = self.get_size()
self.halfwidth = self.width/2.
self.halfheight = self.height/2.
self.dist_scale = self.halfheight / self.auscale
# self.draw(widget, None)
def key_press(self, widget, event):
"""Handle a key press event anywhere in the window"""
# Note: to handle just printables with no modifier keys,
# use e.g. if event.string == "q"
if event.keyval == Gdk.KEY_q:
return Gtk.main_quit()
if event.keyval == Gdk.KEY_space:
self.stepping = not self.stepping
if self.stepping:
GLib.timeout_add(self.timestep, self.idle_cb)
return False
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="""Draw planet orbits from the north ecliptic pole.
Key bindings:
space Start/stop animation
q quit""",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-a', "--au", dest="auscale", type=float, default=11,
action="store",
help="""Scale of the window in astronomical units.
Default is 11, which shows Saturn.
2.6 shows Mars, 30 shows some of Pluto, 50 shows all of Pluto.""")
parser.add_argument('-t', "--timestep", dest="timestep",
type=float, default=30,
help="""Time step in milliseconds (default 30).
Controls how fast the orbits are drawn.""")
args = parser.parse_args(sys.argv[1:])
win = EclipticPoleWindow(auscale=args.auscale, timestep=args.timestep)
Gtk.main()