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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +133 -1
  3. data/README.md +570 -31
  4. data/data/LICENSE-deep_sky_objects.txt +30 -0
  5. data/data/LICENSE-stars.txt +35 -0
  6. data/data/deep_sky_objects.csv +1742 -0
  7. data/data/stars.bin +0 -0
  8. data/lib/skymap/canvas.rb +12 -3
  9. data/lib/skymap/catalog/deep_sky_objects.rb +60 -0
  10. data/lib/skymap/catalog/star_records.rb +322 -0
  11. data/lib/skymap/catalog/stars.rb +75 -12
  12. data/lib/skymap/chart.rb +18 -4
  13. data/lib/skymap/cone.rb +21 -0
  14. data/lib/skymap/context.rb +326 -8
  15. data/lib/skymap/deep_sky_object.rb +140 -0
  16. data/lib/skymap/eyepiece.rb +22 -0
  17. data/lib/skymap/field.rb +20 -0
  18. data/lib/skymap/geometry.rb +47 -0
  19. data/lib/skymap/horizon.rb +33 -3
  20. data/lib/skymap/label_placement.rb +263 -0
  21. data/lib/skymap/layers/cardinal_directions.rb +2 -0
  22. data/lib/skymap/layers/constellation_lines.rb +6 -1
  23. data/lib/skymap/layers/constellation_names.rb +5 -2
  24. data/lib/skymap/layers/deep_sky_objects.rb +128 -0
  25. data/lib/skymap/layers/ecliptic.rb +1 -7
  26. data/lib/skymap/layers/field_circles.rb +169 -0
  27. data/lib/skymap/layers/grid.rb +153 -0
  28. data/lib/skymap/layers/moon.rb +22 -12
  29. data/lib/skymap/layers/planets.rb +4 -0
  30. data/lib/skymap/layers/scale.rb +75 -0
  31. data/lib/skymap/layers/star_names.rb +21 -4
  32. data/lib/skymap/layers/stars.rb +31 -8
  33. data/lib/skymap/layers/sun.rb +13 -9
  34. data/lib/skymap/magnitude_limit.rb +15 -0
  35. data/lib/skymap/print_size.rb +50 -0
  36. data/lib/skymap/projection/oblique_stereographic.rb +87 -0
  37. data/lib/skymap/projection/stereographic.rb +11 -0
  38. data/lib/skymap/renderer/caption.rb +35 -3
  39. data/lib/skymap/renderer/deep_sky_symbol.rb +67 -0
  40. data/lib/skymap/renderer/field_circle.rb +7 -0
  41. data/lib/skymap/renderer/grid.rb +7 -0
  42. data/lib/skymap/renderer/horizon_line.rb +7 -0
  43. data/lib/skymap/renderer/label.rb +3 -1
  44. data/lib/skymap/renderer/scale_bar.rb +7 -0
  45. data/lib/skymap/renderer/svg.rb +204 -15
  46. data/lib/skymap/setup.rb +48 -0
  47. data/lib/skymap/star.rb +127 -2
  48. data/lib/skymap/star_list.rb +25 -0
  49. data/lib/skymap/telescope.rb +17 -0
  50. data/lib/skymap/theme.rb +54 -20
  51. data/lib/skymap/version.rb +1 -1
  52. data/lib/skymap/view.rb +49 -0
  53. data/lib/skymap.rb +24 -0
  54. metadata +31 -2
  55. data/data/stars.csv +0 -9097
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ DeepSkySymbol = Data.define(
6
+ :center,
7
+ :kind,
8
+ :semi_major,
9
+ :semi_minor,
10
+ :rotation,
11
+ :width
12
+ ) do
13
+ def hollow?
14
+ DeepSkySymbol::HOLLOW_KINDS.include?(kind)
15
+ end
16
+
17
+ def outline
18
+ return corners if kind == :nebula
19
+
20
+ steps = outline_steps
21
+
22
+ Array.new(steps) do |step|
23
+ angle = 2 * Math::PI * step / steps
24
+
25
+ at(semi_major * Math.cos(angle), semi_minor * Math.sin(angle))
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def outline_steps
32
+ flatness = [1 - DeepSkySymbol::OUTLINE_TOLERANCE / semi_major, -1].max
33
+
34
+ (Math::PI / Math.acos(flatness)).ceil
35
+ .clamp(DeepSkySymbol::MIN_OUTLINE_STEPS,
36
+ DeepSkySymbol::MAX_OUTLINE_STEPS)
37
+ end
38
+
39
+ def corners
40
+ [[-1, -1], [1, -1], [1, 1], [-1, 1]].map do |sx, sy|
41
+ at(sx * semi_major, sy * semi_minor)
42
+ end
43
+ end
44
+
45
+ def at(dx, dy)
46
+ turn = rotation * Math::PI / 180
47
+
48
+ Point.new(
49
+ x: center.x + dx * Math.cos(turn) - dy * Math.sin(turn),
50
+ y: center.y + dx * Math.sin(turn) + dy * Math.cos(turn)
51
+ )
52
+ end
53
+ end
54
+
55
+ DeepSkySymbol::HOLLOW_KINDS = %i[
56
+ galaxy
57
+ open_cluster
58
+ asterism
59
+ supernova_remnant
60
+ planetary_nebula
61
+ nebula
62
+ ].freeze
63
+ DeepSkySymbol::OUTLINE_TOLERANCE = 0.5
64
+ DeepSkySymbol::MIN_OUTLINE_STEPS = 8
65
+ DeepSkySymbol::MAX_OUTLINE_STEPS = 720
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ FieldCircle = Data.define(:points, :width)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ Grid = Data.define(:points, :width)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ HorizonLine = Data.define(:points)
6
+ end
7
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Skymap
4
4
  module Renderer
