skymap 0.4.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +133 -1
- data/README.md +570 -31
- data/data/LICENSE-deep_sky_objects.txt +30 -0
- data/data/LICENSE-stars.txt +35 -0
- data/data/deep_sky_objects.csv +1742 -0
- data/data/stars.bin +0 -0
- data/lib/skymap/canvas.rb +12 -3
- data/lib/skymap/catalog/deep_sky_objects.rb +60 -0
- data/lib/skymap/catalog/star_records.rb +322 -0
- data/lib/skymap/catalog/stars.rb +75 -12
- data/lib/skymap/chart.rb +18 -4
- data/lib/skymap/cone.rb +21 -0
- data/lib/skymap/context.rb +326 -8
- data/lib/skymap/deep_sky_object.rb +140 -0
- data/lib/skymap/eyepiece.rb +22 -0
- data/lib/skymap/field.rb +20 -0
- data/lib/skymap/geometry.rb +47 -0
- data/lib/skymap/horizon.rb +33 -3
- data/lib/skymap/label_placement.rb +263 -0
- data/lib/skymap/layers/cardinal_directions.rb +2 -0
- data/lib/skymap/layers/constellation_lines.rb +6 -1
- data/lib/skymap/layers/constellation_names.rb +5 -2
- data/lib/skymap/layers/deep_sky_objects.rb +128 -0
- data/lib/skymap/layers/ecliptic.rb +1 -7
- data/lib/skymap/layers/field_circles.rb +169 -0
- data/lib/skymap/layers/grid.rb +153 -0
- data/lib/skymap/layers/moon.rb +22 -12
- data/lib/skymap/layers/planets.rb +4 -0
- data/lib/skymap/layers/scale.rb +75 -0
- data/lib/skymap/layers/star_names.rb +21 -4
- data/lib/skymap/layers/stars.rb +31 -8
- data/lib/skymap/layers/sun.rb +13 -9
- data/lib/skymap/magnitude_limit.rb +15 -0
- data/lib/skymap/print_size.rb +50 -0
- data/lib/skymap/projection/oblique_stereographic.rb +87 -0
- data/lib/skymap/projection/stereographic.rb +11 -0
- data/lib/skymap/renderer/caption.rb +35 -3
- data/lib/skymap/renderer/deep_sky_symbol.rb +67 -0
- data/lib/skymap/renderer/field_circle.rb +7 -0
- data/lib/skymap/renderer/grid.rb +7 -0
- data/lib/skymap/renderer/horizon_line.rb +7 -0
- data/lib/skymap/renderer/label.rb +3 -1
- data/lib/skymap/renderer/scale_bar.rb +7 -0
- data/lib/skymap/renderer/svg.rb +204 -15
- data/lib/skymap/setup.rb +48 -0
- data/lib/skymap/star.rb +127 -2
- data/lib/skymap/star_list.rb +25 -0
- data/lib/skymap/telescope.rb +17 -0
- data/lib/skymap/theme.rb +54 -20
- data/lib/skymap/version.rb +1 -1
- data/lib/skymap/view.rb +49 -0
- data/lib/skymap.rb +24 -0
- metadata +31 -2
- data/data/stars.csv +0 -9097
data/data/stars.bin
ADDED
|
Binary file
|
data/lib/skymap/canvas.rb
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Skymap
|
|
4
|
-
Canvas = Data.define(:size, :padding) do
|
|
5
|
-
def initialize(size:, padding:)
|
|
6
|
-
unless
|
|
4
|
+
Canvas = Data.define(:size, :padding, :print_size) do
|
|
5
|
+
def initialize(size:, padding:, print_size: nil)
|
|
6
|
+
unless size.is_a?(Numeric) && size.real? && size.finite? && size > 0
|
|
7
|
+
raise ArgumentError, "size must be a number above 0"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
unless padding.is_a?(Numeric) && padding.real? && padding >= 0 &&
|
|
11
|
+
padding < size / 2.0
|
|
7
12
|
raise ArgumentError,
|
|
8
13
|
"padding must not be negative and must be less than half the size"
|
|
9
14
|
end
|
|
10
15
|
|
|
16
|
+
unless print_size.nil? || print_size.is_a?(PrintSize)
|
|
17
|
+
raise ArgumentError, "print_size must be a Skymap::PrintSize"
|
|
18
|
+
end
|
|
19
|
+
|
|
11
20
|
super
|
|
12
21
|
end
|
|
13
22
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Catalog
|
|
7
|
+
class DeepSkyObjects
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
PATH = File.expand_path("../../../data/deep_sky_objects.csv", __dir__)
|
|
11
|
+
MAGNITUDE_BANDS = {"V" => :visual, "B" => :blue}.freeze
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
|
+
return enum_for(:each) unless block_given?
|
|
15
|
+
|
|
16
|
+
CSV.foreach(PATH, headers: true) do |row|
|
|
17
|
+
yield deep_sky_object_from(row)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def deep_sky_object_from(row)
|
|
24
|
+
DeepSkyObject.new(
|
|
25
|
+
designation: row["designation"],
|
|
26
|
+
name: row["name"],
|
|
27
|
+
type: row["type"].to_sym,
|
|
28
|
+
equatorial_coordinates: Astronoby::Coordinates::Equatorial.new(
|
|
29
|
+
right_ascension: Astronoby::Angle.from_hours(
|
|
30
|
+
row["right_ascension"].to_f
|
|
31
|
+
),
|
|
32
|
+
declination: Astronoby::Angle.from_degrees(row["declination"].to_f),
|
|
33
|
+
epoch: Astronoby::JulianDate::J2000
|
|
34
|
+
),
|
|
35
|
+
magnitude: row["magnitude"]&.to_f,
|
|
36
|
+
magnitude_band: magnitude_band(row["magnitude_band"]),
|
|
37
|
+
major_axis: arcminutes(row["major_axis"]),
|
|
38
|
+
minor_axis: arcminutes(row["minor_axis"]),
|
|
39
|
+
position_angle: position_angle(row["position_angle"])
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def magnitude_band(value)
|
|
44
|
+
MAGNITUDE_BANDS.fetch(value) if value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def arcminutes(value)
|
|
48
|
+
return unless value
|
|
49
|
+
|
|
50
|
+
Astronoby::Angle.from_degrees(value.to_f / 60.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def position_angle(value)
|
|
54
|
+
return unless value
|
|
55
|
+
|
|
56
|
+
Astronoby::Angle.from_degrees(value.to_f)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../geometry"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Catalog
|
|
7
|
+
class StarRecords
|
|
8
|
+
MAGIC = "SKYS"
|
|
9
|
+
VERSION = 1
|
|
10
|
+
FORMAT = "L<l<s<s<s<S<s<S<"
|
|
11
|
+
RECORD_SIZE = 20
|
|
12
|
+
BRIGHTEST_BAND = 4
|
|
13
|
+
FAINTEST_BAND = 9
|
|
14
|
+
BANDS = (BRIGHTEST_BAND..FAINTEST_BAND)
|
|
15
|
+
ZONE_HEIGHT = 2
|
|
16
|
+
ZONES = 180 / ZONE_HEIGHT
|
|
17
|
+
OFFSETS_COUNT = BANDS.size * ZONES + 1
|
|
18
|
+
UNIT_SIZE = 4
|
|
19
|
+
SETTINGS = [
|
|
20
|
+
VERSION, BRIGHTEST_BAND, FAINTEST_BAND, ZONE_HEIGHT, RECORD_SIZE
|
|
21
|
+
].freeze
|
|
22
|
+
HEADER_SIZE = MAGIC.bytesize + (SETTINGS.size + 2) * UNIT_SIZE
|
|
23
|
+
NO_RECORD = 2**32 - 1
|
|
24
|
+
RIGHT_ASCENSION_STEPS = 2**32
|
|
25
|
+
DECLINATION_STEPS = 2**31
|
|
26
|
+
MISSING = -32768
|
|
27
|
+
PARALLAX_STEPS_PER_MAS = 10
|
|
28
|
+
STORED_RANGES = {
|
|
29
|
+
magnitude: -32768..32767,
|
|
30
|
+
proper_motion: -32767..32767,
|
|
31
|
+
parallax: 0..65535,
|
|
32
|
+
radial_velocity: -32767..32767,
|
|
33
|
+
hr: 0..65535
|
|
34
|
+
}.freeze
|
|
35
|
+
Record = Data.define(
|
|
36
|
+
:right_ascension,
|
|
37
|
+
:declination,
|
|
38
|
+
:magnitude,
|
|
39
|
+
:proper_motion_ra,
|
|
40
|
+
:proper_motion_dec,
|
|
41
|
+
:parallax,
|
|
42
|
+
:radial_velocity,
|
|
43
|
+
:hr
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def self.band(magnitude)
|
|
47
|
+
hundredths = (magnitude * 100).round
|
|
48
|
+
|
|
49
|
+
(-(-hundredths).div(100)).clamp(BRIGHTEST_BAND, FAINTEST_BAND)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.bands_within(magnitude_limit)
|
|
53
|
+
BANDS.select do |band|
|
|
54
|
+
magnitude_limit.nil? || band == BRIGHTEST_BAND ||
|
|
55
|
+
band - 1 < magnitude_limit
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.zone(declination)
|
|
60
|
+
((declination + 90) / ZONE_HEIGHT).floor.clamp(0, ZONES - 1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.encode(stars)
|
|
64
|
+
ordered = stars.sort_by do |star|
|
|
65
|
+
[
|
|
66
|
+
band(star.magnitude), zone(star.declination), star.right_ascension,
|
|
67
|
+
star.declination, star.magnitude, star.hr || 0
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
counts = Hash.new(0)
|
|
71
|
+
ordered.each do |star|
|
|
72
|
+
counts[[band(star.magnitude), zone(star.declination)]] += 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
offsets = []
|
|
76
|
+
total = 0
|
|
77
|
+
BANDS.each do |band|
|
|
78
|
+
ZONES.times do |zone|
|
|
79
|
+
offsets << total
|
|
80
|
+
total += counts[[band, zone]]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
offsets << total
|
|
84
|
+
|
|
85
|
+
by_hr = []
|
|
86
|
+
ordered.each_with_index { |star, index|
|
|
87
|
+
by_hr[star.hr] = index if star.hr
|
|
88
|
+
}
|
|
89
|
+
by_hr = by_hr.map { |index| index || NO_RECORD }
|
|
90
|
+
|
|
91
|
+
header = [MAGIC, *SETTINGS, ordered.size, by_hr.size]
|
|
92
|
+
|
|
93
|
+
[*header, *offsets, *by_hr].pack("a4L<*") +
|
|
94
|
+
ordered.map { |star| pack(star) }.join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.pack(star)
|
|
98
|
+
[
|
|
99
|
+
(star.right_ascension / 360.0 * RIGHT_ASCENSION_STEPS).round %
|
|
100
|
+
RIGHT_ASCENSION_STEPS,
|
|
101
|
+
[
|
|
102
|
+
(star.declination / 90.0 * DECLINATION_STEPS).round,
|
|
103
|
+
DECLINATION_STEPS - 1
|
|
104
|
+
].min,
|
|
105
|
+
fit((star.magnitude * 100).round, :magnitude),
|
|
106
|
+
fit(star.proper_motion_ra, :proper_motion) || MISSING,
|
|
107
|
+
fit(star.proper_motion_dec, :proper_motion) || MISSING,
|
|
108
|
+
fit(
|
|
109
|
+
((star.parallax || 0) * PARALLAX_STEPS_PER_MAS).round,
|
|
110
|
+
:parallax
|
|
111
|
+
),
|
|
112
|
+
fit(star.radial_velocity, :radial_velocity) || MISSING,
|
|
113
|
+
fit(star.hr || 0, :hr)
|
|
114
|
+
].pack(FORMAT)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.fit(value, field)
|
|
118
|
+
return value if value.nil? || STORED_RANGES.fetch(field).cover?(value)
|
|
119
|
+
|
|
120
|
+
raise ArgumentError,
|
|
121
|
+
"#{field} is out of the range of the file: #{value}"
|
|
122
|
+
end
|
|
123
|
+
private_class_method :pack, :fit
|
|
124
|
+
|
|
125
|
+
attr_reader :count
|
|
126
|
+
|
|
127
|
+
def initialize(data)
|
|
128
|
+
unless data.bytesize >= HEADER_SIZE &&
|
|
129
|
+
data.byteslice(0, MAGIC.bytesize) == MAGIC
|
|
130
|
+
raise ArgumentError, "data is not a star catalog"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@data = data
|
|
134
|
+
settings = data.unpack("L<#{SETTINGS.size}", offset: MAGIC.bytesize)
|
|
135
|
+
unless settings == SETTINGS
|
|
136
|
+
raise ArgumentError, "star catalog was built with other settings"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
counts_at = MAGIC.bytesize + SETTINGS.size * UNIT_SIZE
|
|
140
|
+
@count, hr_count = data.unpack("L<2", offset: counts_at)
|
|
141
|
+
read_tables(hr_count)
|
|
142
|
+
unless @records_at + @count * RECORD_SIZE == data.bytesize
|
|
143
|
+
raise ArgumentError, "star catalog has the wrong size"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def each_with_hr
|
|
148
|
+
return enum_for(:each_with_hr) unless block_given?
|
|
149
|
+
|
|
150
|
+
@by_hr.each do |index|
|
|
151
|
+
next if index == NO_RECORD
|
|
152
|
+
|
|
153
|
+
yield decode(@data.unpack(FORMAT, offset: record_at(index)))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def each_in_band(band)
|
|
158
|
+
return enum_for(:each_in_band, band) unless block_given?
|
|
159
|
+
|
|
160
|
+
ZONES.times do |zone|
|
|
161
|
+
each_in_cell(band, zone, nil) { |record| yield record }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def each(magnitude_limit: nil, center: nil, radius: nil)
|
|
166
|
+
unless block_given?
|
|
167
|
+
return enum_for(:each, magnitude_limit:, center:,
|
|
168
|
+
radius:)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
search = center && Search.new(center, radius)
|
|
172
|
+
self.class.bands_within(magnitude_limit).each do |band|
|
|
173
|
+
zones(search).each do |zone|
|
|
174
|
+
each_in_cell(band, zone, search) do |record|
|
|
175
|
+
next if magnitude_limit && record.magnitude > magnitude_limit
|
|
176
|
+
|
|
177
|
+
yield record if search.nil? || search.include?(record)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def read_tables(hr_count)
|
|
186
|
+
@offsets = @data.unpack("L<#{OFFSETS_COUNT}", offset: HEADER_SIZE)
|
|
187
|
+
by_hr_at = HEADER_SIZE + OFFSETS_COUNT * UNIT_SIZE
|
|
188
|
+
@by_hr = @data.unpack("L<#{hr_count}", offset: by_hr_at)
|
|
189
|
+
@records_at = by_hr_at + hr_count * UNIT_SIZE
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def zones(search)
|
|
193
|
+
return 0...ZONES unless search
|
|
194
|
+
|
|
195
|
+
search.zones
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def each_in_cell(band, zone, search)
|
|
199
|
+
first, last = cell(band, zone)
|
|
200
|
+
windows = search ? search.windows : [[0, RIGHT_ASCENSION_STEPS - 1]]
|
|
201
|
+
|
|
202
|
+
windows.each do |from, to|
|
|
203
|
+
start = lower_bound(first, last, from)
|
|
204
|
+
index = start
|
|
205
|
+
while index < last
|
|
206
|
+
raw = @data.unpack(FORMAT, offset: record_at(index))
|
|
207
|
+
break if raw[0] > to
|
|
208
|
+
|
|
209
|
+
yield decode(raw)
|
|
210
|
+
index += 1
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def cell(band, zone)
|
|
216
|
+
row = (band - BRIGHTEST_BAND) * ZONES + zone
|
|
217
|
+
|
|
218
|
+
[@offsets[row], @offsets[row + 1]]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def lower_bound(first, last, steps)
|
|
222
|
+
low = first
|
|
223
|
+
high = last
|
|
224
|
+
while low < high
|
|
225
|
+
middle = (low + high) / 2
|
|
226
|
+
right_ascension = @data.unpack1("L<", offset: record_at(middle))
|
|
227
|
+
if right_ascension < steps
|
|
228
|
+
low = middle + 1
|
|
229
|
+
else
|
|
230
|
+
high = middle
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
low
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def record_at(index)
|
|
237
|
+
@records_at + index * RECORD_SIZE
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def decode(raw)
|
|
241
|
+
right_ascension, declination, magnitude, pm_ra, pm_dec, parallax,
|
|
242
|
+
radial_velocity, hr = raw
|
|
243
|
+
|
|
244
|
+
Record.new(
|
|
245
|
+
right_ascension: right_ascension * 360.0 / RIGHT_ASCENSION_STEPS,
|
|
246
|
+
declination: declination * 90.0 / DECLINATION_STEPS,
|
|
247
|
+
magnitude: magnitude / 100.0,
|
|
248
|
+
proper_motion_ra: (pm_ra unless pm_ra == MISSING),
|
|
249
|
+
proper_motion_dec: (pm_dec unless pm_dec == MISSING),
|
|
250
|
+
parallax: (
|
|
251
|
+
parallax.fdiv(PARALLAX_STEPS_PER_MAS) unless parallax.zero?
|
|
252
|
+
),
|
|
253
|
+
radial_velocity: (radial_velocity unless radial_velocity == MISSING),
|
|
254
|
+
hr: (hr unless hr.zero?)
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
class Search
|
|
259
|
+
attr_reader :center, :radius
|
|
260
|
+
|
|
261
|
+
def initialize(center, radius)
|
|
262
|
+
@center = center
|
|
263
|
+
@radius = radius
|
|
264
|
+
@cosine = Math.cos(radius * Math::PI / 180)
|
|
265
|
+
@center_vector = vector(center.first, center.last)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def zones
|
|
269
|
+
lowest = [@center.last - @radius, -90.0].max
|
|
270
|
+
highest = [@center.last + @radius, 90.0].min
|
|
271
|
+
|
|
272
|
+
StarRecords.zone(lowest)..StarRecords.zone(highest)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def windows
|
|
276
|
+
width = half_width
|
|
277
|
+
return [[0, RIGHT_ASCENSION_STEPS - 1]] if width >= 180
|
|
278
|
+
|
|
279
|
+
from = @center.first - width
|
|
280
|
+
to = @center.first + width
|
|
281
|
+
if from.negative?
|
|
282
|
+
[[0, steps(to)], [steps(from + 360), RIGHT_ASCENSION_STEPS - 1]]
|
|
283
|
+
elsif to >= 360
|
|
284
|
+
[[0, steps(to - 360)], [steps(from), RIGHT_ASCENSION_STEPS - 1]]
|
|
285
|
+
else
|
|
286
|
+
[[steps(from), steps(to)]]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def include?(record)
|
|
291
|
+
point = vector(record.right_ascension, record.declination)
|
|
292
|
+
|
|
293
|
+
Geometry.dot(@center_vector, point) >= @cosine
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def half_width
|
|
299
|
+
return 180.0 if @center.last.abs + @radius >= 90
|
|
300
|
+
|
|
301
|
+
Math.asin(
|
|
302
|
+
Math.sin(@radius * Math::PI / 180) /
|
|
303
|
+
Math.cos(@center.last * Math::PI / 180)
|
|
304
|
+
) * 180 / Math::PI
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def steps(degrees)
|
|
308
|
+
(degrees / 360.0 * RIGHT_ASCENSION_STEPS).floor
|
|
309
|
+
.clamp(0, RIGHT_ASCENSION_STEPS - 1)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def vector(right_ascension, declination)
|
|
313
|
+
Geometry.direction(
|
|
314
|
+
right_ascension * Math::PI / 180,
|
|
315
|
+
declination * Math::PI / 180
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
private_constant :Search
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
data/lib/skymap/catalog/stars.rb
CHANGED
|
@@ -1,37 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "csv"
|
|
4
|
-
|
|
5
3
|
module Skymap
|
|
6
4
|
module Catalog
|
|
7
5
|
class Stars
|
|
8
6
|
include Enumerable
|
|
9
7
|
|
|
10
|
-
PATH = File.expand_path("../../../data/stars.
|
|
8
|
+
PATH = File.expand_path("../../../data/stars.bin", __dir__)
|
|
9
|
+
|
|
10
|
+
def self.records
|
|
11
|
+
@records ||= StarRecords.new(File.binread(PATH))
|
|
12
|
+
end
|
|
13
|
+
private_class_method :records
|
|
11
14
|
|
|
12
15
|
def each
|
|
13
16
|
return enum_for(:each) unless block_given?
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
records.each { |record| yield star_from(record) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def search(magnitude_limit: nil, cone: nil)
|
|
22
|
+
return whole_sky(magnitude_limit) unless cone
|
|
23
|
+
|
|
24
|
+
records.each(
|
|
25
|
+
magnitude_limit: magnitude_limit,
|
|
26
|
+
center: [
|
|
27
|
+
cone.center.right_ascension.degrees,
|
|
28
|
+
cone.center.declination.degrees
|
|
29
|
+
],
|
|
30
|
+
radius: cone.radius.degrees
|
|
31
|
+
).map { |record| star_from(record) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def by_hr
|
|
35
|
+
@by_hr ||= Star.by_hr(
|
|
36
|
+
records.each_with_hr.map { |record| star_from(record) }
|
|
37
|
+
)
|
|
18
38
|
end
|
|
19
39
|
|
|
20
40
|
private
|
|
21
41
|
|
|
22
|
-
def
|
|
42
|
+
def records
|
|
43
|
+
self.class.send(:records)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def whole_sky(magnitude_limit)
|
|
47
|
+
stars = StarRecords.bands_within(magnitude_limit).flat_map do |band|
|
|
48
|
+
band_stars(band)
|
|
49
|
+
end
|
|
50
|
+
return stars unless magnitude_limit
|
|
51
|
+
|
|
52
|
+
stars.select { |star| star.magnitude <= magnitude_limit }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def band_stars(band)
|
|
56
|
+
@band_stars ||= {}
|
|
57
|
+
@band_stars[band] ||= records.each_in_band(band)
|
|
58
|
+
.map { |record| star_from(record) }.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def star_from(record)
|
|
23
62
|
Star.new(
|
|
24
|
-
hr:
|
|
63
|
+
hr: record.hr,
|
|
25
64
|
equatorial_coordinates: Astronoby::Coordinates::Equatorial.new(
|
|
26
|
-
right_ascension: Astronoby::Angle.
|
|
27
|
-
|
|
65
|
+
right_ascension: Astronoby::Angle.from_degrees(
|
|
66
|
+
record.right_ascension
|
|
28
67
|
),
|
|
29
|
-
declination: Astronoby::Angle.from_degrees(
|
|
68
|
+
declination: Astronoby::Angle.from_degrees(record.declination),
|
|
30
69
|
epoch: Astronoby::JulianDate::J2000
|
|
31
70
|
),
|
|
32
|
-
magnitude:
|
|
71
|
+
magnitude: record.magnitude,
|
|
72
|
+
proper_motion_ra: proper_motion(record.proper_motion_ra),
|
|
73
|
+
proper_motion_dec: proper_motion(record.proper_motion_dec),
|
|
74
|
+
parallax: parallax(record.parallax),
|
|
75
|
+
radial_velocity: radial_velocity(record.radial_velocity)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def proper_motion(milliarcseconds_per_year)
|
|
80
|
+
return unless milliarcseconds_per_year
|
|
81
|
+
|
|
82
|
+
Astronoby::AngularVelocity.from_milliarcseconds_per_year(
|
|
83
|
+
milliarcseconds_per_year
|
|
33
84
|
)
|
|
34
85
|
end
|
|
86
|
+
|
|
87
|
+
def parallax(milliarcseconds)
|
|
88
|
+
return unless milliarcseconds
|
|
89
|
+
|
|
90
|
+
Astronoby::Angle.from_degree_arcseconds(milliarcseconds / 1000.0)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def radial_velocity(kilometers_per_second)
|
|
94
|
+
return unless kilometers_per_second
|
|
95
|
+
|
|
96
|
+
Astronoby::Velocity.from_kilometers_per_second(kilometers_per_second)
|
|
97
|
+
end
|
|
35
98
|
end
|
|
36
99
|
end
|
|
37
100
|
end
|
data/lib/skymap/chart.rb
CHANGED
|
@@ -2,22 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Skymap
|
|
4
4
|
class Chart
|
|
5
|
-
def initialize(observer:, instant:, canvas:, theme: Theme.dark)
|
|
5
|
+
def initialize(observer:, instant:, canvas:, theme: Theme.dark, view: nil)
|
|
6
6
|
@observer = observer
|
|
7
7
|
@instant = instant
|
|
8
8
|
@canvas = canvas
|
|
9
9
|
@theme = theme
|
|
10
|
+
@view = view
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def render(*layers)
|
|
13
14
|
context = Context.new(
|
|
14
15
|
observer: @observer,
|
|
15
16
|
instant: @instant,
|
|
16
|
-
canvas: @canvas
|
|
17
|
+
canvas: @canvas,
|
|
18
|
+
view: @view
|
|
17
19
|
)
|
|
18
|
-
elements =
|
|
20
|
+
elements = horizon(context) +
|
|
21
|
+
layers.flat_map { |layer| layer.elements(context) }
|
|
19
22
|
|
|
20
|
-
Renderer::SVG.new(canvas: @canvas, theme: @theme
|
|
23
|
+
Renderer::SVG.new(canvas: @canvas, theme: @theme, clipped: !@view.nil?)
|
|
24
|
+
.render(LabelPlacement.new(canvas: @canvas).arrange(elements))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def horizon(context)
|
|
30
|
+
return [] if context.full_sky?
|
|
31
|
+
|
|
32
|
+
Horizon.line(context).map do |points|
|
|
33
|
+
Renderer::HorizonLine.new(points: points)
|
|
34
|
+
end
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
data/lib/skymap/cone.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
Cone = Data.define(:center, :radius) do
|
|
5
|
+
def initialize(center:, radius:)
|
|
6
|
+
unless center.is_a?(Astronoby::Coordinates::Equatorial) &&
|
|
7
|
+
center.epoch == Astronoby::JulianDate::J2000
|
|
8
|
+
raise ArgumentError,
|
|
9
|
+
"center must be Astronoby::Coordinates::Equatorial at J2000"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
unless radius.is_a?(Astronoby::Angle) && radius.positive? &&
|
|
13
|
+
radius.radians <= Math::PI
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"radius must be an Astronoby::Angle above 0 and up to 180 degrees"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|