astronoby 0.8.0 → 0.10.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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +159 -0
  4. data/README.md +12 -5
  5. data/UPGRADING.md +109 -0
  6. data/docs/README.md +109 -16
  7. data/docs/angles.md +2 -1
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +73 -13
  10. data/docs/deep_sky_bodies.md +101 -0
  11. data/docs/ephem.md +6 -3
  12. data/docs/equinoxes_solstices_times.md +4 -3
  13. data/docs/glossary.md +97 -1
  14. data/docs/iers.md +40 -0
  15. data/docs/instant.md +21 -16
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +5 -2
  19. data/docs/observer.md +21 -7
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +193 -35
  22. data/docs/rise_transit_set_times.md +10 -8
  23. data/docs/{celestial_bodies.md → solar_system_bodies.md} +27 -5
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +69 -4
  26. data/lib/astronoby/angles/dms.rb +18 -1
  27. data/lib/astronoby/angles/hms.rb +14 -1
  28. data/lib/astronoby/angular_velocity.rb +97 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +49 -0
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +142 -0
  31. data/lib/astronoby/bodies/earth.rb +9 -42
  32. data/lib/astronoby/bodies/jupiter.rb +10 -0
  33. data/lib/astronoby/bodies/mars.rb +10 -0
  34. data/lib/astronoby/bodies/mercury.rb +10 -0
  35. data/lib/astronoby/bodies/moon.rb +162 -15
  36. data/lib/astronoby/bodies/neptune.rb +10 -0
  37. data/lib/astronoby/bodies/saturn.rb +10 -0
  38. data/lib/astronoby/bodies/solar_system_body.rb +257 -53
  39. data/lib/astronoby/bodies/sun.rb +79 -4
  40. data/lib/astronoby/bodies/uranus.rb +10 -0
  41. data/lib/astronoby/bodies/venus.rb +10 -0
  42. data/lib/astronoby/body.rb +6 -0
  43. data/lib/astronoby/cache.rb +1 -0
  44. data/lib/astronoby/center.rb +84 -0
  45. data/lib/astronoby/constants.rb +7 -2
  46. data/lib/astronoby/constellation.rb +9 -1
  47. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  48. data/lib/astronoby/coordinates/equatorial.rb +66 -13
  49. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  50. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  51. data/lib/astronoby/distance.rb +41 -0
  52. data/lib/astronoby/duration.rb +116 -0
  53. data/lib/astronoby/earth_rotation.rb +70 -0
  54. data/lib/astronoby/equinox_solstice.rb +31 -8
  55. data/lib/astronoby/errors.rb +11 -0
  56. data/lib/astronoby/events/conjunction.rb +51 -0
  57. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  58. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  59. data/lib/astronoby/events/extremum_calculator.rb +80 -0
  60. data/lib/astronoby/events/extremum_event.rb +15 -0
  61. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  62. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  63. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  64. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  65. data/lib/astronoby/events/opposition.rb +19 -0
  66. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -6
  67. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  68. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  69. data/lib/astronoby/events/twilight_calculator.rb +1 -1
  70. data/lib/astronoby/events/twilight_event.rb +24 -6
  71. data/lib/astronoby/events/twilight_events.rb +26 -6
  72. data/lib/astronoby/extremum_finder.rb +148 -0
  73. data/lib/astronoby/instant.rb +35 -9
  74. data/lib/astronoby/libration.rb +25 -0
  75. data/lib/astronoby/mean_obliquity.rb +8 -0
  76. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  77. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  78. data/lib/astronoby/nutation.rb +10 -20
  79. data/lib/astronoby/observer.rb +67 -49
  80. data/lib/astronoby/orientation.rb +107 -0
  81. data/lib/astronoby/position.rb +16 -0
  82. data/lib/astronoby/precession.rb +61 -60
  83. data/lib/astronoby/reference_frame.rb +73 -7
  84. data/lib/astronoby/reference_frames/apparent.rb +25 -16
  85. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  86. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  87. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  88. data/lib/astronoby/reference_frames/teme.rb +153 -0
  89. data/lib/astronoby/reference_frames/topocentric.rb +31 -5
  90. data/lib/astronoby/refraction.rb +26 -5
  91. data/lib/astronoby/root_finder.rb +83 -0
  92. data/lib/astronoby/rotation.rb +49 -0
  93. data/lib/astronoby/stellar_propagation.rb +162 -0
  94. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +31 -0
  95. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +101 -0
  96. data/lib/astronoby/time/greenwich_sidereal_time.rb +41 -58
  97. data/lib/astronoby/time/local_apparent_sidereal_time.rb +63 -0
  98. data/lib/astronoby/time/local_mean_sidereal_time.rb +63 -0
  99. data/lib/astronoby/time/local_sidereal_time.rb +59 -26
  100. data/lib/astronoby/time/sidereal_time.rb +64 -0
  101. data/lib/astronoby/true_obliquity.rb +4 -0
  102. data/lib/astronoby/util/maths.rb +8 -0
  103. data/lib/astronoby/util/time.rb +10 -467
  104. data/lib/astronoby/vector.rb +10 -0
  105. data/lib/astronoby/velocity.rb +44 -0
  106. data/lib/astronoby/version.rb +1 -1
  107. data/lib/astronoby.rb +33 -0
  108. metadata +58 -6