5
- Label = Data.define(:position, :text, :size)
5
+ Label = Data.define(:position, :text, :size) do
6
+ include TextWidth
7
+ end
6
8
  end
7
9
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ ScaleBar = Data.define(:from, :to, :tick, :width)
6
+ end
7
+ end
@@ -6,18 +6,26 @@ module Skymap
6
6
  module Renderer
7
7
  class SVG
8
8
  HORIZON_WIDTH = 3
9
+ FRAME_WIDTH = 1
9
10
  BOUNDARY_DASH_RATIO = 4
11
+ DEEP_SKY_DASH_RATIO = 3
12
+ DEEP_SKY_DOT_GAP_RATIO = 2.5
13
+ DEEP_SKY_TICK_RATIO = 0.6
14
+ DEEP_SKY_BAR_RATIO = 1.8
15
+ DEEP_SKY_CORNER_RATIO = 0.3
10
16
 
11
- def initialize(canvas:, theme:)
17
+ def initialize(canvas:, theme:, clipped: false)
12
18
  @canvas = canvas
13
19
  @theme = theme
20
+ @clipped = clipped
14
21
  end
15
22
 
16
23
  def render(elements)
24
+ printed_size = (@canvas.print_size || @canvas.size).to_s
17
25
  svg = Victor::SVG.new(
18
26
  viewBox: "0 0 #{@canvas.size} #{@canvas.size}",
19
- width: @canvas.size,
20
- height: @canvas.size
27
+ width: printed_size,
28
+ height: printed_size
21
29
  )
22
30
 
23
31
  draw_background(svg)
@@ -28,16 +36,51 @@ module Skymap
28
36
  r: @canvas.radius.round(2),
29
37
  fill: @theme.sky,
30
38
  stroke: @theme.horizon,
31
- stroke_width: HORIZON_WIDTH
39
+ stroke_width: @clipped ? FRAME_WIDTH : HORIZON_WIDTH
32
40
  )
33
41
 
42
+ if @clipped
43
+ clip_to_sky(svg)
44
+ svg.g(clip_path: "url(##{clip_id})") { draw_elements(svg, elements) }
45
+ else
46
+ draw_elements(svg, elements)
47
+ end
48
+
49
+ svg.render
50
+ end
51
+
52
+ private
53
+
54
+ def clip_to_sky(svg)
55
+ radius = @canvas.radius.round(2)
56
+ cx = @canvas.center.x.round(2)
57
+ cy = @canvas.center.y.round(2)
58
+
59
+ svg.defs do
60
+ svg.clipPath(id: clip_id) { svg.circle(cx: cx, cy: cy, r: radius) }
61
+ end
62
+ end
63
+
64
+ def clip_id
65
+ geometry = [@canvas.center.x, @canvas.center.y, @canvas.radius]
66
+ .map { |value| value.round(2).to_s.tr(".", "_") }
67
+
68
+ "sky-#{geometry.join("-")}"
69
+ end
70
+
71
+ def draw_elements(svg, elements)
34
72
  elements.each do |element|
