ephem 0.4.1 → 0.5.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.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ephem
4
+ module Core
5
+ # The orientation of a body, expressed as the three Euler angles that rotate
6
+ # a reference frame (e.g. J2000/ICRF) into the body-fixed frame, optionally
7
+ # with their time derivatives.
8
+ #
9
+ # For binary PCK orientation kernels the angles are the classical 3-1-3
10
+ # (Z-X-Z) sequence: +phi+ and +theta+ orient the pole and +psi+ is the
11
+ # rotation about it (the prime meridian). Angles are in radians and rates,
12
+ # when present, in radians per day — matching ephem's per-day rate
13
+ # convention for SPK velocities (divide by 86400 for radians per second).
14
+ class Orientation
15
+ # @return [Numeric] first Euler angle (radians)
16
+ attr_reader :phi
17
+
18
+ # @return [Numeric] second Euler angle (radians)
19
+ attr_reader :theta
20
+
21
+ # @return [Numeric] third Euler angle (radians)
22
+ attr_reader :psi
23
+
24
+ # @return [Array<Numeric>, nil] [phi, theta, psi] rates (radians/day),
25
+ # or nil when the orientation carries no rates
26
+ attr_reader :rates
27
+
28
+ # @param phi [Numeric] first Euler angle (radians)
29
+ # @param theta [Numeric] second Euler angle (radians)
30
+ # @param psi [Numeric] third Euler angle (radians)
31
+ # @param rates [Array<Numeric>, nil] optional [phi, theta, psi] rates
32
+ # (radians/day)
33
+ # @raise [Ephem::InvalidInputError] if any angle or rate is not numeric
34
+ def initialize(phi, theta, psi, rates: nil)
35
+ unless phi.is_a?(Numeric) && theta.is_a?(Numeric) && psi.is_a?(Numeric)
36
+ raise InvalidInputError, "Orientation angles must be numeric"
37
+ end
38
+
39
+ unless rates.nil? || valid_rates?(rates)
40
+ raise InvalidInputError, "Orientation rates must be three numerics"
41
+ end
42
+
43
+ @phi = phi
44
+ @theta = theta
45
+ @psi = psi
46
+ @rates = rates&.freeze
47
+ freeze
48
+ end
49
+
50
+ def self.[](phi, theta, psi, rates: nil)
51
+ new(phi, theta, psi, rates: rates)
52
+ end
53
+
54
+ # @return [Boolean] whether this orientation carries rates
55
+ def rates?
56
+ !@rates.nil?
57
+ end
58
+
59
+ # @return [Array<Numeric>] the three Euler angles [phi, theta, psi]
60
+ def to_a
61
+ [phi, theta, psi]
62
+ end
63
+
64
+ # The rotation matrix that maps the reference frame into the body-fixed
65
+ # frame, built from the 3-1-3 (Z-X-Z) Euler angles:
66
+ # +M = Rz(psi) * Rx(theta) * Rz(phi)+. Rates are ignored.
67
+ #
68
+ # @return [Array<Array<Float>>] a 3x3 rotation matrix
69
+ def to_matrix
70
+ Rotation.multiply(
71
+ Rotation.about_z(psi),
72
+ Rotation.about_x(theta),
73
+ Rotation.about_z(phi)
74
+ )
75
+ end
76
+
77
+ # @param index [Integer] 0 for phi, 1 for theta, 2 for psi
78
+ # @return [Numeric] the angle at the given index
79
+ # @raise [Ephem::IndexError] if index is not 0, 1, or 2
80
+ def [](index)
81
+ case index
82
+ when 0 then phi
83
+ when 1 then theta
84
+ when 2 then psi
85
+ else raise IndexError, "Invalid index: #{index}"
86
+ end
87
+ end
88
+
89
+ def inspect
90
+ body = "phi: #{phi}, theta: #{theta}, psi: #{psi}"
91
+ body += ", rates: #{rates}" if rates?
92
+ "Orientation[#{body}]"
93
+ end
94
+ alias_method :to_s, :inspect
95
+
96
+ def hash
97
+ [phi, theta, psi, rates, self.class].hash
98
+ end
99
+
100
+ def ==(other)
101
+ unless other.is_a?(self.class)
102
+ raise InvalidInputError, "Can only compare with another Orientation"
103
+ end
104
+
105
+ to_a == other.to_a && rates == other.rates
106
+ end
107
+ alias_method :eql?, :==
108
+
109
+ private
110
+
111
+ def valid_rates?(rates)
112
+ rates.is_a?(Array) &&
113
+ rates.size == 3 &&
114
+ rates.all?(Numeric)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ephem
4
+ module Core
5
+ # Builds and applies 3x3 rotation matrices. Kernel-agnostic: callers choose
6
+ # the axis sequence and order that matches their frame convention.
7
+ #
8
+ # The elementary rotations use the coordinate-frame (passive) convention:
9
+ # they express a fixed vector in a frame rotated by +angle about the axis
10
+ module Rotation
11
+ # @param angle [Numeric] rotation angle in radians
12
+ # @return [Array<Array<Float>>] rotation about the X axis
13
+ def self.about_x(angle)
14
+ cosine = Math.cos(angle)
15
+ sine = Math.sin(angle)
16
+ [
17
+ [1.0, 0.0, 0.0],
18
+ [0.0, cosine, sine],
19
+ [0.0, -sine, cosine]
20
+ ]
21
+ end
22
+
23
+ # @param angle [Numeric] rotation angle in radians
24
+ # @return [Array<Array<Float>>] rotation about the Y axis
25
+ def self.about_y(angle)
26
+ cosine = Math.cos(angle)
27
+ sine = Math.sin(angle)
28
+ [
29
+ [cosine, 0.0, -sine],
30
+ [0.0, 1.0, 0.0],
31
+ [sine, 0.0, cosine]
32
+ ]
33
+ end
34
+
35
+ # @param angle [Numeric] rotation angle in radians
36
+ # @return [Array<Array<Float>>] rotation about the Z axis
37
+ def self.about_z(angle)
38
+ cosine = Math.cos(angle)
39
+ sine = Math.sin(angle)
40
+ [
41
+ [cosine, sine, 0.0],
42
+ [-sine, cosine, 0.0],
43
+ [0.0, 0.0, 1.0]
44
+ ]
45
+ end
46
+
47
+ # Product of rotation matrices in the given order, as standard matrix
48
+ # multiplication: +multiply(a, b, c)+ returns +a * b * c+.
49
+ #
50
+ # @param matrices [Array<Array<Array<Float>>>] one or more 3x3 matrices
51
+ # @return [Array<Array<Float>>] the combined rotation matrix
52
+ def self.multiply(*matrices)
53
+ matrices.reduce { |product, matrix| multiply_pair(product, matrix) }
54
+ end
55
+
56
+ # Applies a rotation matrix to a vector.
57
+ #
58
+ # @param matrix [Array<Array<Float>>] a 3x3 rotation matrix
59
+ # @param vector [Core::Vector, Array<Numeric>] the vector to rotate
60
+ # @return [Core::Vector] the rotated vector
61
+ def self.apply(matrix, vector)
62
+ x, y, z = vector.to_a
63
+ Vector.new(
64
+ matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z,
65
+ matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z,
66
+ matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z
67
+ )
68
+ end
69
+
70
+ def self.multiply_pair(left, right)
71
+ Array.new(3) do |row|
72
+ Array.new(3) do |column|
73
+ left[row][0] * right[0][column] +
74
+ left[row][1] * right[1][column] +
75
+ left[row][2] * right[2][column]
76
+ end
77
+ end
78
+ end
79
+ private_class_method :multiply_pair
80
+ end
81
+ end
82
+ end
@@ -51,6 +51,11 @@ module Ephem
51
51
  )