@@ -1,18 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents the Sun. Provides twilight events, equinox/solstice
5
+ # calculations, and equation of time.
4
6
  class Sun < SolarSystemBody
5
7
  EQUATORIAL_RADIUS = Distance.from_meters(695_700_000)
6
8
  ABSOLUTE_MAGNITUDE = -26.74
7
9
 
10
+ # @param _ephem_source [Symbol] the ephemeris source type
11
+ # @return [Array<Array>] ephemeris segment identifiers
8
12
  def self.ephemeris_segments(_ephem_source)
9
13
  [[SOLAR_SYSTEM_BARYCENTER, SUN]]
10
14
  end
11
15
 
16
+ # @return [Float] absolute magnitude
12
17
  def self.absolute_magnitude
13
18
  ABSOLUTE_MAGNITUDE
14
19
  end
15
20
 
21
+ # @param observer [Astronoby::Observer] Observer for whom to calculate
22
+ # twilight events
23
+ # @param ephem [::Ephem::SPK] Ephemeris data source
24
+ # @param date [Date] Date for which to calculate twilight events (optional)
25
+ # @param start_time [Time] Start time for twilight event calculation
26
+ # (optional)
27
+ # @param end_time [Time] End time for twilight event calculation (optional)
28
+ # @param utc_offset [String] UTC offset for the given date (e.g., "+02:00")
29
+ # @return [Astronoby::TwilightEvent, Array<Astronoby::TwilightEvent>]
30
+ # Twilight events for the given date or time range.
31
+ def self.twilight_events(
32
+ observer:,
33
+ ephem:,
34
+ date: nil,
35
+ start_time: nil,
36
+ end_time: nil,
37
+ utc_offset: 0
38
+ )
39
+ calculator = TwilightCalculator.new(observer: observer, ephem: ephem)
40
+ if date
41
+ calculator.event_on(date, utc_offset: utc_offset)
42
+ else
43
+ calculator.events_between(start_time, end_time)
44
+ end
45
+ end
46
+
47
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
48
+ # @param ephem [::Ephem::SPK] Ephemeris data source
49
+ # @return [Time] Time of the March equinox for the given year.
50
+ def self.march_equinox(year, ephem:)
51
+ EquinoxSolstice.march_equinox(year, ephem)
52
+ end
53
+
54
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
55
+ # @param ephem [::Ephem::SPK] Ephemeris data source
56
+ # @return [Time] Time of the June solstice for the given year.
57
+ def self.june_solstice(year, ephem:)
58
+ EquinoxSolstice.june_solstice(year, ephem)
59
+ end
60
+
61
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
62
+ # @param ephem [::Ephem::SPK] Ephemeris data source
63
+ # @return [Time] Time of the September equinox for the given year.
64
+ def self.september_equinox(year, ephem:)
65
+ EquinoxSolstice.september_equinox(year, ephem)
66
+ end
67
+
68
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
69
+ # @param ephem [::Ephem::SPK] Ephemeris data source
70
+ # @return [Time] Time of the December solstice for the given year.
71
+ def self.december_solstice(year, ephem:)
72
+ EquinoxSolstice.december_solstice(year, ephem)
73
+ end
74
+
16
75
  # Source:
17
76
  # Title: Explanatory Supplement to the Astronomical Almanac
18
77
  # Authors: Sean E. Urban and P. Kenneth Seidelmann
@@ -31,7 +90,7 @@ module Astronoby
31
90
  # Edition: 2nd edition
32
91
  # Chapter: 28 - Equation of Time
33
92
 
34
- # @return [Integer] Equation of time in seconds
93
+ # @return [Astronoby::Duration] Equation of time
35
94
  def equation_of_time
36
95
  right_ascension = apparent.equatorial.right_ascension
37
96
  t = (@instant.julian_date - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_MILLENIA
@@ -44,7 +103,7 @@ module Astronoby
44
103
  nutation = Nutation.new(instant: instant).nutation_in_longitude
45
104
  obliquity = TrueObliquity.at(@instant)
46
105
 
47
- (
106
+ seconds = (
48
107
  Angle
49
108
  .from_degrees(
50
109
  l0 -
@@ -53,12 +112,28 @@ module Astronoby
53
112
  nutation.degrees * obliquity.cos
54
113
  ).hours * Constants::SECONDS_PER_HOUR
55
114
  ).round
115
+ Duration.from_seconds(seconds)
56
116
  end
57
117
 
58
- private
118
+ # @return [nil] the Sun has no phase angle as seen from Earth
119
+ def phase_angle
120
+ nil
121
+ end
122
+
123
+ # @return [Boolean] always false; the Sun has no primary body
124
+ def approaching_primary?
125
+ false
126
+ end
59
127
 
60
- def requires_sun_data?
128
+ # @return [Boolean] always false; the Sun has no primary body
129
+ def receding_from_primary?
61
130
  false
62
131
  end
132
+
133
+ private
134
+
135
+ def primary_body_geometric
136
+ nil
137
+ end
63
138
  end
64
139
  end
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Uranus.
4
5
  class Uranus < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(25_559_000)
6
7
  ABSOLUTE_MAGNITUDE = -7.11
8
+ ORBITAL_PERIOD = 30688.5
7
9
 
10
+ # @return [Boolean] true; Uranus is a superior planet
11
+ def self.superior_planet?
12
+ true
13
+ end
14
+
15
+ # @param _ephem_source [Symbol] the ephemeris source type
16
+ # @return [Array<Array>] ephemeris segment identifiers
8
17
  def self.ephemeris_segments(_ephem_source)
9
18
  [[SOLAR_SYSTEM_BARYCENTER, URANUS_BARYCENTER]]
10
19
  end
11
20
 
21
+ # @return [Float] absolute magnitude
12
22
  def self.absolute_magnitude
13
23
  ABSOLUTE_MAGNITUDE
14
24
  end
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Venus.
4
5
  class Venus < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(6_051_800)
6
7
  ABSOLUTE_MAGNITUDE = -4.384
8
+ ORBITAL_PERIOD = 224.701
7
9
 
10
+ # @return [Boolean] true; Venus is an inferior planet
11
+ def self.inferior_planet?
12
+ true
13
+ end
14
+
15
+ # @param _ephem_source [Symbol] the ephemeris source type
16
+ # @return [Array<Array>] ephemeris segment identifiers
8
17
  def self.ephemeris_segments(_ephem_source)
9
18
  [[SOLAR_SYSTEM_BARYCENTER, VENUS_BARYCENTER]]
10
19
  end
11
20
 
21
+ # @return [Float] absolute magnitude
12
22
  def self.absolute_magnitude
13
23
  ABSOLUTE_MAGNITUDE
14
24
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Body
5
+ end
6
+ end
@@ -85,6 +85,7 @@ module Astronoby
85
85
  # @return [void]
86
86
  def max_size=(new_size)
87
87
  raise ArgumentError, "max_size must be positive" unless new_size > 0
88
+
88
89
  @mutex.synchronize do
89
90
  @max_size = new_size
90
91
  while @hash.size > @max_size
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Center
5
+ BARYCENTRIC = :barycentric
6
+ GEOCENTRIC = :geocentric
7
+ TOPOCENTRIC = :topocentric
8
+
9
+ class << self
10
+ # @return [Astronoby::Center] the Solar System barycenter
11
+ def barycentric
12
+ @barycentric ||= new(kind: BARYCENTRIC)
13
+ end
14
+
15
+ # @return [Astronoby::Center] the Earth's center
16
+ def geocentric
17
+ @geocentric ||= new(kind: GEOCENTRIC)
18
+ end
19
+
20
+ # @param observer [Astronoby::Observer] the observer
21
+ # @return [Astronoby::Center] a center at the observer's location
22
+ def topocentric(observer)
23
+ new(kind: TOPOCENTRIC, observer: observer)
24
+ end
25
+ end
26
+
27
+ # @return [Symbol] the kind of center
28
+ attr_reader :kind
29
+
30
+ # @return [Astronoby::Observer, nil] the observer, for topocentric centers
31
+ attr_reader :observer
32
+
33
+ # @param kind [Symbol] one of BARYCENTRIC, GEOCENTRIC, TOPOCENTRIC
34
+ # @param observer [Astronoby::Observer, nil] the observer, for topocentric
35
+ def initialize(kind:, observer: nil)
36
+ @kind = kind
37
+ @observer = observer
38
+ freeze
39
+ end
40
+
41
+ # @return [Boolean] true if the center is the Solar System barycenter
42
+ def barycentric?
43
+ @kind == BARYCENTRIC
44
+ end
45
+
46
+ # @return [Boolean] true if the center is the Earth's center
47
+ def geocentric?
48
+ @kind == GEOCENTRIC
49
+ end
50
+
51
+ # @return [Boolean] true if the center is at an observer's location
52
+ def topocentric?
53
+ @kind == TOPOCENTRIC
54
+ end
55
+
56
+ # @return [Boolean] true if the center depends on a specific observer
57
+ def observer_dependent?
58
+ topocentric?
59
+ end
60
+
61
+ # @param other [Astronoby::Center] center to compare with
62
+ # @return [Boolean] true if both centers are equivalent
63
+ def ==(other)
64
+ other.is_a?(self.class) &&
65
+ kind == other.kind &&
66
+ location_key == other.location_key
67
+ end
68
+ alias_method :eql?, :==
69
+
70
+ # @return [Integer] hash value
71
+ def hash
72
+ [self.class, @kind, location_key].hash
73
+ end
74
+
75
+ protected
76
+
77
+ # @return [Array, nil] the geometric location key, or nil if not topocentric
78
+ def location_key
79
+ return unless @observer
80
+
81
+ [@observer.latitude, @observer.longitude, @observer.elevation]
82
+ end
83
+ end
84
+ end
@@ -14,19 +14,24 @@ module Astronoby
14
14
 
15
15
  SECONDS_PER_MINUTE = 60.0
16
16
  MINUTES_PER_HOUR = 60.0
17
- MINUTES_PER_DEGREE = 60.0
18
-
19
17
  SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
20
18
  SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY
19
+ SECONDS_PER_JULIAN_YEAR = SECONDS_PER_DAY * DAYS_PER_JULIAN_YEAR
21
20
  RADIAN_PER_HOUR = Math::PI / 12.0
22
21
  MICROSECOND_IN_DAYS = 1.0 / SECONDS_PER_DAY / 1e6
23
22
 
23
+ ARCMINUTES_PER_DEGREE = 60.0
24
+ ARC_SECONDS_PER_ARCMINUTE = 60.0
25
+ ARCSECONDS_PER_DEGREE = ARC_SECONDS_PER_ARCMINUTE * ARCMINUTES_PER_DEGREE
26
+ MILLIARCSECONDS_PER_DEGREE = ARCSECONDS_PER_DEGREE * 1000
27
+
24
28
  PI_IN_DEGREES = 180.0
25
29
 
26
30
  EQUATION_OF_TIME_CONSTANT = 0.0057183
27
31
 
28
32
  KILOMETER_IN_METERS = 1_000
29
33
  ASTRONOMICAL_UNIT_IN_METERS = 149_597_870_700
34
+ PARSEC_IN_METERS = 3.0856775814913673e16
30
35
  EARTH_EQUATORIAL_RADIUS_IN_METERS = 6378140
31
36
 
32
37
  # WGS84 Earth Constants
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents an IAU constellation with its full name and standard
5
+ # three-letter abbreviation.
4
6
  class Constellation
5
- attr_reader :name, :abbreviation
7
+ # @return [String] the constellation name (e.g., "Orion")
8
+ attr_reader :name
6
9
 
10
+ # @return [String] the IAU three-letter abbreviation (e.g., "Ori")
11
+ attr_reader :abbreviation
12
+
13
+ # @param name [String] the constellation name
14
+ # @param abbreviation [String] the IAU abbreviation
7
15
  def initialize(name, abbreviation)
8
16
  @name = name
9
17
  @abbreviation = abbreviation
@@ -2,14 +2,23 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Ecliptic coordinate system (latitude and longitude relative to the
6
+ # ecliptic plane).
5
7
  class Ecliptic
6
- attr_reader :latitude, :longitude
8
+ # @return [Astronoby::Angle] ecliptic latitude
9
+ attr_reader :latitude
7
10
 
11
+ # @return [Astronoby::Angle] ecliptic longitude
12
+ attr_reader :longitude
13
+
14
+ # @param latitude [Astronoby::Angle] ecliptic latitude
15
+ # @param longitude [Astronoby::Angle] ecliptic longitude
8
16
  def initialize(latitude:, longitude:)
9
17
  @latitude = latitude
10
18
  @longitude = longitude
11
19
  end
12
20
 
21
+ # @return [Astronoby::Coordinates::Ecliptic] zero coordinates
13
22
  def self.zero
14
23
  new(latitude: Angle.zero, longitude: Angle.zero)
15
24
  end
@@ -2,9 +2,24 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Equatorial coordinate system (right ascension and declination).
5
6
  class Equatorial
6
- attr_reader :declination, :right_ascension, :hour_angle, :epoch
7
+ # @return [Astronoby::Angle] declination
8
+ attr_reader :declination
7
9
 
10
+ # @return [Astronoby::Angle] right ascension
11
+ attr_reader :right_ascension
12
+
13
+ # @return [Astronoby::Angle, nil] hour angle, if set
14
+ attr_reader :hour_angle
15
+
16
+ # @return [Numeric] the Julian Date epoch
17
+ attr_reader :epoch
18
+
19
+ # @param declination [Astronoby::Angle] declination
20
+ # @param right_ascension [Astronoby::Angle] right ascension
21
+ # @param hour_angle [Astronoby::Angle, nil] hour angle
22
+ # @param epoch [Numeric] Julian Date epoch (default: J2000.0 = 2451545.0)
8
23
  def initialize(
9
24
  declination:,
10
25
  right_ascension:,
@@ -17,10 +32,15 @@ module Astronoby
17
32
  @epoch = epoch
18
33
  end
19
34
 
35
+ # @return [Astronoby::Coordinates::Equatorial] zero coordinates
20
36
  def self.zero
21
37
  new(declination: Angle.zero, right_ascension: Angle.zero)
22
38
  end
23
39
 
40
+ # Derives equatorial coordinates from a position vector.
41
+ #
42
+ # @param position [Astronoby::Vector<Astronoby::Distance>] position vector
43
+ # @return [Astronoby::Coordinates::Equatorial] equatorial coordinates
24
44
  def self.from_position_vector(position)
25
45
  return zero if position.zero?
26
46
 
@@ -41,17 +61,24 @@ module Astronoby
41
61
  new(declination: declination, right_ascension: right_ascension)
42
62
  end
43
63
 
64
+ # Computes the hour angle for a given time and observer longitude.
65
+ #
66
+ # @param time [Time] the UTC time
67
+ # @param longitude [Astronoby::Angle] the observer's longitude
68
+ # @return [Astronoby::Angle] the hour angle
44
69
  def compute_hour_angle(time:, longitude:)
45
- lst = GreenwichSiderealTime
46
- .from_utc(time.utc)
47
- .to_lst(longitude: longitude)
48
-
49
- ha = (lst.time - @right_ascension.hours)
70
+ last = LocalApparentSiderealTime.from_utc(time.utc, longitude: longitude)
71
+ ha = (last.time - @right_ascension.hours)
50
72
  ha += Constants::HOURS_PER_DAY if ha.negative?
51
73
 
52
74
  Angle.from_hours(ha)
53
75
  end
54
76
 
77
+ # Converts to horizontal coordinates for a given observer and time.
78
+ #
79
+ # @param time [Time] the UTC time
80
+ # @param observer [Astronoby::Observer] the observer
81
+ # @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
55
82
  def to_horizontal(time:, observer:)
56
83
  latitude = observer.latitude
57
84
  longitude = observer.longitude
@@ -77,25 +104,32 @@ module Astronoby
77
104
  )
78
105
  end
79
106
 
107
+ # Converts to ecliptic coordinates.
108
+ #
80
109
  # Source:
81
110
  # Title: Celestial Calculations
82
111
  # Author: J. L. Lawrence
83
112
  # Edition: MIT Press
84
113
  # Chapter: 4 - Orbits and Coordinate Systems
85
- def to_ecliptic(instant:)
86
- mean_obliquity = MeanObliquity.at(instant)
87
-
114
+ #
115
+ # @param instant [Astronoby::Instant] the time instant for the obliquity
116
+ # @param obliquity [Astronoby::Angle] the obliquity of the ecliptic to
117
+ # rotate by. Defaults to the mean obliquity of date; pass the true
118
+ # obliquity when the equatorial coordinates already include nutation
119
+ # (true-of-date apparent and topocentric places).
120
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates
121
+ def to_ecliptic(instant:, obliquity: MeanObliquity.at(instant))
88
122
  y = Angle.from_radians(
89
- @right_ascension.sin * mean_obliquity.cos +
90
- @declination.tan * mean_obliquity.sin
123
+ @right_ascension.sin * obliquity.cos +
124
+ @declination.tan * obliquity.sin
91
125
  )
92
126
  x = Angle.from_radians(@right_ascension.cos)
93
127
  r = Angle.atan(y.radians / x.radians)
94
128
  longitude = Util::Trigonometry.adjustement_for_arctangent(y, x, r)
95
129
 
96
130
  latitude = Angle.asin(
97
- @declination.sin * mean_obliquity.cos -
98
- @declination.cos * mean_obliquity.sin * @right_ascension.sin
131
+ @declination.sin * obliquity.cos -
132
+ @declination.cos * obliquity.sin * @right_ascension.sin
99
133
  )
100
134
 
101
135
  Ecliptic.new(
@@ -103,6 +137,25 @@ module Astronoby
103
137
  longitude: longitude
104
138
  )
105
139
  end
140
+
141
+ # Position angle of another point as seen from this one, measured
142
+ # eastward (counterclockwise) from the direction of the north celestial
143
+ # pole. Both points must be expressed in the same frame.
144
+ #
145
+ # @param other [Astronoby::Coordinates::Equatorial] the target point
146
+ # @return [Astronoby::Angle] position angle, between -180 and 180 degrees
147
+ def position_angle_to(other)
148
+ delta_right_ascension = other.right_ascension - @right_ascension
149
+
150
+ Angle.from_radians(
151
+ Math.atan2(
152
+ other.declination.cos * delta_right_ascension.sin,
153
+ other.declination.sin * @declination.cos -
154
+ other.declination.cos * @declination.sin *
155
+ delta_right_ascension.cos
156
+ )
157
+ )
158
+ end
106
159
  end
107
160
  end
108
161
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Coordinates
5
+ # Geodetic coordinate system (WGS-84 latitude, longitude, and elevation).
6
+ #
7
+ # Geodetic coordinates describe a position on or above the Earth's surface
8
+ # using the WGS-84 reference ellipsoid, the same system used by GPS.
9
+ #
10
+ # The reverse conversion from ECEF (Earth-Centered Earth-Fixed) Cartesian
11
+ # coordinates uses Bowring's iterative method, which converges in 2-3
12
+ # iterations for typical satellite altitudes.
13
+ class Geodetic
14
+ SEMI_MINOR_AXIS =
15
+ Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS *
16
+ (1 - Constants::WGS84_FLATTENING)
17
+ SECOND_ECCENTRICITY_SQUARED =
18
+ (Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS**2 -
19
+ SEMI_MINOR_AXIS**2) /
20
+ SEMI_MINOR_AXIS**2
21
+ MAX_ITERATIONS = 10
22
+ CONVERGENCE_THRESHOLD = 1e-12
23
+
24
+ # @return [Astronoby::Angle] geodetic latitude
25
+ attr_reader :latitude
26
+
27
+ # @return [Astronoby::Angle] geodetic longitude
28
+ attr_reader :longitude
29
+
30
+ # @return [Astronoby::Distance] elevation above the WGS-84 ellipsoid
31
+ attr_reader :elevation
32
+
33
+ # @param latitude [Astronoby::Angle] geodetic latitude
34
+ # @param longitude [Astronoby::Angle] geodetic longitude
35
+ # @param elevation [Astronoby::Distance] elevation above the WGS-84
36
+ # ellipsoid
37
+ def initialize(latitude:, longitude:, elevation:)
38
+ @latitude = latitude
39
+ @longitude = longitude
40
+ @elevation = elevation
41
+ end
42
+
43
+ # Converts ECEF Cartesian coordinates to geodetic coordinates using
44
+ # Bowring's iterative method.
45
+ #
46
+ # Source:
47
+ # Title: Transformation from Spatial to Geographical Coordinates
48
+ # Author: B. R. Bowring
49
+ # Edition: Survey Review, Vol. 23, No. 181, 1976
50
+ #
51
+ # @param position [Astronoby::Vector<Astronoby::Distance>] ECEF position
52
+ # @return [Astronoby::Coordinates::Geodetic] geodetic coordinates
53
+ def self.from_ecef(position)
54
+ x = position.x.m
55
+ y = position.y.m
56
+ z = position.z.m
57
+
58
+ a = Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
59
+ e2 = Constants::WGS84_ECCENTICITY_SQUARED
60
+
61
+ p = Math.sqrt(x * x + y * y)
62
+ longitude = Math.atan2(y, x)
63
+
64
+ theta = Math.atan2(z * a, p * SEMI_MINOR_AXIS)
65
+ latitude = Math.atan2(
66
+ z + SECOND_ECCENTRICITY_SQUARED * SEMI_MINOR_AXIS *
67
+ Math.sin(theta)**3,
68
+ p - e2 * a * Math.cos(theta)**3
69
+ )
70
+
71
+ MAX_ITERATIONS.times do
72
+ prev_latitude = latitude
73
+ theta = Math.atan2(
74
+ (1 - Constants::WGS84_FLATTENING) * Math.sin(latitude),
75
+ Math.cos(latitude)
76
+ )
77
+ latitude = Math.atan2(
78
+ z + SECOND_ECCENTRICITY_SQUARED * SEMI_MINOR_AXIS *
79
+ Math.sin(theta)**3,
80
+ p - e2 * a * Math.cos(theta)**3
81
+ )
82
+ break if (latitude - prev_latitude).abs < CONVERGENCE_THRESHOLD
83
+ end
84
+
85
+ sin_lat = Math.sin(latitude)
86
+ n = a / Math.sqrt(1 - e2 * sin_lat * sin_lat)
87
+
88
+ elevation = if Math.cos(latitude).abs > 1e-10
89
+ p / Math.cos(latitude) - n
90
+ else
91
+ z / sin_lat - n * (1 - e2)
92
+ end
93
+
94
+ new(
95
+ latitude: Angle.from_radians(latitude),
96
+ longitude: Angle.from_radians(longitude),
97
+ elevation: Distance.from_meters(elevation)
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -2,9 +2,21 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Horizontal coordinate system (azimuth and altitude) for a specific
6
+ # observer.
5
7
  class Horizontal
6
- attr_reader :azimuth, :altitude, :observer
8
+ # @return [Astronoby::Angle] azimuth (measured from north, clockwise)
9
+ attr_reader :azimuth
7
10
 
11
+ # @return [Astronoby::Angle] altitude above the horizon
12
+ attr_reader :altitude
13
+
14
+ # @return [Astronoby::Observer] the observer
15
+ attr_reader :observer
16
+
17
+ # @param azimuth [Astronoby::Angle] azimuth
18
+ # @param altitude [Astronoby::Angle] altitude
19
+ # @param observer [Astronoby::Observer] the observer
8
20
  def initialize(
9
21
  azimuth:,
10
22
  altitude:,