35
73
  case element
36
74
  when Boundary then draw_boundary(svg, element)
37
75
  when Caption then draw_caption(svg, element)
38
76
  when Line then draw_line(svg, element)
77
+ when ScaleBar then draw_scale_bar(svg, element)
78
+ when DeepSkySymbol then draw_deep_sky_symbol(svg, element)
39
79
  when Dot then draw_dot(svg, element)
40
80
  when Ecliptic then draw_ecliptic(svg, element)
81
+ when FieldCircle then draw_field_circle(svg, element)
82
+ when Grid then draw_grid(svg, element)
83
+ when HorizonLine then draw_horizon_line(svg, element)
41
84
  when Label then draw_label(svg, element)
42
85
  when Moon then draw_moon(svg, element)
43
86
  when Planet then draw_planet(svg, element)
@@ -45,11 +88,12 @@ module Skymap
45
88
  else raise ArgumentError, "unknown element: #{element.class}"
46
89
  end
47
90
  end
48
-
49
- svg.render
50
91
  end
51
92
 
52
- private
93
+ def format_points(points)
94
+ points.map { |point| "#{point.x.round(2)},#{point.y.round(2)}" }
95
+ .join(" ")
96
+ end
53
97
 
54
98
  def draw_background(svg)
55
99
  return if @theme.background == "none"
@@ -75,13 +119,26 @@ module Skymap
75
119
  )
76
120
  end
77
121
 
122
+ def draw_scale_bar(svg, bar)
123
+ top = bar.from.y - bar.tick
124
+ points = [
125
+ Point.new(x: bar.from.x, y: top), bar.from,
126
+ bar.to, Point.new(x: bar.to.x, y: top)
127
+ ]
128
+
129
+ svg.polyline(
130
+ points: format_points(points),
131
+ fill: "none",
132
+ stroke: @theme.scale,
133
+ stroke_width: bar.width.round(2)
134
+ )
135
+ end
136
+
78
137
  def draw_boundary(svg, boundary)
79
138
  dash = (boundary.width * BOUNDARY_DASH_RATIO).round(2)
80
139
 
