|
| 1 | +#!/usr/bin/env ruby |
| 2 | +# |
| 3 | +# Visualizer to show sun and moon positions, give a certain time and position on |
| 4 | +# the earth. |
| 5 | +# |
| 6 | +# Uses "gosu" so you will need to install the following libraries: |
| 7 | +# * gosu (brew install gosu on macos) |
| 8 | +# * sdl2 (brew install sdl2 on macos) |
| 9 | +# |
| 10 | +# Uses gems: |
| 11 | +# * suncalc |
| 12 | +# * gosu |
| 13 | + |
| 14 | +# The moon is the brighter of the two bodies. The sun is the dimmer of the two. |
| 15 | +# |
| 16 | +# The green line is the horizon and will move up and down depending on the time |
| 17 | +# of the year. |
| 18 | +# |
| 19 | +# Default starting position on the earth is Grand Teton National Park, which |
| 20 | +# will be in the path of totality for the 2017 Eclipse. Default time is the |
| 21 | +# projected time of totality for that location. |
| 22 | +# |
| 23 | +# You can move through time with the left and right arrows. By default, right |
| 24 | +# arrow will move you 1 hour ahead, left will move you one hour behind. You can |
| 25 | +# change the movement speed using the up and down arrows. Your current movement |
| 26 | +# speed is shown in the window title bar. |
| 27 | +# |
| 28 | +# The "r" key will set the time to be sunrise of the current day. |
| 29 | +# |
| 30 | +# The "s" key will set the time to be sunset of the current day. |
| 31 | +# |
| 32 | +# Escape will exit. |
| 33 | +# |
| 34 | +# You can change the position on earth by changing the value of `@coords` |
| 35 | +# defined inthe Eclipse#initialize method. Insert your prefered Lat/Long |
| 36 | +# coordinates in decimal format. |
| 37 | +# |
| 38 | +# You can also change the default start time by changing `@current_time` |
| 39 | +# defined in the Eclipse#initialize method. You can also uses the SunCalc gem |
| 40 | +# to set the time to be sunrise, sunset, mid-day, etc. |
| 41 | + |
| 42 | + |
| 43 | +require 'suncalc' |
| 44 | +require 'time' |
| 45 | + |
| 46 | +require 'gosu' |
| 47 | +# brew install gosu |
| 48 | +# brew install sdl2 |
| 49 | + |
| 50 | +# https://gist.github.com/jlnr/661266 |
| 51 | +class Circle |
| 52 | + attr_reader :columns, :rows |
| 53 | + |
| 54 | + def initialize radius, color = 255 |
| 55 | + @columns = @rows = radius * 2 |
| 56 | + lower_half = (0...radius).map do |y| |
| 57 | + x = Math.sqrt(radius**2 - y**2).round |
| 58 | + right_half = "#{color.chr * x}#{0.chr * (radius - x)}" |
| 59 | + "#{right_half.reverse}#{right_half}" |
| 60 | + end.join |
| 61 | + @blob = lower_half.reverse + lower_half |
| 62 | + @blob.gsub!(/./) { |alpha| "#{255.chr}#{255.chr}#{255.chr}#{alpha}"} |
| 63 | + end |
| 64 | + |
| 65 | + def to_blob |
| 66 | + @blob |
| 67 | + end |
| 68 | +end |
| 69 | + |
| 70 | +class Eclipse < Gosu::Window |
| 71 | + MOVEMENTS_MODES = [ |
| 72 | + # name, times, change in seconds * times |
| 73 | + ['1 Minute', 1, 60], |
| 74 | + ['1 Hour', 60, 60], |
| 75 | + ['1 Day', 24, 3600], |
| 76 | + ['30 Days', 30, 86400], |
| 77 | + ['365 Days', 365, 86400] |
| 78 | + ] |
| 79 | + |
| 80 | + def initialize |
| 81 | + @width = 1024 |
| 82 | + @height = 746 |
| 83 | + super @width, @height |
| 84 | + self.caption = "Eclipse" |
| 85 | + |
| 86 | + @moon_d = 24 |
| 87 | + @moon = Gosu::Image.new(self, Circle.new(@moon_d, 255), false) |
| 88 | + @sun_d = 25 |
| 89 | + @sun = Gosu::Image.new(self, Circle.new(@sun_d, 128), false) |
| 90 | + @you_d = 10 |
| 91 | + @you = Gosu::Image.new(self, Circle.new(@you_d, 200), false) |
| 92 | + |
| 93 | + # @coords = [40.768860, -111.893273] # Salt Lake City, Utah |
| 94 | + @coords = [43.833333, -110.700833] # grand teton NP, 11:36am |
| 95 | + |
| 96 | + @current_time = Time.parse('2017-08-21 10:36:00') # grand teton NP totality, in MST/MDT time |
| 97 | + # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:solar_noon] |
| 98 | + # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:sunrise] |
| 99 | + # @current_time = SunCalc.get_times(Time.now, @coords.first, @coords.last)[:sunset] |
| 100 | + # @current_time = SunCalc.get_times(Time.parse('June 21, 2018'), @coords.first, @coords.last)[:sunrise] |
| 101 | + |
| 102 | + # Can use one of: [:solar_noon, :nadir, :sunrise, :sunset, :sunrise_end, :sunset_start, :dawn, :dusk, :nautical_dawn, :nautical_dusk, :night_end, :night, :golden_hour_end, :golden_hour] |
| 103 | + |
| 104 | + @current_movement_mode = 1 |
| 105 | + |
| 106 | + update_coords |
| 107 | + update_horizon_y |
| 108 | + puts 'update_horizon_y' |
| 109 | + |
| 110 | + @counter = 0 |
| 111 | + end |
| 112 | + |
| 113 | + def button_down(id) |
| 114 | + return if @moving |
| 115 | + case id |
| 116 | + when Gosu::Button::KbRight |
| 117 | + @moving = true |
| 118 | + puts "Forward in time #{MOVEMENTS_MODES[@current_movement_mode][0]}" |
| 119 | + @counter = MOVEMENTS_MODES[@current_movement_mode][1] |
| 120 | + when Gosu::Button::KbLeft |
| 121 | + @moving = true |
| 122 | + puts "Backward in time #{MOVEMENTS_MODES[@current_movement_mode][0]}" |
| 123 | + @counter = -MOVEMENTS_MODES[@current_movement_mode][1] |
| 124 | + when Gosu::Button::KbUp |
| 125 | + @current_movement_mode += 1 |
| 126 | + @current_movement_mode = 0 if @current_movement_mode > MOVEMENTS_MODES.length-1 |
| 127 | + when Gosu::Button::KbDown |
| 128 | + @current_movement_mode -= 1 |
| 129 | + @current_movement_mode = MOVEMENTS_MODES.length-1 if @current_movement_mode < 0 |
| 130 | + when Gosu::Button::KbR |
| 131 | + @current_time = get_times[:sunrise] |
| 132 | + when Gosu::Button::KbS |
| 133 | + @current_time = get_times[:sunset] |
| 134 | + when Gosu::Button::KbEscape |
| 135 | + exit |
| 136 | + end |
| 137 | + end |
| 138 | + |
| 139 | + def get_times |
| 140 | + SunCalc.get_times(@current_time, @coords.first, @coords.last) |
| 141 | + end |
| 142 | + |
| 143 | + # Sets horizon Y coordiate to be same as suns Y position at sunrise |
| 144 | + def update_horizon_y |
| 145 | + sunrise_time = get_times[:sunrise] |
| 146 | + |
| 147 | + sunrise_coords = SunCalc.get_position(sunrise_time, @coords.first, @coords.last) |
| 148 | + |
| 149 | + @horizon_y = map_range([-180, +180], [@height-@sun_d/2, @sun_d], sph2cart(sunrise_coords[:azimuth], sunrise_coords[:altitude])[:y]).to_i |
| 150 | + end |
| 151 | + |
| 152 | + def update_coords |
| 153 | + @sun_coords = SunCalc.get_position(@current_time, @coords.first, @coords.last) |
| 154 | + @moon_coords = SunCalc.get_moon_position(@current_time, @coords.first, @coords.last) |
| 155 | + end |
| 156 | + |
| 157 | + def update |
| 158 | + if @counter != 0 |
| 159 | + update_horizon_y |
| 160 | + update_caption |
| 161 | + end |
| 162 | + |
| 163 | + if @counter < 0 |
| 164 | + @current_time -= MOVEMENTS_MODES[@current_movement_mode][2] |
| 165 | + @counter += 1 |
| 166 | + elsif @counter > 0 |
| 167 | + @current_time += MOVEMENTS_MODES[@current_movement_mode][2] |
| 168 | + @counter -= 1 |
| 169 | + elsif @counter == 0 && @moving |
| 170 | + update_horizon_y |
| 171 | + @moving = false |
| 172 | + end |
| 173 | + |
| 174 | + update_coords |
| 175 | + end |
| 176 | + |
| 177 | + def draw |
| 178 | + # draw horizon |
| 179 | + draw_line(0, @horizon_y, Gosu::Color::GREEN, @width, @horizon_y, Gosu::Color::GREEN) |
| 180 | + |
| 181 | + # draw "you" |
| 182 | + @you.draw(@width/2, @horizon_y-@you_d, 0) |
| 183 | + |
| 184 | + # Draw sun sphere - the dimmer of the two |
| 185 | + @sun.draw( |
| 186 | + map_range([-180, +180], [@width-@sun_d/2, @sun_d], sph2cart(@sun_coords[:azimuth], @sun_coords[:altitude])[:x]).to_i-(@sun_d/2), |
| 187 | + map_range([-180, +180], [@height-@sun_d/2, @sun_d], sph2cart(@sun_coords[:azimuth], @sun_coords[:altitude])[:y]).to_i-(@sun_d/2), |
| 188 | + 0 |
| 189 | + ) |
| 190 | + |
| 191 | + # Draw moon sphere - the brighter of the two |
| 192 | + @moon.draw( |
| 193 | + map_range([-180, +180], [@width-@moon_d/2, @moon_d], sph2cart(@moon_coords[:azimuth], @moon_coords[:altitude])[:x]).to_i-(@moon_d/2), |
| 194 | + map_range([-180, +180], [@height-@moon_d/2, @moon_d], sph2cart(@moon_coords[:azimuth], @moon_coords[:altitude])[:y]).to_i-(@moon_d/2), |
| 195 | + 0 |
| 196 | + ) |
| 197 | + |
| 198 | + unless @moving |
| 199 | + # To prevent needless rapid redrawing if the screen isn't changing |
| 200 | + sleep(0.25) |
| 201 | + update_caption |
| 202 | + end |
| 203 | + end |
| 204 | + |
| 205 | + def update_caption |
| 206 | + self.caption = "#{(@current_time)} - #{MOVEMENTS_MODES[@current_movement_mode][0]}" |
| 207 | + end |
| 208 | + |
| 209 | + # Maps a number from one numeric range to another. In this case we need to be |
| 210 | + # able to map the coordinates of the sun/moon in to the coordinate range of |
| 211 | + # the Gosu window. |
| 212 | + def map_range(a, b, s) |
| 213 | + af, al, bf, bl = a.first, a.last, b.first, b.last |
| 214 | + bf + (s - af)*(bl - bf).quo(al - af) |
| 215 | + end |
| 216 | + |
| 217 | + # Convert Spherical coordinates (azimuth/elevation) in to cartesian (x/y) coordinates |
| 218 | + # https://www.mathworks.com/help/matlab/ref/sph2cart.html?requestedDomain=www.mathworks.com#input_argument_d0e929631 |
| 219 | + def sph2cart(azimuth, elevation) |
| 220 | + { |
| 221 | + # x: 180 * Math.cos(elevation) * Math.cos(azimuth), |
| 222 | + # y: 180 * Math.cos(elevation) * Math.sin(azimuth) |
| 223 | + x: 180 * Math.cos(elevation) * Math.sin(azimuth), |
| 224 | + y: 180 * Math.cos(elevation) * Math.cos(azimuth) |
| 225 | + |
| 226 | + } |
| 227 | + end |
| 228 | + |
| 229 | +end |
| 230 | + |
| 231 | +Eclipse.new.show |
0 commit comments