52
52
  end
53
53
 
54
+ def inspect
55
+ "State[position: #{position}, velocity: #{velocity}]"
56
+ end
57
+ alias_method :to_s, :inspect
58
+
54
59
  # Converts the state vectors to arrays.
55
60
  #
56
61
  # @return [Array<Array<Numeric>>] Array containing position and velocity
@@ -11,6 +11,8 @@ module Ephem
11
11
  class Download
12
12
  JPL_BASE_URL = "https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/"
13
13
  IMCCE_BASE_URL = "https://ftp.imcce.fr/pub/ephem/planets/"
14
+ NAIF_PCK_BASE_URL =
15
+ "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/"
14
16
 
15
17
  JPL_KERNELS = %w[
16
18
  de102.bsp
@@ -59,6 +61,11 @@ module Ephem
59
61
  de441.bsp
60
62
  ].freeze
61
63
 
64
+ NAIF_PCK_KERNELS = %w[
65
+ moon_pa_de421_1900-2050.bpc
66
+ moon_pa_de440_200625.bpc
67
+ ].freeze
68
+
62
69
  IMCCE_KERNELS = {
63
70
  "inpop10b.bsp" => "inpop10b_TDB_m100_p100_spice.bsp",
64
71
  "inpop10b_large.bsp" => "inpop10b_TDB_m1000_p1000_spice.bsp",
@@ -90,7 +97,8 @@ module Ephem
90
97
  "inpop21a_large.bsp" => "inpop21a/inpop21a_TDB_m1000_p1000_spice.tar.gz"
91
98
  }.freeze
