astronoby 0.9.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +101 -0
  4. data/README.md +6 -1
  5. data/UPGRADING.md +84 -0
  6. data/docs/README.md +80 -15
  7. data/docs/angles.md +1 -0
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +72 -12
  10. data/docs/deep_sky_bodies.md +1 -1
  11. data/docs/ephem.md +5 -2
  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 +20 -15
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +4 -1
  19. data/docs/observer.md +20 -6
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +192 -34
  22. data/docs/rise_transit_set_times.md +6 -4
  23. data/docs/solar_system_bodies.md +26 -4
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +63 -2
  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 +21 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +32 -17
  31. data/lib/astronoby/bodies/earth.rb +7 -44
  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 +158 -32
  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 +240 -61
  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/center.rb +84 -0
  44. data/lib/astronoby/constellation.rb +9 -1
  45. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  46. data/lib/astronoby/coordinates/equatorial.rb +64 -8
  47. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  48. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  49. data/lib/astronoby/distance.rb +35 -0
  50. data/lib/astronoby/duration.rb +116 -0
  51. data/lib/astronoby/earth_rotation.rb +70 -0
  52. data/lib/astronoby/equinox_solstice.rb +31 -8
  53. data/lib/astronoby/errors.rb +11 -0
  54. data/lib/astronoby/events/conjunction.rb +51 -0
  55. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  56. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  57. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  58. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  59. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  60. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  61. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  62. data/lib/astronoby/events/opposition.rb +19 -0
  63. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  64. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  65. data/lib/astronoby/events/twilight_event.rb +24 -6
  66. data/lib/astronoby/events/twilight_events.rb +26 -6
  67. data/lib/astronoby/extremum_finder.rb +148 -0
  68. data/lib/astronoby/instant.rb +10 -7
  69. data/lib/astronoby/libration.rb +25 -0
  70. data/lib/astronoby/mean_obliquity.rb +8 -0
  71. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  72. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  73. data/lib/astronoby/nutation.rb +10 -20
  74. data/lib/astronoby/observer.rb +67 -49
  75. data/lib/astronoby/orientation.rb +107 -0
  76. data/lib/astronoby/position.rb +16 -0
  77. data/lib/astronoby/precession.rb +61 -60
  78. data/lib/astronoby/reference_frame.rb +73 -7
  79. data/lib/astronoby/reference_frames/apparent.rb +26 -7
  80. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  81. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  82. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  83. data/lib/astronoby/reference_frames/teme.rb +153 -0
  84. data/lib/astronoby/reference_frames/topocentric.rb +30 -4
  85. data/lib/astronoby/refraction.rb +26 -5
  86. data/lib/astronoby/root_finder.rb +83 -0
  87. data/lib/astronoby/rotation.rb +49 -0
  88. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +9 -0
  89. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +42 -5
  90. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  91. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  92. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  93. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  94. data/lib/astronoby/time/sidereal_time.rb +23 -1
  95. data/lib/astronoby/true_obliquity.rb +4 -0
  96. data/lib/astronoby/util/maths.rb +8 -0
  97. data/lib/astronoby/util/time.rb +10 -485
  98. data/lib/astronoby/vector.rb +10 -0
  99. data/lib/astronoby/velocity.rb +39 -0
  100. data/lib/astronoby/version.rb +1 -1
  101. data/lib/astronoby.rb +22 -0
  102. metadata +45 -5
@@ -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
@@ -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
@@ -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,6 +61,11 @@ 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
70
  last = LocalApparentSiderealTime.from_utc(time.utc, longitude: longitude)
46
71
  ha = (last.time - @right_ascension.hours)
@@ -49,6 +74,11 @@ module Astronoby
49
74
  Angle.from_hours(ha)
50
75
  end
51
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
52
82
  def to_horizontal(time:, observer:)
53
83
  latitude = observer.latitude
54
84
  longitude = observer.longitude
@@ -74,25 +104,32 @@ module Astronoby
74
104
  )
75
105
  end
76
106
 
107
+ # Converts to ecliptic coordinates.
108
+ #
77
109
  # Source:
78
110
  # Title: Celestial Calculations
79
111
  # Author: J. L. Lawrence
80
112
  # Edition: MIT Press
81
113
  # Chapter: 4 - Orbits and Coordinate Systems
82
- def to_ecliptic(instant:)
83
- mean_obliquity = MeanObliquity.at(instant)
84
-
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))
85
122
  y = Angle.from_radians(
86
- @right_ascension.sin * mean_obliquity.cos +
87
- @declination.tan * mean_obliquity.sin
123
+ @right_ascension.sin * obliquity.cos +
124
+ @declination.tan * obliquity.sin
88
125
  )
89
126
  x = Angle.from_radians(@right_ascension.cos)
90
127
  r = Angle.atan(y.radians / x.radians)
91
128
  longitude = Util::Trigonometry.adjustement_for_arctangent(y, x, r)
92
129
 
93
130
  latitude = Angle.asin(
94
- @declination.sin * mean_obliquity.cos -
95
- @declination.cos * mean_obliquity.sin * @right_ascension.sin
131
+ @declination.sin * obliquity.cos -
132
+ @declination.cos * obliquity.sin * @right_ascension.sin
96
133
  )
