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
|
@@ -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
|
data/lib/skymap/renderer/svg.rb
CHANGED
|
@@ -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:
|
|
20
|
-
height:
|
|
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
|
-
|
|
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
|
|
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:
|
|
95
|
-
"#{point.x.round(2)},#{point.y.round(2)}"
|
|
96
|
-
}.join(" "),
|
|
163
|
+
points: format_points(element.points),
|
|
97
164
|
fill: "none",
|
|
98
|
-
stroke:
|
|
99
|
-
stroke_width:
|
|
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
|
data/lib/skymap/setup.rb
ADDED
|
@@ -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(
|
|
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.
|
|
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
|