92
99
 
93
- SUPPORTED_KERNELS = (JPL_KERNELS + IMCCE_KERNELS.keys).freeze
100
+ SUPPORTED_KERNELS =
101
+ (JPL_KERNELS + NAIF_PCK_KERNELS + IMCCE_KERNELS.keys).freeze
94
102
 
95
103
  def self.call(name:, target:)
96
104
  new(name, target).call
@@ -104,12 +112,22 @@ module Ephem
104
112
 
105
113
  def call
106
114
  FileUtils.mkdir_p(@target_path.dirname)
107
- jpl_kernel? ? download_jpl : download_imcce
115
+ download_kernel
108
116
  true
109
117
  end
110
118
 
111
119
  private
112
120
 
121
+ def download_kernel
122
+ if jpl_kernel?
123
+ download_jpl
124
+ elsif pck_kernel?
125
+ download_naif_pck
126
+ else
127
+ download_imcce
128
+ end
129
+ end
130
+
113
131
  def validate_requested_kernel!
114
132
  unless SUPPORTED_KERNELS.include?(@name)
115
133
  raise UnsupportedError,
@@ -121,8 +139,20 @@ module Ephem
121
139
  JPL_KERNELS.include?(@name)
122
140
  end
123
141
 
142
+ def pck_kernel?
143
+ NAIF_PCK_KERNELS.include?(@name)
144
+ end
145
+
124
146
  def download_jpl
125
- uri = URI.join(JPL_BASE_URL, @name)
147
+ download_direct(JPL_BASE_URL)
148
+ end
149
+
150
+ def download_naif_pck
151
+ download_direct(NAIF_PCK_BASE_URL)
152
+ end
153
+
154
+ def download_direct(base_url)
155
+ uri = URI.join(base_url, @name)
126
156
  @target_path.open("wb") do |file|
127
157
  stream_http_to_file(uri, file)
128
158
  end
data/lib/ephem/excerpt.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ephem
4
- # The Excerpt class creates SPK file excerpts with reduced time spans and
5
- # target bodies. This is useful for creating smaller files that focus only on
6
- # the data needed for specific applications.
4
+ # The Excerpt class creates DAF excerpts (SPK or binary PCK) with reduced time
5
+ # spans and bodies. This is useful for creating smaller files that focus only
6
+ # on the data needed for specific applications.
7
7
  #
8
8
  # @example Create an excerpt with specific time range and bodies
9
9
  # spk = Ephem::SPK.open("de421.bsp")
@@ -19,11 +19,11 @@ module Ephem
19
19
  J2000_EPOCH = Core::Constants::Time::J2000_EPOCH
20
20
  RECORD_SIZE = 1024
21
21
 
22
- # @param spk [Ephem::SPK] The SPK object to create an excerpt from
23
- def initialize(spk)
24
- @spk = spk
25
- @daf = spk.instance_variable_get(:@daf)
26
- @binary_reader = @daf.instance_variable_get(:@binary_reader)
22
+ # @param kernel [Ephem::SPK, Ephem::PCK] The kernel to excerpt from
23
+ def initialize(kernel)
24
+ @kernel = kernel
25
+ @daf = kernel.daf
26
+ @binary_reader = @daf.binary_reader
27
27
  end
28
28
 
29
29
  # Creates an excerpt of the SPK file
@@ -31,11 +31,12 @@ module Ephem
31
31
  # @param output_path [String] Path where the excerpt will be written
32
32
  # @param start_jd [Float] Start time as Julian Date
33
33
  # @param end_jd [Float] End time as Julian Date
34
- # @param target_ids [Array<Integer>, nil] Optional list of target IDs to
35
- # include
34
+ # @param target_ids [Array<Integer>, nil] Optional list of target/body IDs
35
+ # to include
36
36
  # @param debug [Boolean] Whether to print debug information
37
37
  #