97
134
 
98
135
  Ecliptic.new(
@@ -100,6 +137,25 @@ module Astronoby
100
137
  longitude: longitude
101
138
  )
102
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
103
159
  end
104
160
  end
105
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:,
@@ -1,93 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents a distance with meters as its internal representation.
5
+ # Provides conversions between meters, kilometers, astronomical units,
6
+ # and parsecs.
7
+ #
8
+ # @example Create a distance from astronomical units
9
+ # distance = Astronoby::Distance.from_au(1.0)
10
+ # distance.km # => 149597870.7
11
+ #
4
12
  class Distance
5
13
  include Comparable
6
14
 
7
15
  class << self
16
+ # @return [Astronoby::Distance] a zero distance
8
17
  def zero
9
18
  new(0)
10
19
  end
11
20
 
21
+ # @param meters [Numeric] the distance in meters
22
+ # @return [Astronoby::Distance] a new Distance
12
23
  def from_meters(meters)
13
24
  new(meters)
14
25
  end
15
26
  alias_method :from_m, :from_meters
16
27
 
28
+ # @param kilometers [Numeric] the distance in kilometers
29
+ # @return [Astronoby::Distance] a new Distance
17
30
  def from_kilometers(kilometers)
18
31
  meters = kilometers * Constants::KILOMETER_IN_METERS
19
32
  from_meters(meters)
20
33
  end
21
34
  alias_method :from_km, :from_kilometers
22
35
 
36
+ # @param astronomical_units [Numeric] the distance in AU
37
+ # @return [Astronoby::Distance] a new Distance
23
38
  def from_astronomical_units(astronomical_units)
24
39
  meters = astronomical_units * Constants::ASTRONOMICAL_UNIT_IN_METERS
25
40
  from_meters(meters)
26
41
  end
27
42
  alias_method :from_au, :from_astronomical_units
28
43
 
44
+ # @param parsecs [Numeric] the distance in parsecs
45
+ # @return [Astronoby::Distance] a new Distance
29
46
  def from_parsecs(parsecs)
30
47
  meters = parsecs * Constants::PARSEC_IN_METERS
31
48
  from_meters(meters)
32
49
  end
33
50
  alias_method :from_pc, :from_parsecs
34
51
 
52
+ # @param array [Array<Numeric>] array of meter values
53
+ # @return [Astronoby::Vector<Astronoby::Distance>] a vector of Distances
35
54
  def vector_from_meters(array)
36
55
  Vector.elements(array.map { from_meters(_1) })
37
56
  end
38
57
  alias_method :vector_from_m, :vector_from_meters
39
58
  end
40
59
 
60
+ # @return [Numeric] the distance in meters
41
61
  attr_reader :meters
42
62
  alias_method :m, :meters
43
63
 
64
+ # @param meters [Numeric] the distance in meters
44
65
  def initialize(meters)
45
66
  @meters = meters
46
67
  freeze
47
68
  end
48
69
 
70
+ # @return [Float] the distance in kilometers
49
71
  def kilometers
50
72
  @meters / Constants::KILOMETER_IN_METERS.to_f
51
73
  end
52
74
  alias_method :km, :kilometers
53
75
 
76
+ # @return [Float] the distance in astronomical units
54
77
  def astronomical_units
55
78
  @meters / Constants::ASTRONOMICAL_UNIT_IN_METERS.to_f
56
79
  end
57
80
  alias_method :au, :astronomical_units
58
81
 
82
+ # @param other [Astronoby::Distance] distance to add
83
+ # @return [Astronoby::Distance] the sum
59
84
  def +(other)
60
85
  self.class.from_meters(meters + other.meters)
61
86
  end
62
87
 
88
+ # @param other [Astronoby::Distance] distance to subtract
89
+ # @return [Astronoby::Distance] the difference
63
90
  def -(other)
64
91
  self.class.from_meters(@meters - other.meters)
65
92
  end
66
93
 
94
+ # @return [Astronoby::Distance] the negated distance
67
95
  def -@
68
96
  self.class.from_meters(-@meters)
69
97
  end
70
98
 
99
+ # @return [Boolean] true if the distance is positive
71
100
  def positive?
72
101
  meters > 0
73
102
  end
74
103
 
104
+ # @return [Boolean] true if the distance is negative
75
105
  def negative?
76
106
  meters < 0
77
107
  end
78
108
 
109
+ # @return [Boolean] true if the distance is zero
79
110
  def zero?
80
111
  meters.zero?
81
112
  end
82
113
 
114
+ # @return [Numeric] the square of the distance in meters
83
115
  def abs2
84
116
  meters**2
85
117
  end
86
118
 
119
+ # @return [Integer] hash value
87
120
  def hash
88
121
  [meters, self.class].hash
89
122
  end
90
123
 
124
+ # @param other [Astronoby::Distance] distance to compare with
125
+ # @return [Integer, nil] -1, 0, or 1; nil if not comparable
91
126
  def <=>(other)
92
127
  return unless other.is_a?(self.class)
93
128