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/lib/skymap/context.rb
CHANGED
|
@@ -2,22 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
module Skymap
|
|
4
4
|
class Context
|
|
5
|
-
|
|
5
|
+
MAX_ANGLE_FROM_VIEW_COSINE = Math.cos(150 * Math::PI / 180)
|
|
6
|
+
STAR_POSITION_TOLERANCE = 0.005
|
|
7
|
+
SHIFT_SLACK = 1e-6
|
|
8
|
+
SECONDS_PER_JULIAN_YEAR = Astronoby::Constants::SECONDS_PER_JULIAN_YEAR
|
|
9
|
+
ASTRONOMICAL_UNIT = 149_597_870_700.0
|
|
10
|
+
SPEED_OF_LIGHT = 299_792_458.0
|
|
11
|
+
PROPER_MOTION_ROUNDING = 1e-15
|
|
12
|
+
POSITION_ANGLE_STEP = 1e-4
|
|
13
|
+
PRECESSION_ARCSECONDS_PER_YEAR = 50.3
|
|
14
|
+
FASTEST_PROPER_MOTION_ARCSECONDS_PER_YEAR = 10.4
|
|
15
|
+
NUTATION_AND_ABERRATION_ARCSECONDS = 60.0
|
|
16
|
+
private_constant :PRECESSION_ARCSECONDS_PER_YEAR,
|
|
17
|
+
:FASTEST_PROPER_MOTION_ARCSECONDS_PER_YEAR,
|
|
18
|
+
:NUTATION_AND_ABERRATION_ARCSECONDS, :STAR_POSITION_TOLERANCE,
|
|
19
|
+
:SHIFT_SLACK, :SECONDS_PER_JULIAN_YEAR, :ASTRONOMICAL_UNIT,
|
|
20
|
+
:SPEED_OF_LIGHT, :PROPER_MOTION_ROUNDING, :POSITION_ANGLE_STEP
|
|
6
21
|
|
|
7
|
-
|
|
22
|
+
attr_reader :observer, :instant, :canvas, :view
|
|
23
|
+
|
|
24
|
+
def initialize(observer:, instant:, canvas:, view: nil)
|
|
8
25
|
@observer = observer
|
|
9
26
|
@instant = instant
|
|
10
27
|
@canvas = canvas
|
|
28
|
+
@view = view
|
|
11
29
|
@transformation = Astronoby::HorizontalTransformation.new(
|
|
12
30
|
observer: observer,
|
|
13
31
|
instant: instant
|
|
14
32
|
)
|
|
15
33
|
@horizontal = {}
|
|
34
|
+
@star_horizontals = {}.compare_by_identity
|
|
35
|
+
@catalog_horizontals = {}.compare_by_identity
|
|
36
|
+
@star_shifts = {}.compare_by_identity
|
|
16
37
|
@projections = {}
|
|
17
38
|
end
|
|
18
39
|
|
|
19
40
|
def horizontal(star)
|
|
20
|
-
|
|
41
|
+
@star_horizontals[star] ||= star_horizontal(star)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def possibly_visible?(star, margin = 0)
|
|
45
|
+
catalog = catalog_horizontal(star)
|
|
46
|
+
shift = star_shift(star) + SHIFT_SLACK
|
|
47
|
+
return false if catalog.altitude.radians < -shift
|
|
48
|
+
return true if full_sky?
|
|
49
|
+
|
|
50
|
+
reach = view_reach(margin) + shift
|
|
51
|
+
return true if reach >= Math::PI
|
|
52
|
+
|
|
53
|
+
cosine_from_view(catalog) >= Math.cos(reach)
|
|
21
54
|
end
|
|
22
55
|
|
|
23
56
|
def horizontal_coordinates(coordinates)
|
|
@@ -27,7 +60,13 @@ module Skymap
|
|
|
27
60
|
coordinates.epoch
|
|
28
61
|
]
|
|
29
62
|
|
|
30
|
-
@horizontal[key] ||=
|
|
63
|
+
@horizontal[key] ||= convert(coordinates)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_horizontal(position)
|
|
67
|
+
return position unless position.is_a?(Astronoby::Coordinates::Equatorial)
|
|
68
|
+
|
|
69
|
+
horizontal_coordinates(position)
|
|
31
70
|
end
|
|
32
71
|
|
|
33
72
|
def position(star)
|
|
@@ -39,13 +78,292 @@ module Skymap
|
|
|
39
78
|
projection(radius).project(altitude: altitude, azimuth: azimuth)
|
|
40
79
|
end
|
|
41
80
|
|
|
81
|
+
def span(angle, altitude:, azimuth:, radius: @canvas.radius)
|
|
82
|
+
projection(radius).scale_at(altitude: altitude, azimuth: azimuth) *
|
|
83
|
+
angle.radians
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rotation_at(coordinates, position_angle:)
|
|
87
|
+
center = project_horizontal(horizontal_coordinates(coordinates))
|
|
88
|
+
toward = project_horizontal(
|
|
89
|
+
convert(step_toward(coordinates, position_angle))
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
Astronoby::Angle.from_radians(
|
|
93
|
+
Math.atan2(toward.y - center.y, toward.x - center.x)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def scale_at_point(point, radius: @canvas.radius)
|
|
98
|
+
projection(radius).scale_at_point(point)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def disk_radius(default, angular_diameter:, altitude:, azimuth:)
|
|
102
|
+
return default if full_sky?
|
|
103
|
+
|
|
104
|
+
[
|
|
105
|
+
default,
|
|
106
|
+
span(angular_diameter, altitude: altitude, azimuth: azimuth) / 2
|
|
107
|
+
].max
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def full_sky?
|
|
111
|
+
@view.nil?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cone
|
|
115
|
+
return if full_sky?
|
|
116
|
+
|
|
117
|
+
@cone ||= Cone.new(
|
|
118
|
+
center: view_equatorial,
|
|
119
|
+
radius: Astronoby::Angle.from_radians(
|
|
120
|
+
[view_reach(StarSize::MAX_RADIUS_RATIO * @canvas.radius) +
|
|
121
|
+
cone_drift, Math::PI].min
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def in_view?(point, margin = 0)
|
|
127
|
+
return true if full_sky?
|
|
128
|
+
|
|
129
|
+
distance = Math.hypot(point.x - @canvas.center.x,
|
|
130
|
+
point.y - @canvas.center.y)
|
|
131
|
+
distance <= @canvas.radius + margin
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def crosses_view?(points)
|
|
135
|
+
return true if full_sky?
|
|
136
|
+
|
|
137
|
+
return in_view?(points.first) if points.size == 1
|
|
138
|
+
|
|
139
|
+
points.each_cons(2).any? do |from, to|
|
|
140
|
+
distance_to_segment(@canvas.center, from, to) <= @canvas.radius
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def reachable?(horizontal)
|
|
145
|
+
return true if full_sky?
|
|
146
|
+
|
|
147
|
+
cosine_from_view(horizontal) > MAX_ANGLE_FROM_VIEW_COSINE
|
|
148
|
+
end
|
|
149
|
+
|
|
42
150
|
private
|
|
43
151
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
152
|
+
def view_equatorial
|
|
153
|
+
center = @view.center
|
|
154
|
+
unless center.is_a?(Astronoby::Coordinates::Horizontal)
|
|
155
|
+
return Astronoby::Coordinates::Equatorial.new(
|
|
156
|
+
right_ascension: center.right_ascension,
|
|
157
|
+
declination: center.declination,
|
|
158
|
+
epoch: Astronoby::JulianDate::J2000
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
latitude = @observer.latitude
|
|
163
|
+
altitude = center.altitude
|
|
164
|
+
azimuth = center.azimuth
|
|
165
|
+
hour_angle = Math.atan2(
|
|
166
|
+
-azimuth.sin * altitude.cos,
|
|
167
|
+
altitude.sin * latitude.cos - altitude.cos * latitude.sin * azimuth.cos
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
Astronoby::Coordinates::Equatorial.new(
|
|
171
|
+
right_ascension: Astronoby::Angle.from_radians(
|
|
172
|
+
(Astronoby::Angle.from_hours(sidereal_time).radians - hour_angle) %
|
|
173
|
+
(2 * Math::PI)
|
|
174
|
+
),
|
|
175
|
+
declination: Astronoby::Angle.from_radians(
|
|
176
|
+
Math.asin(
|
|
177
|
+
(altitude.sin * latitude.sin +
|
|
178
|
+
altitude.cos * latitude.cos * azimuth.cos).clamp(-1, 1)
|
|
179
|
+
)
|
|
180
|
+
),
|
|
181
|
+
epoch: Astronoby::JulianDate::J2000
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def cone_drift
|
|
186
|
+
arcseconds = elapsed_years * (
|
|
187
|
+
PRECESSION_ARCSECONDS_PER_YEAR +
|
|
188
|
+
FASTEST_PROPER_MOTION_ARCSECONDS_PER_YEAR
|
|
189
|
+
) + NUTATION_AND_ABERRATION_ARCSECONDS
|
|
190
|
+
|
|
191
|
+
Astronoby::Angle.from_degree_arcseconds(arcseconds).radians
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def project_horizontal(horizontal)
|
|
195
|
+
project(altitude: horizontal.altitude, azimuth: horizontal.azimuth)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def step_toward(coordinates, position_angle)
|
|
199
|
+
right_ascension = coordinates.right_ascension
|
|
200
|
+
declination = coordinates.declination
|
|
201
|
+
east = [-right_ascension.sin, right_ascension.cos, 0.0]
|
|
202
|
+
north = [
|
|
203
|
+
-declination.sin * right_ascension.cos,
|
|
204
|
+
-declination.sin * right_ascension.sin,
|
|
205
|
+
declination.cos
|
|
206
|
+
]
|
|
207
|
+
point = [
|
|
208
|
+
declination.cos * right_ascension.cos,
|
|
209
|
+
declination.cos * right_ascension.sin,
|
|
210
|
+
declination.sin
|
|
211
|
+
]
|
|
212
|
+
nearby = Geometry.normalize(
|
|
213
|
+
point.each_index.map do |axis|
|
|
214
|
+
point[axis] + POSITION_ANGLE_STEP * (
|
|
215
|
+
position_angle.sin * east[axis] + position_angle.cos * north[axis]
|
|
216
|
+
)
|
|
217
|
+
end
|
|
48
218
|
)
|
|
219
|
+
|
|
220
|
+
Astronoby::Coordinates::Equatorial.new(
|
|
221
|
+
right_ascension: Astronoby::Angle.from_radians(
|
|
222
|
+
Math.atan2(nearby[1], nearby[0]) % (2 * Math::PI)
|
|
223
|
+
),
|
|
224
|
+
declination: Astronoby::Angle.from_radians(
|
|
225
|
+
Math.asin(nearby[2].clamp(-1, 1))
|
|
226
|
+
),
|
|
227
|
+
epoch: coordinates.epoch
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def star_horizontal(star)
|
|
232
|
+
catalog = catalog_horizontal(star)
|
|
233
|
+
shift = star_shift(star)
|
|
234
|
+
return catalog if shift.zero?
|
|
235
|
+
|
|
236
|
+
scale = projection(@canvas.radius)
|
|
237
|
+
.scale_at(altitude: catalog.altitude, azimuth: catalog.azimuth)
|
|
238
|
+
return catalog if shift * scale < STAR_POSITION_TOLERANCE
|
|
239
|
+
|
|
240
|
+
convert(star.coordinates_at(@instant))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def catalog_horizontal(star)
|
|
244
|
+
@catalog_horizontals[star] ||= convert(star.equatorial_coordinates)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def star_shift(star)
|
|
248
|
+
@star_shifts[star] ||= compute_star_shift(star)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def compute_star_shift(star)
|
|
252
|
+
return 0.0 if star.proper_motion_ra.nil? || elapsed_years.zero?
|
|
253
|
+
|
|
254
|
+
rate = Math.hypot(
|
|
255
|
+
proper_motion_bound(star.proper_motion_ra),
|
|
256
|
+
proper_motion_bound(star.proper_motion_dec)
|
|
257
|
+
) * SECONDS_PER_JULIAN_YEAR
|
|
258
|
+
velocity = (star.radial_velocity&.meters_per_second || 0).abs
|
|
259
|
+
seconds = elapsed_years * SECONDS_PER_JULIAN_YEAR
|
|
260
|
+
parallax = star.parallax&.radians || 0
|
|
261
|
+
slack = velocity / SPEED_OF_LIGHT +
|
|
262
|
+
velocity * seconds * parallax / ASTRONOMICAL_UNIT
|
|
263
|
+
return Float::INFINITY if slack >= 0.5
|
|
264
|
+
|
|
265
|
+
rate * elapsed_years / (1 - slack)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def proper_motion_bound(proper_motion)
|
|
269
|
+
rate = proper_motion.radians_per_second
|
|
270
|
+
|
|
271
|
+
rate.negative? ? -rate + PROPER_MOTION_ROUNDING : rate
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def elapsed_years
|
|
275
|
+
@elapsed_years ||= (@instant.tt - Astronoby::JulianDate::J2000).abs /
|
|
276
|
+
Astronoby::Constants::DAYS_PER_JULIAN_YEAR
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def view_reach(margin)
|
|
280
|
+
quarter = @view.field_of_view.radians / 4.0
|
|
281
|
+
|
|
282
|
+
2 * Math.atan(Math.tan(quarter) * (1 + margin / @canvas.radius))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def cosine_from_view(horizontal)
|
|
286
|
+
east, north, up = view_direction
|
|
287
|
+
|
|
288
|
+
horizontal.altitude.cos *
|
|
289
|
+
(horizontal.azimuth.cos * east + horizontal.azimuth.sin * north) +
|
|
290
|
+
horizontal.altitude.sin * up
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def convert(coordinates)
|
|
294
|
+
case coordinates.epoch
|
|
295
|
+
when Astronoby::JulianDate::J2000
|
|
296
|
+
@transformation.apply(coordinates)
|
|
297
|
+
when @instant.tt
|
|
298
|
+
horizontal_of_date(coordinates)
|
|
299
|
+
else
|
|
300
|
+
raise ArgumentError,
|
|
301
|
+
"coordinates must be at J2000 or at the date of the chart"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def horizontal_of_date(coordinates)
|
|
306
|
+
Astronoby::Coordinates::Equatorial.new(
|
|
307
|
+
right_ascension: coordinates.right_ascension,
|
|
308
|
+
declination: coordinates.declination,
|
|
309
|
+
hour_angle: Astronoby::Angle.from_hours(
|
|
310
|
+
(sidereal_time - coordinates.right_ascension.hours) %
|
|
311
|
+
Astronoby::Constants::HOURS_PER_DAY
|
|
312
|
+
),
|
|
313
|
+
epoch: coordinates.epoch
|
|
314
|
+
).to_horizontal(time: @instant.to_time, observer: @observer)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def sidereal_time
|
|
318
|
+
@sidereal_time ||= Astronoby::LocalApparentSiderealTime.from_utc(
|
|
319
|
+
@instant.to_time,
|
|
320
|
+
longitude: @observer.longitude
|
|
321
|
+
).time
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def projection(radius)
|
|
325
|
+
@projections[radius] ||= if full_sky?
|
|
326
|
+
Projection::Stereographic.new(center: @canvas.center, radius: radius)
|
|
327
|
+
else
|
|
328
|
+
center = view_center
|
|
329
|
+
Projection::ObliqueStereographic.new(
|
|
330
|
+
center: @canvas.center,
|
|
331
|
+
radius: radius,
|
|
332
|
+
altitude: center.altitude,
|
|
333
|
+
azimuth: center.azimuth,
|
|
334
|
+
field_of_view: @view.field_of_view,
|
|
335
|
+
orientation: @view.orientation,
|
|
336
|
+
pole: celestial_pole,
|
|
337
|
+
rotation: @view.rotation,
|
|
338
|
+
mirrored: @view.mirrored
|
|
339
|
+
)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def view_center
|
|
344
|
+
@view_center ||= to_horizontal(@view.center)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def celestial_pole
|
|
348
|
+
latitude = @observer.latitude
|
|
349
|
+
|
|
350
|
+
[latitude.cos, 0.0, latitude.sin]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def view_direction
|
|
354
|
+
@view_direction ||= Horizon.direction([view_center])
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def distance_to_segment(point, from, to)
|
|
358
|
+
dx = to.x - from.x
|
|
359
|
+
dy = to.y - from.y
|
|
360
|
+
length = dx * dx + dy * dy
|
|
361
|
+
return Math.hypot(from.x - point.x, from.y - point.y) if length.zero?
|
|
362
|
+
|
|
363
|
+
along = (((point.x - from.x) * dx + (point.y - from.y) * dy) / length)
|
|
364
|
+
.clamp(0, 1)
|
|
365
|
+
|
|
366
|
+
Math.hypot(from.x + along * dx - point.x, from.y + along * dy - point.y)
|
|
49
367
|
end
|
|
50
368
|
end
|
|
51
369
|
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
DeepSkyObject = Data.define(
|
|
5
|
+
:designation,
|
|
6
|
+
:name,
|
|
7
|
+
:type,
|
|
8
|
+
:equatorial_coordinates,
|
|
9
|
+
:magnitude,
|
|
10
|
+
:magnitude_band,
|
|
11
|
+
:major_axis,
|
|
12
|
+
:minor_axis,
|
|
13
|
+
:position_angle
|
|
14
|
+
) do
|
|
15
|
+
def initialize(
|
|
16
|
+
designation:,
|
|
17
|
+
type:,
|
|
18
|
+
equatorial_coordinates:,
|
|
19
|
+
name: nil,
|
|
20
|
+
magnitude: nil,
|
|
21
|
+
magnitude_band: nil,
|
|
22
|
+
major_axis: nil,
|
|
23
|
+
minor_axis: nil,
|
|
24
|
+
position_angle: nil
|
|
25
|
+
)
|
|
26
|
+
unless non_blank_string?(designation)
|
|
27
|
+
raise ArgumentError, "designation must be a non-blank String"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless name.nil? || non_blank_string?(name)
|
|
31
|
+
raise ArgumentError, "name must be a non-blank String or nil"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless DeepSkyObject::TYPES.include?(type)
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"type must be one of #{DeepSkyObject::TYPES.join(", ")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless equatorial_coordinates.is_a?(Astronoby::Coordinates::Equatorial) &&
|
|
40
|
+
equatorial_coordinates.epoch == Astronoby::JulianDate::J2000
|
|
41
|
+
raise ArgumentError,
|
|
42
|
+
"equatorial_coordinates must be Astronoby::Coordinates::Equatorial " \
|
|
43
|
+
"at J2000"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless magnitude?(magnitude)
|
|
47
|
+
raise ArgumentError, "magnitude must be a finite Numeric or nil"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless DeepSkyObject::MAGNITUDE_BANDS.include?(magnitude_band) ||
|
|
51
|
+
magnitude_band.nil?
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"magnitude_band must be one of " \
|
|
54
|
+
"#{DeepSkyObject::MAGNITUDE_BANDS.join(", ")} or nil"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if magnitude_band && magnitude.nil?
|
|
58
|
+
raise ArgumentError, "magnitude_band must come with a magnitude"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless axis?(major_axis)
|
|
62
|
+
raise ArgumentError,
|
|
63
|
+
"major_axis must be an Astronoby::Angle above 0 or nil"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless axis?(minor_axis)
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"minor_axis must be an Astronoby::Angle above 0 or nil"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if minor_axis && major_axis.nil?
|
|
72
|
+
raise ArgumentError, "minor_axis must come with a major_axis"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if minor_axis && minor_axis > major_axis
|
|
76
|
+
raise ArgumentError, "minor_axis must not be larger than major_axis"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless position_angle?(position_angle)
|
|
80
|
+
raise ArgumentError, "position_angle must be an Astronoby::Angle or nil"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def messier?
|
|
87
|
+
designation.match?(/\AM\d+\z/)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
alias_method :given_minor_axis, :minor_axis
|
|
91
|
+
private :given_minor_axis
|
|
92
|
+
|
|
93
|
+
def minor_axis
|
|
94
|
+
given_minor_axis || major_axis
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
alias_method :given_magnitude_band, :magnitude_band
|
|
98
|
+
private :given_magnitude_band
|
|
99
|
+
|
|
100
|
+
def magnitude_band
|
|
101
|
+
given_magnitude_band || (:visual unless magnitude.nil?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def non_blank_string?(value)
|
|
107
|
+
value.is_a?(String) && !value.strip.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def magnitude?(value)
|
|
111
|
+
value.nil? ||
|
|
112
|
+
(value.is_a?(Numeric) && value.real? && value.finite?)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def position_angle?(value)
|
|
116
|
+
value.nil? ||
|
|
117
|
+
(value.is_a?(Astronoby::Angle) && value.radians.finite?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def axis?(value)
|
|
121
|
+
value.nil? ||
|
|
122
|
+
(value.is_a?(Astronoby::Angle) &&
|
|
123
|
+
value.radians.finite? && value.positive?)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
DeepSkyObject::MAGNITUDE_BANDS = %i[visual blue].freeze
|
|
128
|
+
|
|
129
|
+
DeepSkyObject::TYPES = %i[
|
|
130
|
+
galaxy
|
|
131
|
+
open_cluster
|
|
132
|
+
globular_cluster
|
|
133
|
+
planetary_nebula
|
|
134
|
+
nebula
|
|
135
|
+
supernova_remnant
|
|
136
|
+
asterism
|
|
137
|
+
double_star
|
|
138
|
+
other
|
|
139
|
+
].freeze
|
|
140
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
Eyepiece = Data.define(:focal_length_mm, :apparent_field_of_view) do
|
|
5
|
+
def initialize(focal_length_mm:, apparent_field_of_view:)
|
|
6
|
+
unless focal_length_mm.is_a?(Numeric) && focal_length_mm.real? &&
|
|
7
|
+
focal_length_mm.finite? && focal_length_mm > 0
|
|
8
|
+
raise ArgumentError, "focal_length_mm must be a number above 0"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless apparent_field_of_view.is_a?(Astronoby::Angle) &&
|
|
12
|
+
apparent_field_of_view.positive? &&
|
|
13
|
+
apparent_field_of_view.radians <= Math::PI
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"apparent_field_of_view must be an Astronoby::Angle " \
|
|
16
|
+
"above 0 and up to 180 degrees"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/skymap/field.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
Field = Data.define(:true_field, :label) do
|
|
5
|
+
def initialize(true_field:, label:)
|
|
6
|
+
unless true_field.is_a?(Astronoby::Angle) && true_field.positive? &&
|
|
7
|
+
true_field.radians <= Math::PI
|
|
8
|
+
raise ArgumentError,
|
|
9
|
+
"true_field must be an Astronoby::Angle " \
|
|
10
|
+
"above 0 and up to 180 degrees"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
unless label.is_a?(String) && !label.strip.empty?
|
|
14
|
+
raise ArgumentError, "label must be a string that is not blank"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Geometry
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def dot(a, b)
|
|
8
|
+
a.zip(b).sum { |x, y| x * y }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def direction(right_ascension, declination)
|
|
12
|
+
[
|
|
13
|
+
Math.cos(declination) * Math.cos(right_ascension),
|
|
14
|
+
Math.cos(declination) * Math.sin(right_ascension),
|
|
15
|
+
Math.sin(declination)
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cross(a, b)
|
|
20
|
+
[
|
|
21
|
+
a[1] * b[2] - a[2] * b[1],
|
|
22
|
+
a[2] * b[0] - a[0] * b[2],
|
|
23
|
+
a[0] * b[1] - a[1] * b[0]
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def normalize(vector)
|
|
28
|
+
length = Math.sqrt(dot(vector, vector))
|
|
29
|
+
|
|
30
|
+
vector.map { |component| component / length }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def away_from(reference, forward)
|
|
34
|
+
along = dot(reference, forward)
|
|
35
|
+
|
|
36
|
+
reference.zip(forward).map { |r, f| r - along * f }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def frame(forward)
|
|
40
|
+
axis = (0..2).min_by { |index| forward[index].abs }
|
|
41
|
+
reference = Array.new(3) { |index| (index == axis) ? 1.0 : 0.0 }
|
|
42
|
+
up = normalize(away_from(reference, forward))
|
|
43
|
+
|
|
44
|
+
[forward, cross(up, forward), up]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/skymap/horizon.rb
CHANGED
|
@@ -38,13 +38,43 @@ module Skymap
|
|
|
38
38
|
crossing_point(context, from, to) + [position(context, to)]
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
runs_of(context, path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def line(context)
|
|
45
|
+
azimuths = (0...360).map do |degrees|
|
|
46
|
+
Astronoby::Coordinates::Horizontal.new(
|
|
47
|
+
altitude: Astronoby::Angle.zero,
|
|
48
|
+
azimuth: Astronoby::Angle.from_degrees(degrees),
|
|
49
|
+
observer: context.observer
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
path = (azimuths + azimuths.first(1)).map do |horizontal|
|
|
53
|
+
next unless context.reachable?(horizontal)
|
|
54
|
+
|
|
55
|
+
context.project(
|
|
56
|
+
altitude: horizontal.altitude,
|
|
57
|
+
azimuth: horizontal.azimuth
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
joined(runs_of(context, path))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def joined(runs)
|
|
65
|
+
return runs unless runs.size > 1 && runs.first.first == runs.last.last
|
|
66
|
+
|
|
67
|
+
[runs.last + runs.first.drop(1)] + runs[1...-1]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def runs_of(context, path)
|
|
41
71
|
path.chunk { |point| !point.nil? }.filter_map do |visible, points|
|
|
42
|
-
points if visible && points.size > 1
|
|
72
|
+
points if visible && points.size > 1 && context.crosses_view?(points)
|
|
43
73
|
end
|
|
44
74
|
end
|
|
45
75
|
|
|
46
76
|
def position(context, horizontal)
|
|
47
|
-
return if below?(horizontal)
|
|
77
|
+
return if below?(horizontal) || !context.reachable?(horizontal)
|
|
48
78
|
|
|
49
79
|
context.project(
|
|
50
80
|
altitude: horizontal.altitude,
|
|
@@ -68,6 +98,6 @@ module Skymap
|
|
|
68
98
|
horizontal.altitude.negative?
|
|
69
99
|
end
|
|
70
100
|
|
|
71
|
-
private_class_method :position, :
|
|
101
|
+
private_class_method :position, :below?
|
|
72
102
|
end
|
|
73
103
|
end
|