38
- # @return [Ephem::SPK] A new SPK instance for the excerpt file
38
+ # @return [Ephem::SPK, Ephem::PCK] A new instance for the excerpt file,
39
+ # matching the source kernel kind
39
40
  def extract(output_path:, start_jd:, end_jd:, target_ids: nil, debug: false)
40
41
  start_seconds = seconds_since_j2000(start_jd)
41
42
  end_seconds = seconds_since_j2000(end_jd)
@@ -46,11 +47,15 @@ module Ephem
46
47
  process_segments(writer, start_seconds, end_seconds, target_ids, debug)
47
48
  output_file.close
48
49
 
49
- SPK.open(output_path)
50
+ reopen(output_path)
50
51
  end
51
52
 
52
53
  private
53
54
 
55
+ def reopen(path)
56
+ (@daf.file_type == :pck) ? PCK.open(path) : SPK.open(path)
57
+ end
58
+
54
59
  def seconds_since_j2000(jd)
55
60
  (jd - J2000_EPOCH) * S_PER_DAY
56
61
  end
@@ -4,6 +4,8 @@ module Ephem
4
4
  module IO
5
5
  class BinaryReader
6
6
  RECORD_SIZE = 1024
7
+ ENDIANNESS_DOUBLE_FORMATS = {little: "E", big: "G"}.freeze
8
+ ENDIANNESS_UINT32_FORMATS = {little: "V", big: "N"}.freeze
7
9
 
8
10
  def initialize(file_object)
9
11
  validate_file_object(file_object)
@@ -81,20 +83,20 @@ module Ephem
81
83
  def read_and_pad_record
82
84
  data = @file.read(RECORD_SIZE)
83
85
  raise IOError, "Failed to read record" unless data
86
+
84
87
  data.ljust(RECORD_SIZE, "\0")
85
88
  end
86
89
 
87
90
  def read_array_data(length, endianness)
88
91
  data = @file.read(8 * length)
89
92
  raise IOError, "Failed to read array" unless data
93
+
90
94
  data.unpack("#{endianness_format(endianness)}#{length}")
91
95
  end
92
96
 
93
97
  def endianness_format(endianness)
94
- case endianness
95
- when :little then "E"
96
- when :big then "G"
97
- else raise ArgumentError, "Invalid endianness: #{endianness}"
98
+ ENDIANNESS_DOUBLE_FORMATS.fetch(endianness) do
99
+ raise ArgumentError, "Invalid endianness: #{endianness}"
98
100
  end
99
101
  end
100
102
  end
data/lib/ephem/io/daf.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Ephem
4
4
  module IO
5
5
  class DAF
6
- attr_reader :record_data, :endianness
6
+ attr_reader :binary_reader, :record_data, :endianness
7
7
 
8
8
  def initialize(file_object)
9
9
  @binary_reader = BinaryReader.new(file_object)
@@ -15,6 +15,11 @@ module Ephem
15
15
  @comments ||= load_comments
16
16
  end
17
17
 
18
+ # @return [Symbol, nil]
19
+ def file_type
20
+ @file_type ||= detect_file_type
21
+ end
22
+
18
23
  def summaries(&block)
19
24
  return enum_for(:summaries) unless block_given?
20
25
 
@@ -36,6 +41,18 @@ module Ephem
36
41
 
37
42
  private
38
43
 
44
+ def detect_file_type
45
+ case @record_data.locator_identifier
46
+ when "DAF/SPK" then :spk
47
+ when "DAF/PCK" then :pck
48
+ else
49
+ case @record_data.integer_count
50
+ when 6 then :spk
51
+ when 5 then :pck
52
+ end
53
+ end
54
+ end
55
+
39
56
  def setup_file_format
40
57
  file_record = @binary_reader.read_record(1)
41
58
  endianness_info = EndiannessManager.new(file_record).detect_endianness
@@ -43,11 +43,11 @@ module Ephem
43
43
  end
44
44
 
45
45
  def endian_uint32
46
- (@endianness == :little) ? "V" : "N"
46
+ BinaryReader::ENDIANNESS_UINT32_FORMATS[@endianness]
47
47
  end
48
48
 
49
49
  def endian_double
50
- (@endianness == :little) ? "E" : "G"
50
+ BinaryReader::ENDIANNESS_DOUBLE_FORMATS[@endianness]
51
51
  end
52
52
  end
53
53
  end
@@ -57,6 +57,7 @@ module Ephem
57
57
  @record_data = record_data
58
58
  @binary_reader = binary_reader
