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,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ class LabelPlacement
5
+ RANKS = %i[field planet star deep_sky].freeze
6
+ MARGIN_RATIO = 0.25
7
+ GAP_RATIO = 0.5
8
+ DOT_RATIO = 0.25
9
+ CELL_RATIO = 4
10
+ DIAGONAL = Math.sqrt(0.5)
11
+ DIRECTIONS = [
12
+ [0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [-1, 1], [1, -1], [-1, -1]
13
+ ].freeze
14
+
15
+ Box = Data.define(:left, :top, :right, :bottom) do
16
+ def intersects?(other)
17
+ left < other.right && other.left < right &&
18
+ top < other.bottom && other.top < bottom
19
+ end
20
+
21
+ def bounds
22
+ self
23
+ end
24
+
25
+ def blocks?(other)
26
+ intersects?(other)
27
+ end
28
+
29
+ def reaches?(x, y, radius)
30
+ dx = [left - x, 0, x - right].max
31
+ dy = [top - y, 0, y - bottom].max
32
+
33
+ dx * dx + dy * dy < radius * radius
34
+ end
35
+
36
+ def crossed_by?(from, to)
37
+ dx = to.x - from.x
38
+ dy = to.y - from.y
39
+ entering = 0.0
40
+ leaving = 1.0
41
+
42
+ [
43
+ [-dx, from.x - left], [dx, right - from.x],
44
+ [-dy, from.y - top], [dy, bottom - from.y]
45
+ ].each do |direction, distance|
46
+ if direction.zero?
47
+ return false if distance.negative?
48
+ else
49
+ ratio = distance / direction
50
+ if direction.negative?
51
+ entering = ratio if ratio > entering
52
+ elsif ratio < leaving
53
+ leaving = ratio
54
+ end
55
+ end
56
+ end
57
+
58
+ entering <= leaving
59
+ end
60
+ end
61
+
62
+ Disc = Data.define(:center, :radius) do
63
+ def bounds
64
+ Box.new(
65
+ left: center.x - radius, top: center.y - radius,
66
+ right: center.x + radius, bottom: center.y + radius
67
+ )
68
+ end
69
+
70
+ def blocks?(box)
71
+ box.reaches?(center.x, center.y, radius)
72
+ end
73
+ end
74
+
75
+ Segment = Data.define(:from, :to, :bounds) do
76
+ def self.between(from, to)
77
+ new(
78
+ from: from,
79
+ to: to,
80
+ bounds: Box.new(
81
+ left: [from.x, to.x].min, top: [from.y, to.y].min,
82
+ right: [from.x, to.x].max, bottom: [from.y, to.y].max
83
+ )
84
+ )
85
+ end
86
+
87
+ def blocks?(box)
88
+ bounds.intersects?(box) && box.crossed_by?(from, to)
89
+ end
90
+ end
91
+
92
+ class Index
93
+ def initialize(cell, limit)
94
+ @cell = cell
95
+ @last = (limit / cell).floor
96
+ @cells = {}
97
+ end
98
+
99
+ def add(blocker)
100
+ cells_of(blocker.bounds) { |cell| (@cells[cell] ||= []) << blocker }
101
+ end
102
+
103
+ def blocked?(box)
104
+ cells_of(box) do |cell|
105
+ blockers = @cells[cell]
106
+ return true if blockers&.any? { |blocker| blocker.blocks?(box) }
107
+ end
108
+
109
+ false
110
+ end
111
+
112
+ private
113
+
114
+ def cells_of(box)
115
+ rows = span(box.top, box.bottom)
116
+
117
+ span(box.left, box.right).each do |column|
118
+ rows.each { |row| yield [column, row] }
119
+ end
120
+ end
121
+
122
+ def span(from, to)
123
+ [(from / @cell).floor, 0].max..[(to / @cell).floor, @last].min
124
+ end
125
+ end
126
+ private_constant :Box, :Disc, :Segment, :Index
127
+
128
+ def initialize(canvas:)
129
+ @canvas = canvas
130
+ end
131
+
132
+ def arrange(elements)
133
+ captions = elements.select { |element| anchored?(element) }
134
+ return elements if captions.empty?
135
+
136
+ text_size = captions.map(&:size).min
137
+ index = Index.new(CELL_RATIO * text_size, @canvas.size)
138
+ fixed_boxes(elements).each { |box| index.add(box) }
139
+ obstacles(elements, text_size).each { |obstacle| index.add(obstacle) }
140
+ placed = {}.compare_by_identity
141
+
142
+ by_priority(captions).each do |caption|
143
+ position = free_position(caption, index)
144
+ next unless position
145
+
146
+ index.add(box(caption, position))
147
+ placed[caption] = caption.with(position: position)
148
+ end
149
+
150
+ elements.filter_map do |element|
151
+ anchored?(element) ? placed[element] : element
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def anchored?(element)
158
+ element.is_a?(Renderer::Caption) && !element.anchor.nil?
159
+ end
160
+
161
+ def by_priority(captions)
162
+ captions.each_with_index
163
+ .sort_by { |caption, index| [rank(caption.kind), index] }
164
+ .map(&:first)
165
+ end
166
+
167
+ def rank(kind)
168
+ RANKS.index(kind) || RANKS.size
169
+ end
170
+
171
+ def fixed_boxes(elements)
172
+ elements.filter_map do |element|
173
+ next unless text?(element) && !anchored?(element)
174
+
175
+ box(element, element.position)
176
+ end
177
+ end
178
+
179
+ def text?(element)
180
+ element.is_a?(Renderer::Caption) || element.is_a?(Renderer::Label)
181
+ end
182
+
183
+ def obstacles(elements, text_size)
184
+ bright = DOT_RATIO * text_size
185
+
186
+ elements.flat_map do |element|
187
+ case element
188
+ when Renderer::DeepSkySymbol then symbol(element)
189
+ when Renderer::Planet, Renderer::Moon, Renderer::Sun
190
+ [Disc.new(center: element.center, radius: element.radius)]
191
+ when Renderer::Dot
192
+ next [] if element.radius < bright
193
+
194
+ [Disc.new(center: element.center, radius: element.radius)]
195
+ else
196
+ []
197
+ end
198
+ end
199
+ end
200
+
201
+ def symbol(symbol)
202
+ unless symbol.hollow?
203
+ return [Disc.new(center: symbol.center, radius: symbol.semi_major)]
204
+ end
205
+
206
+ points = symbol.outline
207
+
208
+ points.zip(points.rotate).map { |from, to| Segment.between(from, to) }
209
+ end
210
+
211
+ def free_position(caption, index)
212
+ candidates(caption).find do |position, check_sky|
213
+ !index.blocked?(box(caption, position)) &&
214
+ (!check_sky || inside_sky?(caption, position))
215
+ end&.first
216
+ end
217
+
218
+ def candidates(caption)
219
+ around = DIRECTIONS.map { |direction| [at(caption, direction), true] }
220
+
221
+ [[caption.position, false]] + around
222
+ end
223
+
224
+ def at(caption, direction)
225
+ anchor = caption.anchor
226
+ size = caption.size
227
+ ux, uy = direction
228
+ reach_x = uy.zero? ? caption.clearance : caption.clearance * DIAGONAL
229
+ reach_y = ux.zero? ? caption.clearance : caption.clearance * DIAGONAL
230
+ gap = GAP_RATIO * size
231
+
232
+ Point.new(
233
+ x: anchor.x + ux * (reach_x + gap + caption.width / 2),
234
+ y: anchor.y + uy * (reach_y + size)
235
+ )
236
+ end
237
+
238
+ def inside_sky?(caption, position)
239
+ half_width = caption.width / 2
240
+ half_height = caption.size / 2
241
+
242
+ [-1, 1].product([-1, 1]).all? do |sx, sy|
243
+ Math.hypot(
244
+ position.x + sx * half_width - @canvas.center.x,
245
+ position.y + sy * half_height - @canvas.center.y
246
+ ) <= @canvas.radius
247
+ end
248
+ end
249
+
250
+ def box(text, position)
251
+ margin = MARGIN_RATIO * text.size
252
+ half_width = text.width / 2 + margin
253
+ half_height = text.size / 2 + margin
254
+
255
+ Box.new(
256
+ left: position.x - half_width,
257
+ top: position.y - half_height,
258
+ right: position.x + half_width,
259
+ bottom: position.y + half_height
260
+ )
261
+ end
262
+ end
263
+ end
@@ -7,6 +7,8 @@ module Skymap
7
7
  CARDINAL_SIZE_RATIO = 0.6
8
8
 
9
9
  def elements(context)
10
+ return [] unless context.full_sky?
11
+
10
12
  canvas = context.canvas
11
13
 
12
14
  CARDINAL_DIRECTIONS.map do |text, azimuth|
@@ -11,11 +11,14 @@ module Skymap
11
11
  end
12
12
 
13
13
  def elements(context)
14
- stars_by_hr = Star.by_hr(@stars)
14
+ stars_by_hr = StarList.wrap(@stars).by_hr
15
15
 
16
16
  @lines.filter_map do |line|
17
17
  ends = [line.from, line.to].map { |hrs| stars_by_hr.values_at(*hrs) }
18
18
  next if ends.flatten.include?(nil)
19
+ next unless ends.flatten.all? { |star|
20
+ context.reachable?(context.horizontal(star))
21
+ }
19
22
 
20
23
  directions = ends.map do |end_stars|
21
24
  Horizon.direction(
@@ -34,6 +37,8 @@ module Skymap
34
37
  Point.midpoint(end_stars.map { |star| context.position(star) })
35
38
  end
36
39
  end
40
+ next unless context.crosses_view?([from, to])
41
+
37
42
  Renderer::Line.new(
38
43
  from: from,
39
44
  to: to,
@@ -10,7 +10,7 @@ module Skymap
10
10
  end
11
11
 
12
12
  def elements(context)
13
- stars_by_hr = Star.by_hr(@stars)
13
+ stars_by_hr = StarList.wrap(@stars).by_hr
14
14
 
15
15
  @lines.group_by(&:constellation).filter_map do |abbreviation, lines|
16
16
  name = @names[abbreviation]
@@ -23,8 +23,11 @@ module Skymap
23
23
  next if risen.sum(&:size) * 2 <= parts.sum(&:size)
24
24
 
25
25
  biggest = risen.max_by(&:size)
26
+ position = Point.midpoint(biggest.map { |s| context.position(s) })
27
+ next unless context.in_view?(position)
28
+
26
29
  Renderer::Caption.new(
27
- position: Point.midpoint(biggest.map { |s| context.position(s) }),
30
+ position: position,
28
31
  text: name,
29
32
  size: Renderer::Caption.size_for(context.canvas),
30
33
  kind: :constellation
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Layers
5
+ class DeepSkyObjects
6
+ DEFAULT_MAGNITUDE_LIMIT = 11
7
+ MIN_RADIUS_RATIO = 0.012
8
+ LINE_WIDTH_RATIO = 0.003
9
+
10
+ def initialize(
11
+ objects:,
12
+ magnitude_limit: DEFAULT_MAGNITUDE_LIMIT,
13
+ labels: false
14
+ )
15
+ MagnitudeLimit.check(magnitude_limit)
16
+
17
+ @objects = objects
18
+ @magnitude_limit = magnitude_limit
19
+ @labels = labels
20
+ end
21
+
22
+ def elements(context)
23
+ radius = context.canvas.radius
24
+ minimum = MIN_RADIUS_RATIO * radius
25
+ width = LINE_WIDTH_RATIO * radius
26
+
27
+ brightest_first.flat_map do |object|
28
+ draw(context, object, minimum, width)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def brightest_first
35
+ @objects.each_with_index
36
+ .reject { |object, _| too_faint?(object) }
37
+ .sort_by { |object, index| [object.magnitude || Float::INFINITY, index] }
38
+ .map(&:first)
39
+ end
40
+
41
+ def too_faint?(object)
42
+ object.magnitude && object.magnitude > @magnitude_limit
43
+ end
44
+
45
+ def draw(context, object, minimum, width)
46
+ coordinates = object.equatorial_coordinates
47
+ horizontal = context.horizontal_coordinates(coordinates)
48
+ return [] if horizontal.altitude.negative?
49
+
50
+ center = context.project(
51
+ altitude: horizontal.altitude,
52
+ azimuth: horizontal.azimuth
53
+ )
54
+ semi_major, semi_minor = semi_axes(context, object, horizontal, minimum)
55
+ return [] unless context.in_view?(center, semi_major)
56
+
57
+ drawn = Renderer::DeepSkySymbol.new(
58
+ center: center,
59
+ kind: object.type,
60
+ semi_major: semi_major,
61
+ semi_minor: semi_minor,
62
+ rotation: rotation(context, object),
63
+ width: width
64
+ )
65
+
66
+ [drawn, caption(context, object, center, semi_major)].compact
67
+ end
68
+
69
+ def semi_axes(context, object, horizontal, minimum)
70
+ major, minor = axes(object)
71
+ return [minimum, minimum] unless major
72
+
73
+ pixels = ->(angle) {
74
+ context.span(
75
+ angle,
76
+ altitude: horizontal.altitude,
77
+ azimuth: horizontal.azimuth
78
+ ) / 2
79
+ }
80
+ semi_major = pixels.call(major)
81
+ semi_minor = pixels.call(minor)
82
+ growth = [minimum / semi_major, 1].max
83
+
84
+ [semi_major * growth, semi_minor * growth]
85
+ end
86
+
87
+ def axes(object)
88
+ major = object.major_axis
89
+ return unless major
90
+
91
+ minor = object.minor_axis
92
+ return [major, minor] if round?(object) || object.position_angle
93
+
94
+ equal_area = Astronoby::Angle.from_radians(
95
+ Math.sqrt(major.radians * minor.radians)
96
+ )
97
+
98
+ [equal_area, equal_area]
99
+ end
100
+
101
+ def round?(object)
102
+ object.major_axis == object.minor_axis
103
+ end
104
+
105
+ def rotation(context, object)
106
+ angle = object.position_angle
107
+ return 0.0 if angle.nil? || round?(object)
108
+
109
+ context.rotation_at(
110
+ object.equatorial_coordinates,
111
+ position_angle: angle
112
+ ).degrees
113
+ end
114
+
115
+ def caption(context, object, center, radius)
116
+ return unless @labels
117
+
118
+ Renderer::Caption.beside(
119
+ center: center,
120
+ radius: radius,
121
+ canvas: context.canvas,
122
+ text: object.designation,
123
+ kind: :deep_sky
124
+ )
125
+ end
126
+ end
127
+ end
128
+ end
@@ -17,7 +17,7 @@ module Skymap
17
17
  def elements(context)
18
18
  width = LINE_WIDTH_RATIO * context.canvas.radius
19
19
 
20
- joined(Horizon.visible_runs(context, @points)).map do |points|
20
+ Horizon.joined(Horizon.visible_runs(context, @points)).map do |points|
21
21
  Renderer::Ecliptic.new(points: points, width: width)
22
22
  end
23
23
  end
@@ -33,12 +33,6 @@ module Skymap
33
33
  epoch: Astronoby::JulianDate::J2000
34
34
  )
35
35
  end
36
-
37
- def joined(runs)
38
- return runs unless runs.size > 1 && runs.first.first == runs.last.last
39
-
40
- [runs.last + runs.first.drop(1)] + runs[1...-1]
41
- end
42
36
  end
43
37
  end
44
38
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Layers
5
+ class FieldCircles
6
+ LINE_WIDTH_RATIO = 0.004
7
+ MAX_DEVIATION = 0.1
8
+ MIN_SAMPLES = 16
9
+ MAX_SAMPLES = 360
10
+
11
+ def initialize(fields:, center: nil, labels: false)
12
+ check_fields(fields)
13
+ check_center(center)
14
+
15
+ @fields = fields
16
+ @center = center
17
+ @labels = labels
18
+ end
19
+
20
+ def elements(context)
21
+ center = center_in(context)
22
+ return [] unless center
23
+
24
+ frame = frame_around(center)
25
+ width = LINE_WIDTH_RATIO * context.canvas.radius
26
+
27
+ @fields.flat_map do |field|
28
+ circle(context, center, frame, field, width)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def circle(context, center, frame, field, width)
35
+ radius = Astronoby::Angle.from_radians(field.true_field.radians / 2.0)
36
+ samples = sample_count(context, center, radius)
37
+ path = path_of(context, ring(context, frame, radius, samples))
38
+ runs = Horizon.joined(Horizon.runs_of(context, path))
39
+ return [] if runs.empty?
40
+
41
+ circles = runs.map do |points|
42
+ Renderer::FieldCircle.new(points: points, width: width)
43
+ end
44
+ circles + caption(context, field, path.compact)
45
+ end
46
+
47
+ def sample_count(context, center, radius)
48
+ pixels = context.span(
49
+ radius,
50
+ altitude: center.altitude,
51
+ azimuth: center.azimuth
52
+ )
53
+ step = Math.acos([1 - MAX_DEVIATION / pixels, -1].max)
54
+ return MAX_SAMPLES unless step.positive?
55
+
56
+ samples = (Math::PI / step).ceil.clamp(MIN_SAMPLES, MAX_SAMPLES)
57
+
58
+ samples + -samples % 4
59
+ end
60
+
61
+ def ring(context, frame, radius, samples)
62
+ forward, side, up = frame
63
+ points = (0...samples).map do |index|
64
+ angle = 2 * Math::PI * index / samples
65
+ direction = (0..2).map do |axis|
66
+ radius.cos * forward[axis] +
67
+ radius.sin * (Math.cos(angle) * side[axis] +
68
+ Math.sin(angle) * up[axis])
69
+ end
70
+ Astronoby::Coordinates::Horizontal.new(
71
+ altitude: Astronoby::Angle.from_radians(
72
+ Math.asin(direction[2].clamp(-1, 1))
73
+ ),
74
+ azimuth: Astronoby::Angle.from_radians(
75
+ Math.atan2(direction[1], direction[0])
76
+ ),
77
+ observer: context.observer
78
+ )
79
+ end
80
+
81
+ points + points.first(1)
82
+ end
83
+
84
+ def path_of(context, horizontals)
85
+ [position(context, horizontals.first)] +
86
+ horizontals.each_cons(2).flat_map do |from, to|
87
+ crossing(context, from, to) + [position(context, to)]
88
+ end
89
+ end
90
+
91
+ def crossing(context, from, to)
92
+ return [] unless context.full_sky?
93
+
94
+ Horizon.crossing_point(context, from, to)
95
+ end
96
+
97
+ def position(context, horizontal)
98
+ return if context.full_sky? && horizontal.altitude.negative?
99
+ return unless context.reachable?(horizontal)
100
+
101
+ context.project(
102
+ altitude: horizontal.altitude,
103
+ azimuth: horizontal.azimuth
104
+ )
105
+ end
106
+
107
+ def caption(context, field, points)
108
+ return [] unless @labels
109
+
110
+ xs, ys = points.map(&:x), points.map(&:y)
111
+ caption = Renderer::Caption.beside(
112
+ center: Point.new(
113
+ x: (xs.min + xs.max) / 2.0,
114
+ y: (ys.min + ys.max) / 2.0
115
+ ),
116
+ radius: (ys.max - ys.min) / 2.0,
117
+ canvas: context.canvas,
118
+ text: field.label,
119
+ kind: :field
120
+ )
121
+
122
+ context.in_view?(caption.position) ? [caption] : []
123
+ end
124
+
125
+ def center_in(context)
126
+ center = @center || context.view&.center
127
+ return unless center
128
+
129
+ context.to_horizontal(center)
130
+ end
131
+
132
+ def frame_around(center)
133
+ Geometry.frame(
134
+ [
135
+ center.altitude.cos * center.azimuth.cos,
136
+ center.altitude.cos * center.azimuth.sin,
137
+ center.altitude.sin
138
+ ]
139
+ )
140
+ end
141
+
142
+ def check_fields(fields)
143
+ valid = fields.is_a?(Array) && !fields.empty? &&
144
+ fields.all? { |field| field?(field) }
145
+ return if valid
146
+
147
+ raise ArgumentError,
148
+ "fields must be a list of Skymap::Field or Skymap::Setup, " \
149
+ "or anything with a true_field angle and a label"
150
+ end
151
+
152
+ def field?(field)
153
+ field.respond_to?(:true_field) && field.respond_to?(:label) &&
154
+ field.true_field.is_a?(Astronoby::Angle) &&
155
+ field.true_field.positive? && field.true_field.radians <= Math::PI &&
156
+ field.label.is_a?(String)
157
+ end
158
+
159
+ def check_center(center)
160
+ return if center.nil? ||
161
+ center.is_a?(Astronoby::Coordinates::Equatorial) ||
162
+ center.is_a?(Astronoby::Coordinates::Horizontal)
163
+
164
+ raise ArgumentError,
165
+ "center must be equatorial or horizontal coordinates"
166
+ end
167
+ end
168
+ end
169
+ end