astronoby 0.9.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/.ruby-version +1 -1
- data/CHANGELOG.md +235 -0
- data/README.md +19 -1
- data/UPGRADING.md +270 -0
- data/docs/README.md +105 -16
- data/docs/altitude_crossings.md +92 -0
- data/docs/angles.md +1 -0
- data/docs/configuration.md +28 -17
- data/docs/coordinates.md +72 -12
- data/docs/deep_sky_bodies.md +46 -1
- data/docs/ephem.md +5 -2
- data/docs/equinoxes_solstices_times.md +4 -3
- data/docs/glossary.md +99 -1
- data/docs/iers.md +40 -0
- data/docs/instant.md +33 -21
- data/docs/lunar_eclipses.md +208 -0
- data/docs/lunar_observation.md +87 -0
- data/docs/moon_phases.md +4 -1
- data/docs/observer.md +20 -6
- data/docs/planetary_phenomena.md +78 -0
- data/docs/reference_frames.md +192 -34
- data/docs/rise_transit_set_times.md +6 -4
- data/docs/solar_system_bodies.md +26 -4
- data/docs/twilight_times.md +43 -24
- data/lib/astronoby/angle.rb +63 -2
- data/lib/astronoby/angles/dms.rb +18 -1
- data/lib/astronoby/angles/hms.rb +14 -1
- data/lib/astronoby/angular_velocity.rb +21 -0
- data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
- data/lib/astronoby/bodies/deep_sky_object_position.rb +35 -38
- data/lib/astronoby/bodies/earth.rb +7 -44
- data/lib/astronoby/bodies/jupiter.rb +10 -0
- data/lib/astronoby/bodies/mars.rb +10 -0
- data/lib/astronoby/bodies/mercury.rb +10 -0
- data/lib/astronoby/bodies/moon.rb +158 -32
- data/lib/astronoby/bodies/neptune.rb +10 -0
- data/lib/astronoby/bodies/saturn.rb +10 -0
- data/lib/astronoby/bodies/solar_system_body.rb +240 -61
- data/lib/astronoby/bodies/sun.rb +79 -4
- data/lib/astronoby/bodies/uranus.rb +10 -0
- data/lib/astronoby/bodies/venus.rb +10 -0
- data/lib/astronoby/body.rb +6 -0
- data/lib/astronoby/cache.rb +13 -0
- data/lib/astronoby/center.rb +84 -0
- data/lib/astronoby/constants.rb +1 -2
- data/lib/astronoby/constellation.rb +9 -1
- data/lib/astronoby/constellations/data.rb +1 -1
- data/lib/astronoby/coordinates/ecliptic.rb +10 -1
- data/lib/astronoby/coordinates/equatorial.rb +79 -13
- data/lib/astronoby/coordinates/geodetic.rb +102 -0
- data/lib/astronoby/coordinates/horizontal.rb +13 -1
- data/lib/astronoby/delta_t.rb +44 -0
- data/lib/astronoby/distance.rb +35 -0
- data/lib/astronoby/duration.rb +116 -0
- data/lib/astronoby/earth_rotation.rb +70 -0
- data/lib/astronoby/equinox_solstice.rb +31 -8
- data/lib/astronoby/errors.rb +11 -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/conjunction.rb +51 -0
- data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
- data/lib/astronoby/events/extremum_calculator.rb +23 -176
- data/lib/astronoby/events/greatest_elongation.rb +58 -0
- data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
- data/lib/astronoby/events/lunar_eclipse.rb +111 -0
- data/lib/astronoby/events/lunar_eclipse_calculator.rb +397 -0
- 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/opposition.rb +19 -0
- data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
- data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
- data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
- data/lib/astronoby/events/twilight_calculator.rb +37 -147
- data/lib/astronoby/events/twilight_event.rb +24 -6
- data/lib/astronoby/events/twilight_events.rb +26 -6
- data/lib/astronoby/extremum_finder.rb +148 -0
- data/lib/astronoby/horizon.rb +40 -0
- data/lib/astronoby/horizontal_transformation.rb +112 -0
- data/lib/astronoby/instant.rb +56 -16
- data/lib/astronoby/libration.rb +25 -0
- data/lib/astronoby/mean_obliquity.rb +8 -0
- data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
- data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
- data/lib/astronoby/nutation.rb +10 -20
- data/lib/astronoby/observer.rb +67 -49
- data/lib/astronoby/orientation.rb +107 -0
- data/lib/astronoby/position.rb +16 -0
- data/lib/astronoby/precession.rb +62 -61
- data/lib/astronoby/reference_frame.rb +80 -9
- data/lib/astronoby/reference_frames/apparent.rb +43 -12
- data/lib/astronoby/reference_frames/astrometric.rb +14 -1
- data/lib/astronoby/reference_frames/geometric.rb +7 -1
- data/lib/astronoby/reference_frames/mean_of_date.rb +19 -1
- data/lib/astronoby/reference_frames/teme.rb +159 -0
- data/lib/astronoby/reference_frames/topocentric.rb +36 -4
- data/lib/astronoby/refraction.rb +26 -5
- data/lib/astronoby/root_finder.rb +90 -0
- data/lib/astronoby/rotation.rb +49 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +19 -8
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +44 -5
- data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_sidereal_time.rb +24 -0
- data/lib/astronoby/time/sidereal_time.rb +23 -1
- data/lib/astronoby/true_obliquity.rb +4 -0
- data/lib/astronoby/util/maths.rb +8 -0
- data/lib/astronoby/util/time.rb +0 -497
- data/lib/astronoby/vector.rb +10 -0
- data/lib/astronoby/velocity.rb +39 -0
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +30 -0
- metadata +80 -5
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class Opposition
|
|
5
|
+
# @return [Astronoby::Instant] when the opposition occurs
|
|
6
|
+
attr_reader :instant
|
|
7
|
+
|
|
8
|
+
# @return [Astronoby::Body] the body in opposition with the Sun
|
|
9
|
+
attr_reader :body
|
|
10
|
+
|
|
11
|
+
# @param instant [Astronoby::Instant] when the opposition occurs
|
|
12
|
+
# @param body [Astronoby::Body] the body in opposition with the Sun
|
|
13
|
+
def initialize(instant:, body:)
|
|
14
|
+
@instant = instant
|
|
15
|
+
@body = body
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
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
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds the rising, transit, and setting times for a single day.
|
|
4
5
|
class RiseTransitSetEvent
|
|
5
|
-
|
|
6
|
+
# @return [Time, nil] the rising time
|
|
7
|
+
attr_reader :rising_time
|
|
6
8
|
|
|
9
|
+
# @return [Time, nil] the transit (culmination) time
|
|
10
|
+
attr_reader :transit_time
|
|
11
|
+
|
|
12
|
+
# @return [Time, nil] the setting time
|
|
13
|
+
attr_reader :setting_time
|
|
14
|
+
|
|
15
|
+
# @param rising [Time, nil] the rising time
|
|
16
|
+
# @param transit [Time, nil] the transit time
|
|
17
|
+
# @param setting [Time, nil] the setting time
|
|
7
18
|
def initialize(rising, transit, setting)
|
|
8
19
|
@rising_time = rising
|
|
9
20
|
@transit_time = transit
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds arrays of rising, transit, and setting times over a time range.
|
|
4
5
|
class RiseTransitSetEvents
|
|
5
|
-
|
|
6
|
+
# @return [Array<Time>] rising times
|
|
7
|
+
attr_reader :rising_times
|
|
6
8
|
|
|
9
|
+
# @return [Array<Time>] transit (culmination) times
|
|
10
|
+
attr_reader :transit_times
|
|
11
|
+
|
|
12
|
+
# @return [Array<Time>] setting times
|
|
13
|
+
attr_reader :setting_times
|
|
14
|
+
|
|
15
|
+
# @param risings [Array<Time>] rising times
|
|
16
|
+
# @param transits [Array<Time>] transit times
|
|
17
|
+
# @param settings [Array<Time>] setting times
|
|
7
18
|
def initialize(risings, transits, settings)
|
|
8
19
|
@rising_times = risings
|
|
9
20
|
@transit_times = transits
|
|
@@ -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
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds twilight times (civil, nautical, astronomical) for a single day.
|
|
4
5
|
class TwilightEvent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:morning_nautical_twilight_time,
|
|
8
|
-
:evening_nautical_twilight_time,
|
|
9
|
-
:morning_astronomical_twilight_time,
|
|
10
|
-
:evening_astronomical_twilight_time
|
|
6
|
+
# @return [Time, nil] morning civil twilight time
|
|
7
|
+
attr_reader :morning_civil_twilight_time
|
|
11
8
|
|
|
9
|
+
# @return [Time, nil] evening civil twilight time
|
|
10
|
+
attr_reader :evening_civil_twilight_time
|
|
11
|
+
|
|
12
|
+
# @return [Time, nil] morning nautical twilight time
|
|
13
|
+
attr_reader :morning_nautical_twilight_time
|
|
14
|
+
|
|
15
|
+
# @return [Time, nil] evening nautical twilight time
|
|
16
|
+
attr_reader :evening_nautical_twilight_time
|
|
17
|
+
|
|
18
|
+
# @return [Time, nil] morning astronomical twilight time
|
|
19
|
+
attr_reader :morning_astronomical_twilight_time
|
|
20
|
+
|
|
21
|
+
# @return [Time, nil] evening astronomical twilight time
|
|
22
|
+
attr_reader :evening_astronomical_twilight_time
|
|
23
|
+
|
|
24
|
+
# @param morning_civil_twilight_time [Time, nil]
|
|
25
|
+
# @param evening_civil_twilight_time [Time, nil]
|
|
26
|
+
# @param morning_nautical_twilight_time [Time, nil]
|
|
27
|
+
# @param evening_nautical_twilight_time [Time, nil]
|
|
28
|
+
# @param morning_astronomical_twilight_time [Time, nil]
|
|
29
|
+
# @param evening_astronomical_twilight_time [Time, nil]
|
|
12
30
|
def initialize(
|
|
13
31
|
morning_civil_twilight_time: nil,
|
|
14
32
|
evening_civil_twilight_time: nil,
|