59
59
  @endianness = endianness
60
+ @record_parser = RecordParser.new(endianness: @endianness)
60
61
  setup_summary_format
61
62
  end
62
63
 
@@ -125,10 +126,10 @@ module Ephem
125
126
 
126
127
  def iterate_summary_chain(record_number)
127
128
  while record_number != 0
128
- process_summary_record(record_number) do |name, values|
129
- yield name, values
130
- end
131
- record_number = get_next_record(record_number)
129
+ record_number =
130
+ process_summary_record(record_number) do |name, values|
131
+ yield name, values
132
+ end
132
133
  end
133
134
  end
134
135
 
@@ -145,13 +146,14 @@ module Ephem
145
146
  ) do |name, values|
146
147
  yield name, values
147
148
  end
149
+
150
+ control[:next_record]
148
151
  end
149
152
 
150
153
  def parse_control_data(data)
151
- control_data = data[0, RecordParser::SUMMARY_CONTROL_SIZE]
152
- RecordParser
153
- .new(endianness: @endianness)
154
- .parse_summary_control(control_data)
154
+ @record_parser.parse_summary_control(
155
+ data[0, RecordParser::SUMMARY_CONTROL_SIZE]
156
+ )
155
157
  end
156
158
 
157
159
  def extract_summary_data(data)
@@ -169,25 +171,22 @@ module Ephem
169
171
 
170
172
  def extract_values(data)
171
173
  return [] if data.nil? || data.empty?
174
+
172
175
  data.unpack(@summary_format)
173
176
  end
174
177
 
175
178
  def extract_name(data)
176
179
  return "" if data.nil? || data.empty?
177
- data.strip
178
- end
179
180
 
180
- def get_next_record(record_number)
181
- data = @binary_reader.read_record(record_number)
182
- parse_control_data(data)[:next_record]
181
+ data.strip
183
182
  end
184
183
 
185
184
  def endian_double
186
- (@endianness == :little) ? "E" : "G"
185
+ BinaryReader::ENDIANNESS_DOUBLE_FORMATS[@endianness]
187
186
  end
188
187
 
189
188
  def endian_uint32
190
- (@endianness == :little) ? "V" : "N"
189
+ BinaryReader::ENDIANNESS_UINT32_FORMATS[@endianness]
191
190
  end
192
191
  end
193
192
  end
