astronoby 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +134 -0
- data/README.md +13 -0
- data/UPGRADING.md +186 -0
- data/docs/README.md +32 -8
- data/docs/altitude_crossings.md +92 -0
- data/docs/configuration.md +8 -0
- data/docs/deep_sky_bodies.md +45 -0
- data/docs/glossary.md +2 -0
- data/docs/instant.md +14 -7
- data/docs/lunar_eclipses.md +128 -13
- data/docs/twilight_times.md +26 -11
- data/lib/astronoby/bodies/deep_sky_object_position.rb +6 -24
- data/lib/astronoby/cache.rb +13 -0
- data/lib/astronoby/constants.rb +1 -2
- data/lib/astronoby/constellations/data.rb +1 -1
- data/lib/astronoby/coordinates/equatorial.rb +15 -5
- data/lib/astronoby/delta_t.rb +44 -0
- data/lib/astronoby/events/altitude_crossing.rb +29 -0
- data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
- data/lib/astronoby/events/altitude_extreme.rb +31 -0
- data/lib/astronoby/events/lunar_eclipse.rb +49 -37
- data/lib/astronoby/events/lunar_eclipse_calculator.rb +143 -31
- data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
- data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
- data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
- data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
- data/lib/astronoby/events/twilight_calculator.rb +37 -147
- data/lib/astronoby/horizon.rb +40 -0
- data/lib/astronoby/horizontal_transformation.rb +112 -0
- data/lib/astronoby/instant.rb +50 -13
- data/lib/astronoby/precession.rb +1 -1
- data/lib/astronoby/reference_frame.rb +8 -3
- data/lib/astronoby/reference_frames/apparent.rb +20 -8
- data/lib/astronoby/reference_frames/mean_of_date.rb +6 -0
- data/lib/astronoby/reference_frames/teme.rb +6 -0
- data/lib/astronoby/reference_frames/topocentric.rb +6 -0
- data/lib/astronoby/root_finder.rb +16 -9
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +10 -8
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +11 -9
- data/lib/astronoby/util/time.rb +0 -22
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +9 -1
- metadata +40 -5
- data/lib/astronoby/events/eclipse_phase.rb +0 -27
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class LunarEclipseVisibility
|
|
5
|
+
NOT_VISIBLE = :not_visible
|
|
6
|
+
ENTIRELY_VISIBLE = :entirely_visible
|
|
7
|
+
BEGINS_BEFORE_MOONRISE = :begins_before_moonrise
|
|
8
|
+
ENDS_AFTER_MOONSET = :ends_after_moonset
|
|
9
|
+
PARTIALLY_VISIBLE = :partially_visible
|
|
10
|
+
|
|
11
|
+
PHASES = [
|
|
12
|
+
LunarEclipse::PENUMBRAL,
|
|
13
|
+
LunarEclipse::PARTIAL,
|
|
14
|
+
LunarEclipse::TOTAL
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
SAMPLING_INTERVAL_IN_DAYS =
|
|
18
|
+
3.0 / Constants::HOURS_PER_DAY / Constants::MINUTES_PER_HOUR
|
|
19
|
+
|
|
20
|
+
RADIANS_PER_HOUR =
|
|
21
|
+
Constants::RADIANS_PER_CIRCLE / Constants::HOURS_PER_DAY
|
|
22
|
+
|
|
23
|
+
private_constant :SAMPLING_INTERVAL_IN_DAYS
|
|
24
|
+
private_constant :RADIANS_PER_HOUR
|
|
25
|
+
|
|
26
|
+
# @return [Astronoby::LunarEclipse] the eclipse
|
|
27
|
+
attr_reader :eclipse
|
|
28
|
+
|
|
29
|
+
# @return [Astronoby::Observer] the observer
|
|
30
|
+
attr_reader :observer
|
|
31
|
+
|
|
32
|
+
# @param eclipse [Astronoby::LunarEclipse] the eclipse
|
|
33
|
+
# @param observer [Astronoby::Observer] the observer
|
|
34
|
+
def initialize(eclipse:, observer:)
|
|
35
|
+
@eclipse = eclipse
|
|
36
|
+
@observer = observer
|
|
37
|
+
|
|
38
|
+
nodes = interpolation_nodes(eclipse)
|
|
39
|
+
@node_times = nodes.map { |node| node[:jd] }
|
|
40
|
+
@node_declinations = nodes.map { |node| node[:declination] }
|
|
41
|
+
@node_distances = nodes.map { |node| node[:distance] }
|
|
42
|
+
@node_right_ascensions = unwrapped(
|
|
43
|
+
nodes.map { |node| node[:right_ascension] }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@window_start = eclipse.penumbral.starting_instant.tt
|
|
47
|
+
@window_end = eclipse.penumbral.ending_instant.tt
|
|
48
|
+
|
|
49
|
+
@anchor = eclipse.instant.tt
|
|
50
|
+
@anchor_sidereal_hours = eclipse.instant.gast
|
|
51
|
+
|
|
52
|
+
@sin_latitude = observer.latitude.sin
|
|
53
|
+
@cos_latitude = observer.latitude.cos
|
|
54
|
+
@longitude = observer.longitude.radians
|
|
55
|
+
|
|
56
|
+
position = observer.geocentric_position
|
|
57
|
+
@observer_x = position.x.m
|
|
58
|
+
@observer_y = position.y.m
|
|
59
|
+
@observer_z = position.z.m
|
|
60
|
+
|
|
61
|
+
@memo = {}
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Boolean] true if any part of the eclipse happens with the Moon
|
|
66
|
+
# above the horizon
|
|
67
|
+
def visible?
|
|
68
|
+
!observable_windows.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Boolean] true if the Moon is above the horizon from the first to
|
|
72
|
+
# the last penumbral contact
|
|
73
|
+
def entirely_visible?
|
|
74
|
+
observable_windows.one? &&
|
|
75
|
+
above_horizon?(@window_start) &&
|
|
76
|
+
above_horizon?(@window_end)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Symbol] +NOT_VISIBLE+, +ENTIRELY_VISIBLE+,
|
|
80
|
+
# +BEGINS_BEFORE_MOONRISE+, +ENDS_AFTER_MOONSET+ or +PARTIALLY_VISIBLE+
|
|
81
|
+
def circumstance
|
|
82
|
+
return NOT_VISIBLE unless visible?
|
|
83
|
+
return ENTIRELY_VISIBLE if entirely_visible?
|
|
84
|
+
return PARTIALLY_VISIBLE unless observable_windows.one?
|
|
85
|
+
|
|
86
|
+
if above_horizon?(@window_end)
|
|
87
|
+
BEGINS_BEFORE_MOONRISE
|
|
88
|
+
elsif above_horizon?(@window_start)
|
|
89
|
+
ENDS_AFTER_MOONSET
|
|
90
|
+
else
|
|
91
|
+
PARTIALLY_VISIBLE
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param phase [Symbol] +LunarEclipse::PENUMBRAL+,
|
|
96
|
+
# +LunarEclipse::PARTIAL+ or +LunarEclipse::TOTAL+
|
|
97
|
+
# @return [Symbol, nil] +ENTIRELY_VISIBLE+, +PARTIALLY_VISIBLE+ or
|
|
98
|
+
# +NOT_VISIBLE+, and nil when the eclipse has no such phase
|
|
99
|
+
# @raise [Astronoby::UnsupportedEventError] if the phase is not a phase of
|
|
100
|
+
# a lunar eclipse
|
|
101
|
+
def coverage(phase)
|
|
102
|
+
unless PHASES.include?(phase)
|
|
103
|
+
raise UnsupportedEventError,
|
|
104
|
+
"phase must be one of #{PHASES.join(", ")}, got #{phase.inspect}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
eclipse_phase = @eclipse.public_send(phase)
|
|
108
|
+
return nil if eclipse_phase.nil?
|
|
109
|
+
|
|
110
|
+
starting = eclipse_phase.starting_instant.tt
|
|
111
|
+
ending = eclipse_phase.ending_instant.tt
|
|
112
|
+
crossed_within = horizon_crossing_times.any? do |time|
|
|
113
|
+
time > starting && time < ending
|
|
114
|
+
end
|
|
115
|
+
starts_up = above_horizon?(starting)
|
|
116
|
+
ends_up = above_horizon?(ending)
|
|
117
|
+
|
|
118
|
+
if crossed_within
|
|
119
|
+
PARTIALLY_VISIBLE
|
|
120
|
+
elsif starts_up && ends_up
|
|
121
|
+
ENTIRELY_VISIBLE
|
|
122
|
+
elsif starts_up || ends_up
|
|
123
|
+
PARTIALLY_VISIBLE
|
|
124
|
+
else
|
|
125
|
+
NOT_VISIBLE
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param instant [Astronoby::Instant] an instant of the eclipse
|
|
130
|
+
# @return [Boolean] true if the Moon is above the horizon then
|
|
131
|
+
# @raise [ArgumentError] if the instant is outside the eclipse
|
|
132
|
+
def above_horizon_at?(instant)
|
|
133
|
+
above_horizon?(within_eclipse(instant))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Array<Astronoby::Instant>] the instants of the eclipse at which
|
|
137
|
+
# the Moon rises or sets, in time order
|
|
138
|
+
def horizon_crossings
|
|
139
|
+
@memo[:horizon_crossings] ||= horizon_crossing_times
|
|
140
|
+
.map { |time| Instant.from_terrestrial_time(time) }
|
|
141
|
+
.freeze
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [Array<Range<Astronoby::Instant>>] the observable stretches, in
|
|
145
|
+
# time order
|
|
146
|
+
def observable_windows
|
|
147
|
+
@memo[:observable_windows] ||= begin
|
|
148
|
+
boundaries = [@window_start, *horizon_crossing_times, @window_end]
|
|
149
|
+
boundaries.each_cons(2).filter_map do |starting, ending|
|
|
150
|
+
next unless above_horizon?((starting + ending) / 2.0)
|
|
151
|
+
|
|
152
|
+
Range.new(
|
|
153
|
+
Instant.from_terrestrial_time(starting),
|
|
154
|
+
Instant.from_terrestrial_time(ending)
|
|
155
|
+
)
|
|
156
|
+
end.freeze
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def interpolation_nodes(eclipse)
|
|
163
|
+
phases = [eclipse.partial, eclipse.total].compact
|
|
164
|
+
contacts = [
|
|
165
|
+
[eclipse.penumbral.starting_instant, eclipse.penumbral.starting_geometry],
|
|
166
|
+
[eclipse.penumbral.ending_instant, eclipse.penumbral.ending_geometry],
|
|
167
|
+
[eclipse.instant, eclipse.geometry],
|
|
168
|
+
*phases.flat_map do |phase|
|
|
169
|
+
[
|
|
170
|
+
[phase.starting_instant, phase.starting_geometry],
|
|
171
|
+
[phase.ending_instant, phase.ending_geometry]
|
|
172
|
+
]
|
|
173
|
+
end
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
contacts.sort_by { |instant, _| instant.tt }.map do |instant, geometry|
|
|
177
|
+
coordinates = geometry.moon_coordinates
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
jd: instant.tt,
|
|
181
|
+
right_ascension: coordinates.right_ascension.radians,
|
|
182
|
+
declination: coordinates.declination.radians,
|
|
183
|
+
distance: geometry.moon_distance.m
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def unwrapped(right_ascensions)
|
|
189
|
+
right_ascensions.each_with_object([]) do |value, unwrapped|
|
|
190
|
+
previous = unwrapped.last
|
|
191
|
+
if previous.nil?
|
|
192
|
+
unwrapped << value
|
|
193
|
+
else
|
|
194
|
+
turns = ((value - previous) / Constants::RADIANS_PER_CIRCLE).round
|
|
195
|
+
unwrapped << value - turns * Constants::RADIANS_PER_CIRCLE
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def above_horizon?(time)
|
|
201
|
+
clearance(time).positive?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def clearance(time)
|
|
205
|
+
right_ascension, declination, distance = place(time)
|
|
206
|
+
horizon = Horizon::REFRACTION_ANGLE.radians -
|
|
207
|
+
Horizon.moon_semidiameter_radians(distance)
|
|
208
|
+
|
|
209
|
+
altitude_of(right_ascension, declination, distance, time) - horizon
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def altitude_of(right_ascension, declination, distance, time)
|
|
213
|
+
sidereal = sidereal_radians_at(time)
|
|
214
|
+
cos_declination = Math.cos(declination)
|
|
215
|
+
moon_x = distance * cos_declination * Math.cos(right_ascension)
|
|
216
|
+
moon_y = distance * cos_declination * Math.sin(right_ascension)
|
|
217
|
+
moon_z = distance * Math.sin(declination)
|
|
218
|
+
|
|
219
|
+
cos_sidereal = Math.cos(sidereal)
|
|
220
|
+
sin_sidereal = Math.sin(sidereal)
|
|
221
|
+
observer_x = @observer_x * cos_sidereal - @observer_y * sin_sidereal
|
|
222
|
+
observer_y = @observer_x * sin_sidereal + @observer_y * cos_sidereal
|
|
223
|
+
|
|
224
|
+
x = moon_x - observer_x
|
|
225
|
+
y = moon_y - observer_y
|
|
226
|
+
z = moon_z - @observer_z
|
|
227
|
+
|
|
228
|
+
local_sidereal = sidereal + @longitude
|
|
229
|
+
up_x = @cos_latitude * Math.cos(local_sidereal)
|
|
230
|
+
up_y = @cos_latitude * Math.sin(local_sidereal)
|
|
231
|
+
up_z = @sin_latitude
|
|
232
|
+
|
|
233
|
+
cosine = (x * up_x + y * up_y + z * up_z) /
|
|
234
|
+
Math.sqrt(x * x + y * y + z * z)
|
|
235
|
+
|
|
236
|
+
Math.asin(cosine.clamp(-1.0, 1.0))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def sidereal_radians_at(time)
|
|
240
|
+
sidereal_hours = @anchor_sidereal_hours +
|
|
241
|
+
(time - @anchor) *
|
|
242
|
+
Constants::HOURS_PER_DAY *
|
|
243
|
+
GreenwichMeanSiderealTime::UT_TO_SIDEREAL_RATIO
|
|
244
|
+
|
|
245
|
+
sidereal_hours * RADIANS_PER_HOUR
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def place(time)
|
|
249
|
+
first, second, third = interpolation_window(time)
|
|
250
|
+
x0 = @node_times[first]
|
|
251
|
+
x1 = @node_times[second]
|
|
252
|
+
x2 = @node_times[third]
|
|
253
|
+
w0 = (time - x1) * (time - x2) / ((x0 - x1) * (x0 - x2))
|
|
254
|
+
w1 = (time - x0) * (time - x2) / ((x1 - x0) * (x1 - x2))
|
|
255
|
+
w2 = (time - x0) * (time - x1) / ((x2 - x0) * (x2 - x1))
|
|
256
|
+
|
|
257
|
+
[
|
|
258
|
+
@node_right_ascensions,
|
|
259
|
+
@node_declinations,
|
|
260
|
+
@node_distances
|
|
261
|
+
].map do |values|
|
|
262
|
+
values[first] * w0 + values[second] * w1 + values[third] * w2
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def interpolation_window(time)
|
|
267
|
+
nearest = @node_times
|
|
268
|
+
.each_index
|
|
269
|
+
.min_by { |index| (@node_times[index] - time).abs }
|
|
270
|
+
middle = nearest.clamp(1, @node_times.length - 2)
|
|
271
|
+
|
|
272
|
+
[middle - 1, middle, middle + 1]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def horizon_crossing_times
|
|
276
|
+
@memo[:horizon_crossing_times] ||= RootFinder
|
|
277
|
+
.new(
|
|
278
|
+
value_at: ->(time) { clearance(time) },
|
|
279
|
+
period: SAMPLING_INTERVAL_IN_DAYS,
|
|
280
|
+
samples_per_period: 1
|
|
281
|
+
)
|
|
282
|
+
.roots(@window_start, @window_end)
|
|
283
|
+
.freeze
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def within_eclipse(instant)
|
|
287
|
+
time = instant.tt
|
|
288
|
+
unless time.between?(@window_start, @window_end)
|
|
289
|
+
raise ArgumentError,
|
|
290
|
+
"the instant must be between the first and last penumbral contact"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
time
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -18,9 +18,9 @@ module Astronoby
|
|
|
18
18
|
TAU = Math::PI * 2
|
|
19
19
|
SAMPLE_THRESHOLD = 0.8
|
|
20
20
|
REFINEMENT_ITERATIONS = 3
|
|
21
|
-
|
|
22
|
-
STANDARD_REFRACTION_ANGLE =
|
|
23
|
-
SUN_REFRACTION_ANGLE =
|
|
21
|
+
CONVERGENCE_TOLERANCE_IN_DAYS = 0.5 / Constants::SECONDS_PER_DAY
|
|
22
|
+
STANDARD_REFRACTION_ANGLE = Horizon::REFRACTION_ANGLE
|
|
23
|
+
SUN_REFRACTION_ANGLE = Horizon::SUN_ANGLE
|
|
24
24
|
EVENT_TYPES = [:rising, :transit, :setting].freeze
|
|
25
25
|
|
|
26
26
|
# @param body [Astronoby::SolarSystemBody, Astronoby::DeepSkyObject]
|
|
@@ -260,14 +260,17 @@ module Astronoby
|
|
|
260
260
|
.each_with_index
|
|
261
261
|
.map do |angle, i|
|
|
262
262
|
ratio = angle.radians / hour_angle_rates[i]
|
|
263
|
-
|
|
264
|
-
[time_adjustment, MIN_TIME_ADJUSTMENT].max
|
|
263
|
+
(ratio.nan? || ratio.infinite?) ? 0.0 : ratio
|
|
265
264
|
end
|
|
266
265
|
|
|
267
266
|
# Apply time adjustments
|
|
268
267
|
new_instants = new_instants.each_with_index.map do |instant, i|
|
|
269
268
|
Instant.from_terrestrial_time(instant.tt + time_adjustments[i])
|
|
270
269
|
end
|
|
270
|
+
|
|
271
|
+
break if time_adjustments.all? do |adjustment|
|
|
272
|
+
adjustment.abs < CONVERGENCE_TOLERANCE_IN_DAYS
|
|
273
|
+
end
|
|
271
274
|
end
|
|
272
275
|
|
|
273
276
|
new_instants.map { _1.to_time.round }
|
|
@@ -356,14 +359,7 @@ module Astronoby
|
|
|
356
359
|
end
|
|
357
360
|
|
|
358
361
|
def horizon_angle(distance)
|
|
359
|
-
|
|
360
|
-
SUN_REFRACTION_ANGLE
|
|
361
|
-
elsif @body == Astronoby::Moon
|
|
362
|
-
STANDARD_REFRACTION_ANGLE -
|
|
363
|
-
Angle.from_radians(Moon::EQUATORIAL_RADIUS.m / distance.m)
|
|
364
|
-
else
|
|
365
|
-
STANDARD_REFRACTION_ANGLE
|
|
366
|
-
end
|
|
362
|
+
Horizon.angle_for(body: @body, distance: distance)
|
|
367
363
|
end
|
|
368
364
|
|
|
369
365
|
def reset_state
|
|
@@ -22,13 +22,22 @@ module Astronoby
|
|
|
22
22
|
def initialize(observer:, ephem:)
|
|
23
23
|
@observer = observer
|
|
24
24
|
@ephem = ephem
|
|
25
|
+
@crossing_calculator = AltitudeCrossingCalculator.new(
|
|
26
|
+
body: Sun,
|
|
27
|
+
observer: observer,
|
|
28
|
+
ephem: ephem
|
|
29
|
+
)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def event_on(date, utc_offset: 0)
|
|
28
33
|
start_time = Time
|
|
29
34
|
.new(date.year, date.month, date.day, 0, 0, 0, utc_offset)
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
following = date.next_day
|
|
36
|
+
end_time = Time.new(
|
|
37
|
+
following.year, following.month, following.day,
|
|
38
|
+
0, 0, 0,
|
|
39
|
+
utc_offset
|
|
40
|
+
) - 1e-6
|
|
32
41
|
events = events_between(start_time, end_time)
|
|
33
42
|
|
|
34
43
|
TwilightEvent.new(
|
|
@@ -48,73 +57,26 @@ module Astronoby
|
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def events_between(start_time, end_time)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
rounded_time = event_time.round
|
|
63
|
-
next if equatorial_by_time.key?(rounded_time)
|
|
64
|
-
|
|
65
|
-
instant = Instant.from_time(rounded_time)
|
|
66
|
-
sun_at_time = Sun.new(instant: instant, ephem: @ephem)
|
|
67
|
-
equatorial_by_time[rounded_time] = sun_at_time.apparent.equatorial
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
morning_civil = []
|
|
71
|
-
evening_civil = []
|
|
72
|
-
morning_nautical = []
|
|
73
|
-
evening_nautical = []
|
|
74
|
-
morning_astronomical = []
|
|
75
|
-
evening_astronomical = []
|
|
76
|
-
|
|
77
|
-
arrays_by_period = {
|
|
78
|
-
MORNING => {
|
|
79
|
-
CIVIL => morning_civil,
|
|
80
|
-
NAUTICAL => morning_nautical,
|
|
81
|
-
ASTRONOMICAL => morning_astronomical
|
|
82
|
-
},
|
|
83
|
-
EVENING => {
|
|
84
|
-
CIVIL => evening_civil,
|
|
85
|
-
NAUTICAL => evening_nautical,
|
|
86
|
-
ASTRONOMICAL => evening_astronomical
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
[
|
|
91
|
-
[rts_events.rising_times, MORNING],
|
|
92
|
-
[rts_events.setting_times, EVENING]
|
|
93
|
-
].each do |times, period|
|
|
94
|
-
times.each do |event_time|
|
|
95
|
-
next unless event_time
|
|
96
|
-
|
|
97
|
-
equatorial_coordinates = equatorial_by_time[event_time.round]
|
|
98
|
-
TWILIGHT_ANGLES.each do |twilight, angle|
|
|
99
|
-
arrays_by_period[period][twilight] << compute_twilight_time_from(
|
|
100
|
-
period,
|
|
101
|
-
angle,
|
|
102
|
-
event_time,
|
|
103
|
-
equatorial_coordinates
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
60
|
+
crossings = @crossing_calculator.multiple_crossings_between(
|
|
61
|
+
start_time,
|
|
62
|
+
end_time,
|
|
63
|
+
altitudes: TWILIGHT_ANGLES.values.map { |angle| altitude_for(angle) }
|
|
64
|
+
)
|
|
65
|
+
times = TWILIGHT_ANGLES.keys.zip(crossings).to_h do |twilight, found|
|
|
66
|
+
[twilight, [:rising, :setting].map do |direction|
|
|
67
|
+
found
|
|
68
|
+
.select { |crossing| crossing.direction == direction }
|
|
69
|
+
.map { |crossing| crossing.instant.to_time.round }
|
|
70
|
+
end]
|
|
107
71
|
end
|
|
108
72
|
|
|
109
|
-
within_range = ->(time) { time && time >= start_time && time <= end_time }
|
|
110
|
-
|
|
111
73
|
TwilightEvents.new(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
74
|
+
times[CIVIL][0],
|
|
75
|
+
times[CIVIL][1],
|
|
76
|
+
times[NAUTICAL][0],
|
|
77
|
+
times[NAUTICAL][1],
|
|
78
|
+
times[ASTRONOMICAL][0],
|
|
79
|
+
times[ASTRONOMICAL][1]
|
|
118
80
|
)
|
|
119
81
|
end
|
|
120
82
|
|
|
@@ -130,92 +92,20 @@ module Astronoby
|
|
|
130
92
|
"period_of_the_day, got #{period_of_the_day}"
|
|
131
93
|
end
|
|
132
94
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
compute_twilight_time(
|
|
139
|
-
period_of_the_day,
|
|
140
|
-
zenith_angle,
|
|
141
|
-
observation_events,
|
|
142
|
-
equatorial_coordinates
|
|
95
|
+
crossing = @crossing_calculator.crossing_on(
|
|
96
|
+
date,
|
|
97
|
+
altitude: altitude_for(zenith_angle),
|
|
98
|
+
direction: (period_of_the_day == MORNING) ? :rising : :setting,
|
|
99
|
+
utc_offset: utc_offset
|
|
143
100
|
)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
private
|
|
147
|
-
|
|
148
|
-
def create_midday_instant(date, utc_offset: 0)
|
|
149
|
-
time = Time.new(date.year, date.month, date.day, 12, 0, 0, utc_offset)
|
|
150
|
-
Instant.from_time(time)
|
|
151
|
-
end
|
|
152
101
|
|
|
153
|
-
|
|
154
|
-
Astronoby::RiseTransitSetCalculator.new(
|
|
155
|
-
body: Sun,
|
|
156
|
-
observer: @observer,
|
|
157
|
-
ephem: @ephem
|
|
158
|
-
).event_on(date, utc_offset: utc_offset)
|
|
102
|
+
crossing&.instant&.to_time&.round
|
|
159
103
|
end
|
|
160
104
|
|
|
161
|
-
|
|
162
|
-
period_of_the_day,
|
|
163
|
-
zenith_angle,
|
|
164
|
-
observation_events,
|
|
165
|
-
equatorial_coordinates
|
|
166
|
-
)
|
|
167
|
-
period_time = if period_of_the_day == MORNING
|
|
168
|
-
observation_events.rising_time
|
|
169
|
-
else
|
|
170
|
-
observation_events.setting_time
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
compute_twilight_time_from(
|
|
174
|
-
period_of_the_day,
|
|
175
|
-
zenith_angle,
|
|
176
|
-
period_time,
|
|
177
|
-
equatorial_coordinates
|
|
178
|
-
)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def compute_twilight_time_from(
|
|
182
|
-
period_of_the_day,
|
|
183
|
-
zenith_angle,
|
|
184
|
-
period_time,
|
|
185
|
-
equatorial_coordinates
|
|
186
|
-
)
|
|
187
|
-
# If the sun doesn't rise or set on this day, we can't calculate twilight
|
|
188
|
-
return nil unless period_time
|
|
189
|
-
|
|
190
|
-
hour_angle_at_period = equatorial_coordinates
|
|
191
|
-
.compute_hour_angle(time: period_time, longitude: @observer.longitude)
|
|
192
|
-
|
|
193
|
-
term1 = zenith_angle.cos -
|
|
194
|
-
@observer.latitude.sin * equatorial_coordinates.declination.sin
|
|
195
|
-
term2 = @observer.latitude.cos * equatorial_coordinates.declination.cos
|
|
196
|
-
hour_angle_ratio_at_twilight = term1 / term2
|
|
197
|
-
|
|
198
|
-
# Check if twilight occurs at this location and date
|
|
199
|
-
return nil unless hour_angle_ratio_at_twilight.between?(-1, 1)
|
|
200
|
-
|
|
201
|
-
hour_angle_at_twilight = Angle.acos(hour_angle_ratio_at_twilight)
|
|
202
|
-
time_sign = -1
|
|
203
|
-
|
|
204
|
-
if period_of_the_day == MORNING
|
|
205
|
-
hour_angle_at_twilight = Angle.from_degrees(
|
|
206
|
-
Constants::DEGREES_PER_CIRCLE - hour_angle_at_twilight.degrees
|
|
207
|
-
)
|
|
208
|
-
time_sign = 1
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
twilight_in_hours =
|
|
212
|
-
time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
|
|
213
|
-
GreenwichMeanSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
|
|
214
|
-
twilight_in_seconds = time_sign *
|
|
215
|
-
twilight_in_hours *
|
|
216
|
-
Constants::SECONDS_PER_HOUR
|
|
105
|
+
private
|
|
217
106
|
|
|
218
|
-
|
|
107
|
+
def altitude_for(zenith_angle)
|
|
108
|
+
Angle.from_degrees(90) - zenith_angle
|
|
219
109
|
end
|
|
220
110
|
end
|
|
221
111
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
module Horizon
|
|
5
|
+
# @return [Astronoby::Angle] altitude of the centre of a point-like body at
|
|
6
|
+
# rise and set
|
|
7
|
+
REFRACTION_ANGLE = -Angle.from_dms(0, 34, 0)
|
|
8
|
+
|
|
9
|
+
# @return [Astronoby::Angle] altitude of the Sun's centre at rise and set
|
|
10
|
+
SUN_ANGLE = -Angle.from_dms(0, 50, 0)
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# @param body [Class] the body rising or setting
|
|
14
|
+
# @param distance [Astronoby::Distance] distance to the body, used for
|
|
15
|
+
# the Moon's semidiameter
|
|
16
|
+
# @return [Astronoby::Angle] altitude of the body's centre at rise and set
|
|
17
|
+
def angle_for(body:, distance:)
|
|
18
|
+
if body == Sun
|
|
19
|
+
SUN_ANGLE
|
|
20
|
+
elsif body == Moon
|
|
21
|
+
REFRACTION_ANGLE - moon_semidiameter(distance)
|
|
22
|
+
else
|
|
23
|
+
REFRACTION_ANGLE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param distance [Astronoby::Distance] distance to the Moon
|
|
28
|
+
# @return [Astronoby::Angle] the Moon's semidiameter
|
|
29
|
+
def moon_semidiameter(distance)
|
|
30
|
+
Angle.from_radians(moon_semidiameter_radians(distance.m))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param distance_in_meters [Numeric] distance to the Moon in meters
|
|
34
|
+
# @return [Float] the Moon's semidiameter in radians
|
|
35
|
+
def moon_semidiameter_radians(distance_in_meters)
|
|
36
|
+
Moon::EQUATORIAL_RADIUS.m.to_f / distance_in_meters
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|