81
140
  svg.polyline(
82
- points: boundary.points.map { |point|
83
- "#{point.x.round(2)},#{point.y.round(2)}"
84
- }.join(" "),
141
+ points: format_points(boundary.points),
85
142
  fill: "none",
86
143
  stroke: @theme.boundary,
87
144
  stroke_width: boundary.width.round(2),
@@ -90,13 +147,33 @@ module Skymap
90
147
  end
91
148
 
92
149
  def draw_ecliptic(svg, ecliptic)
150
+ draw_polyline(svg, ecliptic, @theme.ecliptic)
151
+ end
152
+
153
+ def draw_field_circle(svg, circle)
154
+ draw_polyline(svg, circle, @theme.field_circle)
155
+ end
156
+
157
+ def draw_grid(svg, grid)
158
+ draw_polyline(svg, grid, @theme.grid)
159
+ end
160
+
161
+ def draw_polyline(svg, element, stroke)
93
162
  svg.polyline(
94
- points: ecliptic.points.map { |point|
95
- "#{point.x.round(2)},#{point.y.round(2)}"
96
- }.join(" "),
163
+ points: format_points(element.points),
97
164
  fill: "none",
98
- stroke: @theme.ecliptic,
99
- stroke_width: ecliptic.width.round(2),
165
+ stroke: stroke,
166
+ stroke_width: element.width.round(2),
167
+ stroke_linejoin: "round"
168
+ )
169
+ end
170
+
171
+ def draw_horizon_line(svg, line)
172
+ svg.polyline(
173
+ points: format_points(line.points),
174
+ fill: "none",
175
+ stroke: @theme.horizon,
176
+ stroke_width: HORIZON_WIDTH,
100
177
  stroke_linejoin: "round"
101
178
  )
102
179
  end
@@ -155,6 +232,115 @@ module Skymap
155
232
  end
156
233
  end
157
234
 
235
+ def draw_deep_sky_symbol(svg, symbol)
236
+ x = symbol.center.x.round(2)
237
+ y = symbol.center.y.round(2)
238
+ width = symbol.width.round(2)
239
+
240
+ svg.g(
241
+ transform: "rotate(#{symbol.rotation.round(2)} #{x} #{y})",
242
+ fill: "none",
243
+ stroke: @theme.deep_sky,
244
+ stroke_width: width,
245
+ stroke_linejoin: "round"
246
+ ) do
247
+ draw_deep_sky_shape(
248
+ svg,
249
+ symbol.kind,
250
+ x: x,
251
+ y: y,
252
+ rx: symbol.semi_major.round(2),
253
+ ry: symbol.semi_minor.round(2),
254
+ width: width
255
+ )
256
+ end
257
+ end
258
+
259
+ def draw_deep_sky_shape(svg, kind, **shape)
260
+ case kind
261
+ when :galaxy then draw_deep_sky_ellipse(svg, **shape)
262
+ when :open_cluster then draw_deep_sky_ellipse(svg, dashed: true,
263
+ **shape)
264
+ when :asterism then draw_deep_sky_ellipse(svg, dotted: true, **shape)
265
+ when :supernova_remnant then draw_remnant(svg, **shape)
266
+ when :globular_cluster then draw_globular_cluster(svg, **shape)
267
+ when :planetary_nebula then draw_planetary_nebula(svg, **shape)
268
+ when :nebula then draw_nebula(svg, **shape)
269
+ when :double_star then draw_double_star(svg, **shape)
270
+ when :other then draw_cross(svg, **shape)
271
+ else raise ArgumentError, "unknown deep-sky object type: #{kind}"
272
+ end
273
+ end
274
+
275
+ def draw_deep_sky_ellipse(svg, x:, y:, rx:, ry:, width:, **style)
276
+ svg.ellipse(
277
+ cx: x, cy: y, rx: rx, ry: ry, **deep_sky_dashes(width, **style)
278
+ )
279
+ end
280
+
281
+ def deep_sky_dashes(width, dashed: false, dotted: false)
282
+ if dashed
283
+ dash = (width * DEEP_SKY_DASH_RATIO).round(2)
284
+ {stroke_dasharray: "#{dash} #{dash}"}
285
+ elsif dotted
286
+ gap = (width * DEEP_SKY_DOT_GAP_RATIO).round(2)
287
+ {stroke_dasharray: "0 #{gap}", stroke_linecap: "round"}
288
+ else
289
+ {}
290
+ end
291
+ end
292
+
293
+ def draw_remnant(svg, x:, y:, rx:, ry:, width:)
294
+ draw_deep_sky_ellipse(svg, x: x, y: y, rx: rx, ry: ry, width: width,
295
+ dashed: true)
296
+ svg.circle(cx: x, cy: y, r: width, fill: @theme.deep_sky,
297
+ stroke: "none")
298
+ end
299
+
300
+ def draw_globular_cluster(svg, x:, y:, rx:, ry:, width:)
301
+ draw_deep_sky_ellipse(svg, x: x, y: y, rx: rx, ry: ry, width: width)
302
+ draw_cross(svg, x: x, y: y, rx: rx, ry: ry, width: width)
303
+ end
304
+
305
+ def draw_planetary_nebula(svg, x:, y:, rx:, ry:, width:)
306
+ draw_deep_sky_ellipse(svg, x: x, y: y, rx: rx, ry: ry, width: width)
307
+ tick = (DEEP_SKY_TICK_RATIO * [rx, ry].min).round(2)
308
+ [[1, 0], [-1, 0], [0, 1], [0, -1]].each do |dx, dy|
309
+ svg.line(
310
+ x1: (x + dx * rx).round(2),
311
+ y1: (y + dy * ry).round(2),
312
+ x2: (x + dx * (rx + tick)).round(2),
313
+ y2: (y + dy * (ry + tick)).round(2)
314
+ )
315
+ end
316
+ end
317
+
318
+ def draw_nebula(svg, x:, y:, rx:, ry:, width:)
319
+ dash = (width * DEEP_SKY_DASH_RATIO).round(2)
320
+ corner = (DEEP_SKY_CORNER_RATIO * [rx, ry].min).round(2)
321
+
322
+ svg.rect(
323
+ x: (x - rx).round(2),
324
+ y: (y - ry).round(2),
325
+ width: (2 * rx).round(2),
326
+ height: (2 * ry).round(2),
327
+ rx: corner,
328
+ stroke_dasharray: "#{dash} #{dash}"
329
+ )
330
+ end
331
+
332
+ def draw_double_star(svg, x:, y:, rx:, ry:, width:)
333
+ draw_deep_sky_ellipse(svg, x: x, y: y, rx: rx, ry: ry, width: width)
334
+ reach = (DEEP_SKY_BAR_RATIO * rx).round(2)
335
+ svg.line(x1: (x - reach).round(2), y1: y, x2: (x + reach).round(2),
336
+ y2: y)
337
+ end
338
+
339
+ def draw_cross(svg, x:, y:, rx:, ry:, **)
340
+ svg.line(x1: (x - rx).round(2), y1: y, x2: (x + rx).round(2), y2: y)
341
+ svg.line(x1: x, y1: (y - ry).round(2), x2: x, y2: (y + ry).round(2))
342
+ end
343
+
158
344
  def draw_label(svg, label)
159
345
  draw_text(svg, label, @theme.label)
160
346
  end
@@ -168,6 +354,9 @@ module Skymap
168
354
  when :constellation then @theme.constellation_name
169
355
  when :star then @theme.star_name
170
356
  when :planet then @theme.planet_name
357
+ when :field then @theme.field_circle
358
+ when :scale then @theme.scale
359
+ when :deep_sky then @theme.deep_sky
171
360
  else raise ArgumentError, "unknown caption kind: #{kind}"
172
361
  end
173
362
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ Setup = Data.define(:telescope, :eyepiece) do
5
+ def initialize(telescope:, eyepiece:)
6
+ unless telescope.is_a?(Telescope)
7
+ raise ArgumentError, "telescope must be a Skymap::Telescope"
8
+ end
9
+
10
+ unless eyepiece.is_a?(Eyepiece)
11
+ raise ArgumentError, "eyepiece must be a Skymap::Eyepiece"
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ def magnification
18
+ telescope.focal_length_mm.fdiv(eyepiece.focal_length_mm)
19
+ end
20
+
21
+ def true_field
22
+ Astronoby::Angle.from_degrees(
23
+ eyepiece.apparent_field_of_view.degrees / magnification
24
+ )
25
+ end
26
+
27
+ def label
28
+ "#{plain(eyepiece.focal_length_mm.to_f)} mm, " \
29
+ "#{magnification.round}×, #{plain_angle(true_field)}"
30
+ end
31
+
32
+ private
33
+
34
+ def plain_angle(angle)
35
+ seconds = (angle.degrees * 3600).round
36
+ return "#{seconds}″" if seconds < 60
37
+
38
+ minutes = (angle.degrees * 60).round
39
+ return "#{minutes}′" if minutes < 60
40
+
41
+ "#{plain(angle.degrees.round(1))}°"
42
+ end
43
+
44
+ def plain(number)
45
+ number.to_s.delete_suffix(".0")
46
+ end
47
+ end
48
+ end
data/lib/skymap/star.rb CHANGED
@@ -1,9 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skymap
4
- Star = Data.define(:hr, :equatorial_coordinates, :magnitude) do
4
+ Star = Data.define(
5
+ :hr,
6
+ :equatorial_coordinates,
7
+ :magnitude,
8
+ :proper_motion_ra,
9
+ :proper_motion_dec,
10
+ :parallax,
11
+ :radial_velocity
12
+ ) do
5
13
  def self.by_hr(stars)
6
- stars.to_h { |star| [star.hr, star] }
14
+ stars.filter_map { |star| [star.hr, star] if star.hr }.to_h
15
+ end
16
+
17
+ def initialize(
18
+ hr:,
19
+ equatorial_coordinates:,
20
+ magnitude:,
21
+ proper_motion_ra: nil,
22
+ proper_motion_dec: nil,
23
+ parallax: nil,
24
+ radial_velocity: nil
25
+ )
26
+ unless hr?(hr)
27
+ raise ArgumentError, "hr must be a positive Integer or nil"
28
+ end
29
+
30
+ unless equatorial_coordinates.is_a?(Astronoby::Coordinates::Equatorial)
31
+ raise ArgumentError,
32
+ "equatorial_coordinates must be Astronoby::Coordinates::Equatorial"
33
+ end
34
+
35
+ unless magnitude?(magnitude)
36
+ raise ArgumentError, "magnitude must be a finite Numeric"
37
+ end
38
+
39
+ unless proper_motion?(proper_motion_ra)
40
+ raise ArgumentError,
41
+ "proper_motion_ra must be an Astronoby::AngularVelocity or nil"
42
+ end
43
+
44
+ unless proper_motion?(proper_motion_dec)
45
+ raise ArgumentError,
46
+ "proper_motion_dec must be an Astronoby::AngularVelocity or nil"
47
+ end
48
+
49
+ if proper_motion_ra.nil? != proper_motion_dec.nil?
50
+ raise ArgumentError,
51
+ "proper_motion_ra and proper_motion_dec must be given together"
52
+ end
53
+
54
+ if proper_motion_ra &&
55
+ equatorial_coordinates.epoch != Astronoby::JulianDate::J2000
56
+ raise ArgumentError,
57
+ "equatorial_coordinates must be at J2000 when a proper motion " \
58
+ "is given, as it is applied from J2000"
59
+ end
60
+
61
+ unless parallax?(parallax)
62
+ raise ArgumentError,
63
+ "parallax must be an Astronoby::Angle above 0 or nil"
64
+ end
65
+
66
+ unless radial_velocity?(radial_velocity)
67
+ raise ArgumentError,
68
+ "radial_velocity must be an Astronoby::Velocity or nil"
69
+ end
70
+
71
+ super
72
+ end
73
+
74
+ def coordinates_at(instant)
75
+ unless instant.is_a?(Astronoby::Instant)
76
+ raise ArgumentError, "instant must be an Astronoby::Instant"
77
+ end
78
+
79
+ return equatorial_coordinates if proper_motion_ra.nil?
80
+
81
+ propagated = Astronoby::StellarPropagation.equatorial_coordinates_for(
82
+ instant: instant,
83
+ equatorial_coordinates: equatorial_coordinates,
84
+ proper_motion_ra: proper_motion_ra,
85
+ proper_motion_dec: proper_motion_dec,
86
+ parallax: parallax || placeholder_parallax,
87
+ radial_velocity: radial_velocity || placeholder_radial_velocity
88
+ )
89
+
90
+ Astronoby::Coordinates::Equatorial.new(
91
+ right_ascension: propagated.right_ascension,
92
+ declination: propagated.declination,
93
+ epoch: Astronoby::JulianDate::J2000
94
+ )
95
+ end
96
+
97
+ placeholder_parallax = Astronoby::Angle.from_degree_arcseconds(0.001)
98
+ placeholder_radial_velocity =
99
+ Astronoby::Velocity.from_kilometers_per_second(0)
100
+
101
+ private define_method(:placeholder_parallax) { placeholder_parallax }
102
+ private define_method(:placeholder_radial_velocity) {
103
+ placeholder_radial_velocity
104
+ }
105
+
106
+ private
107
+
108
+ def hr?(value)
109
+ value.nil? || (value.is_a?(Integer) && value.positive?)
110
+ end
111
+
112
+ def magnitude?(value)
113
+ value.is_a?(Numeric) && value.real? && value.finite?
114
+ end
115
+
116
+ def proper_motion?(value)
117
+ value.nil? ||
118
+ (value.is_a?(Astronoby::AngularVelocity) &&
119
+ value.radians_per_second.finite?)
120
+ end
121
+
122
+ def parallax?(value)
123
+ value.nil? ||
124
+ (value.is_a?(Astronoby::Angle) &&
125
+ value.radians.finite? && value.positive?)
126
+ end
127
+
128
+ def radial_velocity?(value)
129
+ value.nil? ||
130
+ (value.is_a?(Astronoby::Velocity) &&
131
+ value.meters_per_second.finite?)
7
132
  end
8
133
  end
9
134
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ class StarList
5
+ def self.wrap(stars)
6
+ return stars if stars.respond_to?(:search)
7
+
8
+ new(stars)
9
+ end
10
+
11
+ def initialize(stars)
12
+ @stars = stars
13
+ end
14
+
15
+ def search(magnitude_limit: nil, cone: nil)
16
+ return @stars.to_a unless magnitude_limit
17
+
18
+ @stars.select { |star| star.magnitude <= magnitude_limit }
19
+ end
20
+
21
+ def by_hr
22
+ Star.by_hr(@stars)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ Telescope = Data.define(:focal_length_mm, :aperture_mm) do
5
+ def initialize(focal_length_mm:, aperture_mm:)
6
+ {focal_length_mm: focal_length_mm, aperture_mm: aperture_mm}
7
+ .each do |name, value|
8
+ unless value.is_a?(Numeric) && value.real? && value.finite? &&
9
+ value > 0
10
+ raise ArgumentError, "#{name} must be a number above 0"
11
+ end
12
+ end
13
+
14
+ super
15
+ end
16
+ end
17
+ end