data/lib/ephem/pck.rb ADDED
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ephem
4
+ # Reads a binary PCK (+DAF/PCK+) orientation kernel: the orientation of one or
5
+ # more body frames over time, expressed as Euler angles.
6
+ class PCK
7
+ # Index of the data type within a PCK summary descriptor
8
+ # ([start, end, body, reference_frame, data_type, start_i, end_i]).
9
+ DATA_TYPE_IDENTIFIER = 4
10
+
11
+ attr_reader :daf, :segments
12
+
13
+ # @param daf [Ephem::IO::DAF] DAF containing PCK data
14
+ # @raise [ArgumentError] if the DAF is nil
15
+ def initialize(daf:)
16
+ raise ArgumentError, "DAF cannot be nil" if daf.nil?
17
+
18
+ @daf = daf
19
+ @segments = load_segments
20
+ @bodies = build_bodies
21
+ end
22
+
23
+ # Opens a binary PCK file.
24
+ #
25
+ # @param path [String] Path to the PCK (+.bpc+) file
26
+ # @return [PCK]
27
+ # @raise [ArgumentError] if the file is not a binary PCK or cannot be read
28
+ def self.open(path)
29
+ daf = IO::DAF.new(File.open(path, "rb"))
30
+ unless daf.file_type == :pck
31
+ raise ArgumentError, "#{path} is not a binary PCK (DAF/PCK) file"
32
+ end
33
+
34
+ new(daf: daf)
35
+ rescue Errno::EACCES => e
36
+ raise ArgumentError, "File permission denied: #{path} (#{e.message})"
37
+ rescue
38
+ daf&.close
39
+ raise
40
+ end
41
+
42
+ # @return [void]
43
+ def close
44
+ @daf&.close
45
+ @segments&.each(&:clear_data)
46
+ end
47
+
48
+ # Retrieves the orientation source for a body frame.
49
+ #
50
+ # @param body [Integer] NAIF frame ID of the oriented body
51
+ # @return [Segments::OrientationSegment, Segments::OrientationGroup] a
52
+ # single segment, or a group routing each query to the covering segment
53
+ # when the body spans several time intervals
54
+ # @raise [KeyError] if no segment is found for the given body
55
+ def [](body)
56
+ @bodies.fetch(body) do
57
+ raise KeyError, "No orientation segment found for body: #{body}"
58
+ end
59
+ end
60
+
61
+ # @return [String] the comments stored in the PCK file
62
+ def comments
63
+ @daf.comments
64
+ end
65
+
66
+ # @yieldparam segment [Segments::OrientationSegment]
67
+ # @return [Enumerator] if no block is given
68
+ def each_segment(&block)
69
+ return enum_for(:each_segment) unless block_given?
70
+
71
+ @segments.each(&block)
72
+ end
73
+
74
+ # @return [String] a description of the PCK file and its segments
75
+ def to_s
76
+ <<~DESCRIPTION
77
+ PCK file with #{@segments.size} segments:
78
+ #{@segments.join("\n")}
79
+ DESCRIPTION
80
+ end
81
+
82
+ def excerpt(output_path:, start_jd:, end_jd:, target_ids: nil, debug: false)
83
+ Excerpt
84
+ .new(self)
85
+ .extract(
86
+ output_path: output_path,
87
+ start_jd: start_jd,
88
+ end_jd: end_jd,
89
+ target_ids: target_ids,
90
+ debug: debug
91
+ )
92
+ end
93
+
94
+ private
95
+
96
+ def load_segments
97
+ @daf.summaries.map do |source, descriptor|
98
+ build_segment(source: source, descriptor: descriptor)
99
+ end
100
+ end
101
+
102
+ def build_bodies
103
+ @segments.group_by(&:body).transform_values do |segments|
104
+ Segments::OrientationGroup.wrap(segments)
105
+ end
106
+ end
107
+
108
+ def build_segment(source:, descriptor:)
109
+ data_type = descriptor[DATA_TYPE_IDENTIFIER]
110
+ segment_class = Segments::Registry.lookup(:pck, data_type)
111
+ unless segment_class
112
+ raise UnsupportedError, "Unsupported PCK data type: #{data_type}"
113
+ end
114
+
115
+ segment_class.new(daf: @daf, source: source, descriptor: descriptor)
116
+ end
117
+ end
118
+ end
@@ -38,6 +38,10 @@ module Ephem
38
38
  attr_reader :center
39
39
  # @return [String] the source of the segment
40
40
  attr_reader :source
41
+ # @return [Float] start of coverage as a Julian Date
42
+ attr_reader :start_jd
43
+ # @return [Float] end of coverage as a Julian Date
44
+ attr_reader :end_jd
41
45
 
42
46
  # Initialize a new segment
43
47
  #
@@ -55,14 +59,7 @@ module Ephem
55
59
  def initialize(daf:, source:, descriptor:)
56
60
  @daf = daf
57
61
  @source = source
58
- @start_second,
59
- @end_second,
60
- @target,
61
- @center,
62
- @frame,
63
- @data_type,
64
- @start_i,
65
- @end_i = descriptor
62
+ parse_descriptor(descriptor)
66
63
  @start_jd = compute_julian_date(@start_second)
67
64
  @end_jd = compute_julian_date(@end_second)
68
65
  end
@@ -97,6 +94,15 @@ module Ephem
97
94
  DESCRIPTION
98
95
  end
99
96
 
97
+ # Whether the given time falls within this segment's coverage.
98
+ #
99
+ # @param tdb [Numeric] time in TDB Julian Date
100
+ # @param tdb2 [Numeric] optional fractional part of the TDB date
101
+ # @return [Boolean]
102
+ def covers?(tdb, tdb2 = 0.0)
103
+ (tdb + tdb2).between?(@start_jd, @end_jd)
104
+ end
105
+
100
106
  def compute(_tdb, _tdb2 = 0.0)
101
107
  raise NotImplementedError,
102
108
  "#{self.class} has not implemented compute() for data type #{@data_type}"
@@ -113,6 +119,17 @@ module Ephem
113
119
 
114
120
  private
115
121
 
122
+ def parse_descriptor(descriptor)
123
+ @start_second,
124
+ @end_second,
125
+ @target,
126
+ @center,
127
+ @frame,
128
+ @data_type,
129
+ @start_i,
130
+ @end_i = descriptor
131
+ end
132
+
116
133
  def compute_julian_date(seconds)
117
134
  Time::J2000_EPOCH + seconds / Time::SECONDS_PER_DAY
118
135
  end