voruby 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. data/LICENSE +339 -0
  2. data/REQUIREMENTS +4 -0
  3. data/Rakefile.rb +296 -0
  4. data/lib/voruby/adql/adql.rb +2735 -0
  5. data/lib/voruby/adql/ext.rb +15 -0
  6. data/lib/voruby/adql/loader.rb +5 -0
  7. data/lib/voruby/adql/operations.rb +54 -0
  8. data/lib/voruby/adql/parser.rb +160 -0
  9. data/lib/voruby/adql/transforms.rb +573 -0
  10. data/lib/voruby/ext.rb +17 -0
  11. data/lib/voruby/loader.rb +4 -0
  12. data/lib/voruby/misc/propertyfile.rb +36 -0
  13. data/lib/voruby/plastic/applications.rb +174 -0
  14. data/lib/voruby/plastic/constants.rb +30 -0
  15. data/lib/voruby/plastic/loader.rb +10 -0
  16. data/lib/voruby/plastic/plastic.rb +1 -0
  17. data/lib/voruby/resources/conesearch/conesearch.rb +9 -0
  18. data/lib/voruby/resources/conesearch/conesearch_v0_2.rb +55 -0
  19. data/lib/voruby/resources/conesearch/conesearch_v0_3.rb +50 -0
  20. data/lib/voruby/resources/conesearch/conesearch_v1_0.rb +72 -0
  21. data/lib/voruby/resources/conesearch/loader.rb +4 -0
  22. data/lib/voruby/resources/loader.rb +50 -0
  23. data/lib/voruby/resources/nodes.rb +190 -0
  24. data/lib/voruby/resources/openskynode/loader.rb +4 -0
  25. data/lib/voruby/resources/openskynode/openskynode.rb +9 -0
  26. data/lib/voruby/resources/openskynode/openskynode_v0_1.rb +54 -0
  27. data/lib/voruby/resources/sia/loader.rb +5 -0
  28. data/lib/voruby/resources/sia/sia.rb +9 -0
  29. data/lib/voruby/resources/sia/sia_v0_6.rb +90 -0
  30. data/lib/voruby/resources/sia/sia_v0_7.rb +89 -0
  31. data/lib/voruby/resources/sia/sia_v1_0.rb +122 -0
  32. data/lib/voruby/resources/stsci.rb +59 -0
  33. data/lib/voruby/resources/vodataservice/coverage_v0_2.rb +195 -0
  34. data/lib/voruby/resources/vodataservice/coverage_v0_3.rb +158 -0
  35. data/lib/voruby/resources/vodataservice/loader.rb +5 -0
  36. data/lib/voruby/resources/vodataservice/vodataservice.rb +9 -0
  37. data/lib/voruby/resources/vodataservice/vodataservice_v0_4.rb +189 -0
  38. data/lib/voruby/resources/vodataservice/vodataservice_v0_5.rb +163 -0
  39. data/lib/voruby/resources/vodataservice/vodataservice_v1_0.rb +221 -0
  40. data/lib/voruby/resources/voregistry/loader.rb +4 -0
  41. data/lib/voruby/resources/voregistry/voregistry.rb +9 -0
  42. data/lib/voruby/resources/voregistry/voregistry_v0_2.rb +40 -0
  43. data/lib/voruby/resources/voregistry/voregistry_v0_3.rb +30 -0
  44. data/lib/voruby/resources/voregistry/voregistry_v1_0.rb +86 -0
  45. data/lib/voruby/resources/voresource/loader.rb +17 -0
  46. data/lib/voruby/resources/voresource/voresource.rb +9 -0
  47. data/lib/voruby/resources/voresource/voresource_v0_10.rb +322 -0
  48. data/lib/voruby/resources/voresource/voresource_v0_9.rb +405 -0
  49. data/lib/voruby/resources/voresource/voresource_v1_0.rb +230 -0
  50. data/lib/voruby/services/ext.rb +11 -0
  51. data/lib/voruby/services/gestalt/footprint.rb +95 -0
  52. data/lib/voruby/services/gestalt/wcs_fixer.rb +105 -0
  53. data/lib/voruby/services/gestalt/wesix.rb +155 -0
  54. data/lib/voruby/services/loader.rb +7 -0
  55. data/lib/voruby/services/registry/registry.rb +53 -0
  56. data/lib/voruby/services/resolver/resolver.rb +35 -0
  57. data/lib/voruby/sesame/loader.rb +6 -0
  58. data/lib/voruby/sesame/sesame_v1_0.rb +64 -0
  59. data/lib/voruby/simple/loader.rb +6 -0
  60. data/lib/voruby/simple/parameters.rb +196 -0
  61. data/lib/voruby/simple/sap.rb +446 -0
  62. data/lib/voruby/spacetime/loader.rb +3 -0
  63. data/lib/voruby/spacetime/spacetime.rb +607 -0
  64. data/lib/voruby/stc/coords_v1_20.rb +900 -0
  65. data/lib/voruby/stc/loader.rb +55 -0
  66. data/lib/voruby/stc/region_v1_20.rb +274 -0
  67. data/lib/voruby/stc/stc_v1_20.rb +1196 -0
  68. data/lib/voruby/util.rb +27 -0
  69. data/lib/voruby/voevent/loader.rb +7 -0
  70. data/lib/voruby/voevent/voevent_v1_0.rb +213 -0
  71. data/lib/voruby/voevent/voevent_v1_1.rb +196 -0
  72. data/lib/voruby/votables/chandra.rb +410 -0
  73. data/lib/voruby/votables/cnoc.rb +393 -0
  74. data/lib/voruby/votables/data.rb +179 -0
  75. data/lib/voruby/votables/galex.rb +390 -0
  76. data/lib/voruby/votables/hst.rb +391 -0
  77. data/lib/voruby/votables/int.rb +391 -0
  78. data/lib/voruby/votables/libxml_parser.rb +411 -0
  79. data/lib/voruby/votables/libxml_votable.rb +67 -0
  80. data/lib/voruby/votables/loader.rb +10 -0
  81. data/lib/voruby/votables/meta.rb +763 -0
  82. data/lib/voruby/votables/misc.rb +51 -0
  83. data/lib/voruby/votables/nsa.rb +393 -0
  84. data/lib/voruby/votables/nsar3.rb +410 -0
  85. data/lib/voruby/votables/rexml_parser.rb +408 -0
  86. data/lib/voruby/votables/rexml_votable.rb +67 -0
  87. data/lib/voruby/votables/sdss.rb +393 -0
  88. data/lib/voruby/votables/transforms.rb +388 -0
  89. data/lib/voruby/votables/tree.rb +45 -0
  90. data/lib/voruby/votables/types.rb +391 -0
  91. data/lib/voruby/votables/votable.rb +630 -0
  92. data/lib/voruby/votables/xmm.rb +394 -0
  93. data/test/adql/test1.adql +49 -0
  94. data/test/adql/test2.adql +51 -0
  95. data/test/adql/test3.adql +81 -0
  96. data/test/adql/test4.adql +53 -0
  97. data/test/adql/test5.adql +55 -0
  98. data/test/adql/test6.adql +18 -0
  99. data/test/adql/test7.adql +48 -0
  100. data/test/adql/unittest.rb +1672 -0
  101. data/test/plastic/test.rb +44 -0
  102. data/test/plastic/test.vot +5385 -0
  103. data/test/plastic/unittest.rb +66 -0
  104. data/test/resources/conesearch/conesearch_v0_3.xml +31 -0
  105. data/test/resources/conesearch/conesearch_v1_0.xml +86 -0
  106. data/test/resources/conesearch/unittest_v0_3.rb +22 -0
  107. data/test/resources/conesearch/unittest_v1_0.rb +24 -0
  108. data/test/resources/openskynode/open_sky_node_v0_1.xml +32 -0
  109. data/test/resources/openskynode/unittest_v0_1.rb +31 -0
  110. data/test/resources/sia/simple_image_access_v0_7.xml +36 -0
  111. data/test/resources/sia/simple_image_access_v1_0.xml +122 -0
  112. data/test/resources/sia/unittest_v0_7.rb +24 -0
  113. data/test/resources/sia/unittest_v1_0.rb +29 -0
  114. data/test/resources/stsci.xml +336 -0
  115. data/test/resources/unittest_stsci.rb +25 -0
  116. data/test/resources/vodataservice/catalog_service_resource_v1_0.xml +128 -0
  117. data/test/resources/vodataservice/data_collection_resource_v0_5.xml +54 -0
  118. data/test/resources/vodataservice/data_collection_resource_v1_0.xml +117 -0
  119. data/test/resources/vodataservice/data_service_resource_v1_0.xml +115 -0
  120. data/test/resources/vodataservice/sky_service_resource_v0_10.xml +45 -0
  121. data/test/resources/vodataservice/table_service_resource_v1_0.xml +122 -0
  122. data/test/resources/vodataservice/tabular_sky_service_resource_v0_10.xml +60 -0
  123. data/test/resources/vodataservice/unittest_v0_5.rb +126 -0
  124. data/test/resources/vodataservice/unittest_v1_0.rb +151 -0
  125. data/test/resources/voregistry/authority_resource_v0_3.xml +20 -0
  126. data/test/resources/voregistry/authority_resource_v1_0.xml +82 -0
  127. data/test/resources/voregistry/registry_service_v0_3.xml +20 -0
  128. data/test/resources/voregistry/registry_service_v1_0.xml +107 -0
  129. data/test/resources/voregistry/unittest_v0_3.rb +31 -0
  130. data/test/resources/voregistry/unittest_v1_0.rb +34 -0
  131. data/test/resources/voresource/organisation_resource_v1_0.xml +90 -0
  132. data/test/resources/voresource/resource_organisation_v0_10.xml +22 -0
  133. data/test/resources/voresource/resource_service_v0_10.xml +19 -0
  134. data/test/resources/voresource/resource_v0_10.xml +19 -0
  135. data/test/resources/voresource/resource_v1_0.xml +79 -0
  136. data/test/resources/voresource/service_resource_v1_0.xml +91 -0
  137. data/test/resources/voresource/unittest_v0_10.rb +61 -0
  138. data/test/resources/voresource/unittest_v0_9.rb +4 -0
  139. data/test/resources/voresource/unittest_v1_0.rb +190 -0
  140. data/test/services/gestalt/unittest.rb +74 -0
  141. data/test/services/registry/unittest.rb +34 -0
  142. data/test/services/resolver/unittest.rb +38 -0
  143. data/test/simple/unittest.rb +46 -0
  144. data/test/spacetime/unittest.rb +39 -0
  145. data/test/stc/catalog_entry_location_v1_20.xml +112 -0
  146. data/test/stc/obs_data_location_v1_20.xml +108 -0
  147. data/test/stc/search_location_v1_20.xml +54 -0
  148. data/test/stc/stc_resource_profile_v1_20.xml +60 -0
  149. data/test/stc/unittest_v1_20.rb +620 -0
  150. data/test/voevent/unittest_v1_0.rb +79 -0
  151. data/test/voevent/unittest_v1_1.rb +70 -0
  152. data/test/voevent/voevent_v1_0.xml +96 -0
  153. data/test/voevent/voevent_v1_1.xml +76 -0
  154. data/test/votables/test.vot +366 -0
  155. data/test/votables/unittest.rb +54 -0
  156. metadata +256 -0
@@ -0,0 +1,607 @@
1
+ require 'voruby/spacetime/loader'
2
+
3
+ module VORuby
4
+
5
+ module SpaceTime
6
+
7
+ class SpatialPosition
8
+ attr_accessor :axis1, :axis2, :axis3
9
+
10
+ def initialize(x, y, z)
11
+ self.axis1 = x
12
+ self.axis2 = y
13
+ self.axis3 = z
14
+ end
15
+ end
16
+ class TemporalPosition
17
+ attr_accessor :time
18
+
19
+ def initialize(t)
20
+ self.time = t
21
+ end
22
+ end
23
+
24
+ class Event
25
+ attr_accessor :spatial_position, :temporal_position
26
+
27
+ # Create a space-time even with the given spatial and temporal parts.
28
+ def initialize(spatial, temporal)
29
+ raise Exception::NotASpatialPosition.new(spatial) if !spatial.is_a?(SpatialPosition)
30
+ raise Exception::NotATemporalPosition.new(temporal) if !temporal.is_a?(TemporalPosition)
31
+
32
+ self.spatial_position = spatial
33
+ self.temporal_position = temporal
34
+ end
35
+
36
+ def to_s
37
+ "#{self.spatial_position}, #{self.temporal_position}"
38
+ end
39
+ end
40
+
41
+ module Exception
42
+ class NotASpatialPosition < RuntimeError
43
+ def initialize(value)
44
+ super("#{value} is not a SpatialPosition")
45
+ end
46
+ end
47
+
48
+ class NotATemporalPosition < RuntimeError
49
+ def initialize(value)
50
+ super("#{value} is not a TemporalPosition")
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ module CoordinateSystems
58
+
59
+ class CoordinateOutOfRangeException < RuntimeError
60
+ def initialize(coord_name, value, start_range, end_range)
61
+ super("#{coord_name} of #{value} out of allowed range. Must be between #{start_range} and #{end_range}.")
62
+ end
63
+ end
64
+
65
+ class NotACoordinateException < RuntimeError
66
+ def initialize(value, intended_name)
67
+ super("'#{value}' (#{value.class}) is not (or can't be converted to) a #{intended_name} object.")
68
+ end
69
+ end
70
+
71
+ class NotATimeException < NotACoordinateException
72
+ def initialize(value)
73
+ super(value, 'Time')
74
+ end
75
+ end
76
+
77
+ class InvalidTimeOffsetException < RuntimeError
78
+ def initialize(value)
79
+ super("Offset '#{value}' not recognized. Try a number with s, m, h or d appended.")
80
+ end
81
+ end
82
+
83
+ class StandardTime < SpaceTime::TemporalPosition
84
+ attr_reader :value
85
+
86
+ # Create a StandardTime object from Ruby's core Time object.
87
+ def initialize(time)
88
+ self.value = time
89
+
90
+ super(self.value)
91
+ end
92
+
93
+ def value=(time)
94
+ raise NotATimeException.new(time) if !time.is_a?(Time)
95
+ @value = time
96
+ end
97
+
98
+ # Returns an ISO 8601 compliant string representing the UTC time.
99
+ def to_utc_s
100
+ self.value.getutc.strftime("%Y-%m-%dT%H:%M:%SZ")
101
+ end
102
+
103
+ # Returns an ISO 8601 compliant string representing the local time.
104
+ def to_local_s
105
+ utc_offset_hrs = self.value.getlocal.utc_offset / (60.0 * 60.0)
106
+ utc_offset_min = ((utc_offset_hrs - utc_offset_hrs.floor) * 60.0).to_i
107
+ utc_offset = sprintf("%+03d:%02d", utc_offset_hrs.floor, utc_offset_min)
108
+
109
+ self.value.getlocal.strftime("%Y-%m-%dT%H:%M:%S#{utc_offset}")
110
+ end
111
+
112
+ def extract_time(time_s)
113
+ matches = time_s.match(/^(\d+(\.\d+)?)([smhd])?$/)
114
+ raise InvalidTimeOffsetException.new(time_s) if !matches
115
+
116
+ return calculate_time(matches[1].to_f, matches[3])
117
+ end
118
+
119
+ def calculate_time(value, unit)
120
+ case unit
121
+ when 's' then return value
122
+ when 'm' then return value * 60.0
123
+ when 'h' then return value * 60.0 * 60.0
124
+ when 'd' then return value * 24.0 * 60.0 * 60.0
125
+ else return value
126
+ end
127
+ end
128
+
129
+ # Returns a StandardTime object projected forward the specified
130
+ # amount of time. Input maybe be:
131
+ # 1) a decimal number of seconds
132
+ # 2) a string with a decimal number followed by a modifier specifying the unit.
133
+ # The unit may be s = seconds, m = minutes, h = hours, d = days
134
+ # i.e. 12.3h for 12.3 hours
135
+ def +(time_s)
136
+ new_time = nil
137
+ (time_s.kind_of?(Numeric))? new_time = self.value + time_s: new_time = self.value + extract_time(time_s)
138
+ return StandardTime.new(new_time)
139
+ end
140
+
141
+ # Add the specified number of seconds to the time.
142
+ def increment(seconds)
143
+ self.value = (self + seconds).value
144
+ end
145
+
146
+ # Returns a StandardTime object projected backward the specified
147
+ # amount of time. Input maybe be:
148
+ # 1) a decimal number of seconds
149
+ # 2) a string with a decimal number followed by a modifier specifying the unit.
150
+ # The unit may be s = seconds, m = minutes, h = hours, d = days
151
+ # i.e. '12.3h' for 12.3 hours
152
+ def -(time_s)
153
+ new_time = nil
154
+ (time_s.kind_of?(Numeric))? new_time = self.value + time_s: new_time = self.value - extract_time(time_s)
155
+ return StandardTime.new(new_time)
156
+ end
157
+
158
+ # Subtract the specified number of seconds to the time.
159
+ def decrement(seconds)
160
+ self.value = (self - seconds).value
161
+ end
162
+
163
+ # Compare two StandardTimes.
164
+ # if timeA before timeB, returns -1
165
+ # if timeA == timeB, returns 0
166
+ # if timeA after timeB, returns 1
167
+ def <=>(time)
168
+ self.value <=> time.value
169
+ end
170
+
171
+ def ==(time)
172
+ self.value.eql?(time.value)
173
+ end
174
+
175
+ def to_s
176
+ self.to_utc_s
177
+ end
178
+
179
+ private :extract_time, :calculate_time
180
+ end
181
+
182
+ module Equatorial
183
+
184
+ module Exception
185
+ class RAOutOfRange < CoordinateOutOfRangeException
186
+ def initialize(value)
187
+ super("Right ascension", value, 0, 360)
188
+ end
189
+ end
190
+
191
+ class DecOutOfRange < CoordinateOutOfRangeException
192
+ def initialize(value)
193
+ super("Declination", value, -90, 90)
194
+ end
195
+ end
196
+
197
+ class InvalidSexigesimalFormat < RuntimeError
198
+ def initialize(value)
199
+ super("Sexigesimal value of #{value} in a format that is not understood. Try NN:NN:NN.NN.")
200
+ end
201
+ end
202
+
203
+ class NotARightAscension < NotACoordinateException
204
+ def initialize(value)
205
+ super(value, 'RightAscension')
206
+ end
207
+ end
208
+
209
+ class NotADeclination < NotACoordinateException
210
+ def initialize(value)
211
+ super(value, 'Declination')
212
+ end
213
+ end
214
+ end
215
+
216
+ # Returns true if the provided string looks like it's
217
+ # in standard sexigesimal format.
218
+ def self.looks_like_sexigesimal?(sexig, sep='(:|(\s)+)')
219
+ return true if sexig.match(/^[+-]?\d+#{sep}\d+#{sep}\d+/)
220
+ return false
221
+ end
222
+
223
+ # Returns true if the provided string looks like it's
224
+ # in standard decimal format.
225
+ def self.looks_like_decimal?(decimal)
226
+ return true if decimal.to_s.match(/^[+-]?\d+(\.\d*)?$/)
227
+ return false
228
+ end
229
+
230
+ # Default is to J2000.0 (i.e. equinox2=2000.0, system=:fk5)
231
+ def self.precession_matrix(equinox1, equinox2=2000.0, system=:fk5)
232
+ a, b, c = (system == :fk4)? Equatorial::fk4_rotation_angles(equinox1, equinox2): Equatorial::fk5_rotation_angles(equinox1, equinox2)
233
+
234
+ sina = Math.sin(a)
235
+ sinb = Math.sin(b)
236
+ sinc = Math.sin(c)
237
+
238
+ cosa = Math.cos(a)
239
+ cosb = Math.cos(b)
240
+ cosc = Math.cos(c)
241
+
242
+ Matrix.rows([
243
+ [cosa*cosb*cosc-sina*sinb, sina*cosb+cosa*sinb*cosc, cosa*sinc],
244
+ [-cosa*sinb-sina*cosb*cosc, cosa*cosb-sina*sinb*cosc, -sina*sinc],
245
+ [-cosb*sinc, -sinb*sinc, cosc]
246
+ ])
247
+ end
248
+
249
+ def self.fk5_rotation_angles(equinox1, equinox2=2000.0)
250
+ deg_to_rad = Math::PI / 180.0
251
+ sec_to_rad = deg_to_rad / 3600.0
252
+ t = 0.001 * (equinox2 - equinox1)
253
+ st = 0.001 * (equinox1 - 2000.0)
254
+
255
+ a = sec_to_rad * t *
256
+ (23062.181 +
257
+ st * (139.656 + 0.0139 * st) +
258
+ t * (30.188 - 0.344 * st + 17.998))
259
+ b = sec_to_rad * t * t * (79.280 + 0.410 * st + 0.205 * t) + a
260
+ c = sec_to_rad * t *
261
+ (20043.109 -
262
+ st * (85.33 + 0.217 * st) +
263
+ t * (-42.665 - 0.217 * st - 41.833 * t))
264
+
265
+ return [a, b, c]
266
+ end
267
+
268
+ def self.fk4_rotation_angles(equinox1, equinox2=2000.0)
269
+ deg_to_rad = Math::PI / 180.0
270
+ sec_to_rad = deg_to_rad / 3600.0
271
+ t = 0.001 * (equinox2 - equinox1)
272
+ st = 0.001 * (equinox1 - 1900.0)
273
+
274
+ a = sec_to_rad * t *
275
+ (23042.53 +
276
+ st * (139.75 + 0.06 * st) +
277
+ t * (30.23 - 0.27 * st + 18.0 * t))
278
+ b = sec_to_rad * t * t * (79.27 + 0.66 * st + 0.32 * t) + a
279
+ c = sec_to_rad * t *
280
+ (20046.85 -
281
+ st * (85.33 + 0.37 * st) +
282
+ t * (-42.67 - 0.37 * st - 41.8 * t))
283
+
284
+ return [a, b, c]
285
+ end
286
+
287
+ class RightAscension
288
+ attr_reader :value
289
+
290
+ MIN_ALLOWED_VALUE = 0.0
291
+ MAX_ALLOWED_VALUE = 360.0
292
+
293
+ # Create a RightAscenson object.
294
+ def initialize(ra)
295
+ self.value = ra
296
+ end
297
+
298
+ def value=(ra)
299
+ if ra.is_a?(Numeric) or Equatorial::looks_like_decimal?(ra)
300
+ @value = ra.to_f
301
+ elsif ra.is_a?(String)
302
+ if Equatorial::looks_like_sexigesimal?(ra)
303
+ @value = RightAscension.from_sexigesimal(ra).value
304
+ else
305
+ raise Exception::InvalidSexigesimalFormat.new(ra)
306
+ end
307
+ else
308
+ raise Exception::NotARightAscension.new(ra)
309
+ end
310
+
311
+ raise Exception::RAOutOfRange.new(ra) if !RightAscension::in_range?(@value)
312
+ end
313
+
314
+ # Is the supplied number in the correct range for a right ascension
315
+ # (i.e between 0 and 360 degrees)?
316
+ def self.in_range?(ra)
317
+ (MIN_ALLOWED_VALUE..MAX_ALLOWED_VALUE) === ra
318
+ end
319
+
320
+ # Create a RightAscension object from a string in sexigesimal format.
321
+ # Numerical values may be separated by a space or colon (but not both).
322
+ def self.from_sexigesimal(ra)
323
+ raise Exception::InvalidSexigesimalFormat.new(ra) if !Equatorial::looks_like_sexigesimal?(ra)
324
+
325
+ hours_s, minutes_s, seconds_s = ra.split(/:|\s+/)
326
+
327
+ hours = hours_s.to_f
328
+ minutes = minutes_s.to_f
329
+ seconds = seconds_s.to_f
330
+
331
+ ra_degrees = 15.0 * (hours + (minutes + seconds.to_f / 60.0) / 60.0)
332
+ return RightAscension.new(ra_degrees)
333
+ end
334
+
335
+ # Return the value of the ra as a string in sexigesimal format.
336
+ def to_sexigesimal(sep=':')
337
+ rah = (self.value / 15.0).to_i()
338
+ ram = ((self.value - 15.0 * rah) * (60.0 / 15.0)).to_i()
339
+ ras = self.value - (rah * 15.0) - (ram / (60.0 / 15.0))
340
+ ras = ras * (60.0 * 60.0 / 15.0)
341
+
342
+ return sprintf("%02u#{sep}%02u#{sep}%0.2f", rah, ram, ras)
343
+ end
344
+
345
+ # Returns a RightAscension object projected forward the specified
346
+ # number of degrees.
347
+ def +(degrees)
348
+ new_degrees = self.value + degrees
349
+ new_degrees = new_degrees - MAX_ALLOWED_VALUE if new_degrees > MAX_ALLOWED_VALUE
350
+
351
+ RightAscension.new(new_degrees)
352
+ end
353
+
354
+ # Add the specified number of degrees to the ra.
355
+ def increment(degrees)
356
+ new_degrees = self + degrees
357
+ self.value = new_degrees.value
358
+ end
359
+
360
+ # Returns a RightAscension object projected backward the specified
361
+ # number of degrees.
362
+ def -(degrees)
363
+ new_degrees = self.value - degrees
364
+ new_degrees = MAX_ALLOWED_VALUE + new_degrees if new_degrees < MIN_ALLOWED_VALUE
365
+
366
+ RightAscension.new(new_degrees)
367
+ end
368
+
369
+ # Subtract the specified number of degrees from the ra.
370
+ def decrement(degrees)
371
+ new_degrees = self - degrees
372
+ self.value = new_degrees.value
373
+ end
374
+
375
+ def <=>(ra)
376
+ self.value <=> ra.value
377
+ end
378
+
379
+ def ==(ra)
380
+ self.value == ra.value
381
+ end
382
+
383
+ def to_s
384
+ "#{self.to_sexigesimal}"
385
+ end
386
+ end
387
+
388
+ class Declination
389
+ attr_reader :value
390
+
391
+ MIN_ALLOWED_VALUE = -90.0
392
+ MAX_ALLOWED_VALUE = 90.0
393
+
394
+ # Create a Declination object from a decimal number.
395
+ def initialize(dec)
396
+ self.value = dec
397
+ end
398
+
399
+ def value=(dec)
400
+ if dec.is_a?(Numeric) or Equatorial::looks_like_decimal?(dec)
401
+ @value = dec.to_f
402
+ elsif dec.is_a?(String)
403
+ if Equatorial::looks_like_sexigesimal?(dec)
404
+ @value = Declination.from_sexigesimal(dec).value
405
+ else
406
+ raise Exception::InvalidSexigesimalFormat.new(dec)
407
+ end
408
+ else
409
+ raise Exception::NotADeclinationAscension.new(dec)
410
+ end
411
+
412
+ raise Exception::DecOutOfRange.new(dec) if !Declination::in_range?(@value)
413
+ end
414
+
415
+ # Is the supplied number in the correct range for a declination
416
+ # (i.e between -90 and 90 degrees)?
417
+ def self.in_range?(dec)
418
+ (MIN_ALLOWED_VALUE..MAX_ALLOWED_VALUE) === dec
419
+ end
420
+
421
+ # Create a Declination object from a string in sexigesimal format.
422
+ # Numerical values may be separated by a space or colon (but not both).
423
+ def self.from_sexigesimal(dec)
424
+ raise Exception::InvalidSexigesimalFormat.new(dec) if !Equatorial::looks_like_sexigesimal?(dec)
425
+
426
+ degree, arcmin, arcsec = dec.split(/:|\s+/)
427
+ degree_f = degree.to_f()
428
+ arcmin_f = (arcmin.to_f() / 60.0)
429
+ arcsec_f = (arcsec.to_f() / (60.0 * 60.0))
430
+
431
+ return Declination.new(degree_f - arcmin_f - arcsec_f) if degree_f < 0
432
+ return Declination.new(degree_f + arcmin_f + arcsec_f)
433
+ end
434
+
435
+ # Return the value of the dec as a string in sexigesimal format.
436
+ def to_sexigesimal(sep=':')
437
+ degrees = self.value
438
+ decsign = '+'
439
+ if degrees < 0
440
+ decsign = '-'
441
+ degrees = -degrees
442
+ end
443
+ decd = degrees.to_i()
444
+ decm = ((degrees - decd) * 60.0).to_i()
445
+ decs = degrees - decd - (decm / 60.0)
446
+ decs = decs * 60.0 * 60.0
447
+
448
+ return sprintf("%s%02u#{sep}%02u#{sep}%0.2f", decsign, decd, decm, decs)
449
+ end
450
+
451
+ # Returns a declination object projected forward the specified
452
+ # number of degrees. Note that if the new object will have a
453
+ # value greater than 90 degrees, the declination will be assumed
454
+ # to be exactly 90 degrees, and similarly for -90 degrees.
455
+ def +(degrees)
456
+ new_degrees = self.value + degrees
457
+ new_degrees = MAX_ALLOWED_VALUE if new_degrees > MAX_ALLOWED_VALUE
458
+ new_degrees = MIN_ALLOWED_VALUE if new_degrees < MIN_ALLOWED_VALUE
459
+
460
+ Declination.new(new_degrees)
461
+ end
462
+
463
+ # Add the specified number of degrees to the dec.
464
+ def increment(degrees)
465
+ new_degrees = self + degrees
466
+ self.value = new_degrees.value
467
+ end
468
+
469
+ # Returns a declination object projected backwards the specified
470
+ # number of degrees. Note that if the new object will have a
471
+ # value greater less than -90 degrees, the declination will be assumed
472
+ # to be exactly -90 degrees.
473
+ def -(degrees)
474
+ new_degrees = self.value - degrees
475
+ new_degrees = MIN_ALLOWED_VALUE if new_degrees < MIN_ALLOWED_VALUE
476
+
477
+ Declination.new(new_degrees)
478
+ end
479
+
480
+ # Subtract the specified number of degrees from the dec.
481
+ def decrement(degrees)
482
+ new_degrees = self - degrees
483
+ self.value = new_degrees.value
484
+ end
485
+
486
+ def <=>(dec)
487
+ self.value <=> dec.value
488
+ end
489
+
490
+ def ==(dec)
491
+ self.value == dec.value
492
+ end
493
+
494
+ def to_s
495
+ "#{self.to_sexigesimal}"
496
+ end
497
+ end
498
+
499
+ class RADecPosition < SpaceTime::SpatialPosition
500
+ attr_reader :ra, :dec, :distance
501
+ attr_accessor :equinox, :system
502
+
503
+ # Create a Position object. ra and dec may be:
504
+ # 1) RightAscension or Declination objects
505
+ # 2) Sexigesimal strings in standard format
506
+ # 3) Strings that looks like decimals
507
+ # 4) Degrees in decimal format.
508
+ def initialize(ra, dec, distance=nil, equinox=2000.0, system=:fk5)
509
+ self.ra = ra
510
+ self.dec = dec
511
+ self.distance = distance
512
+ self.equinox = equinox
513
+ self.system = system
514
+
515
+ super(self.ra, self.dec, self.distance)
516
+ end
517
+
518
+ def ra=(ra)
519
+ (ra.is_a?(RightAscension))? @ra = ra: @ra = RightAscension.new(ra)
520
+ end
521
+
522
+ def dec=(dec)
523
+ (dec.is_a?(Declination))? @dec = dec: @dec = Declination.new(dec)
524
+ end
525
+
526
+ def distance=(dist)
527
+ @distance = dist
528
+ end
529
+
530
+ # Returns the angular separation between two positions in degrees.
531
+ def angular_separation(position)
532
+ # Convert to radians
533
+ ra1 = self.ra.value * (Math::PI / 180.0)
534
+ dec1 = self.dec.value * (Math::PI / 180.0)
535
+ ra2 = position.ra.value * (Math::PI / 180.0)
536
+ dec2 = position.dec.value * (Math::PI / 180.0)
537
+
538
+ ninety = 90.0 * (Math::PI / 180) # One quarter of a circle (90 degrees) in radians
539
+
540
+ sep = Math.acos(
541
+ Math.cos(ninety - dec1) * Math.cos(ninety - dec2) +
542
+ Math.sin(ninety - dec1) * Math.sin(ninety - dec2) * Math.cos(ra1 - ra2)
543
+ )
544
+
545
+ return sep * (180.0 / Math::PI)
546
+ end
547
+
548
+ def ==(position)
549
+ # Precess the new position object into the same equinox as the receiver.
550
+ position = position.precess(self.equinox, self.system) if position.equinox != self.equinox and position.system != self.system
551
+ self.ra == position.ra && self.dec == position.dec && self.distance == position.distance
552
+ end
553
+
554
+ # Precess the position to the specified equinox.
555
+ # The constants for the precession matrix and general algorithm are taken from the IDL astro routines PREMAT and PRECESS.
556
+ # Notes: Accuracy of precession decreases for declination values near 90 degrees.
557
+ # Should not be used more than 2.5 centuries from 2000 on the FK5 system (1950.0 on the FK4 system).
558
+ def precess(to_equinox=2000.0, system=:fk5)
559
+ if self.equinox != to_equinox or self.system != system
560
+ deg_to_rad = Math::PI / 180.0
561
+
562
+ from_components = Matrix.row_vector(RADecPosition::equatorial_to_rectangular(self.ra.value, self.dec.value))
563
+ precession_matrix = Equatorial::precession_matrix(self.equinox, to_equinox, system)
564
+ to_components = from_components * precession_matrix
565
+ ra, dec = RADecPosition::rectangular_to_equatorial(to_components[0, 0], to_components[0, 1], to_components[0, 2])
566
+
567
+ return RADecPosition.new(ra, dec, self.distance, to_equinox, system)
568
+ else
569
+ return self
570
+ end
571
+ end
572
+
573
+ def to_s
574
+ coords = "#{self.ra}, #{self.dec}"
575
+ coords << ", #{self.distance}" if self.distance
576
+
577
+ coords
578
+ end
579
+
580
+ # Convert RA and Dec in decimal degrees to rectangular coordinates.
581
+ def self.equatorial_to_rectangular(ra, dec)
582
+ deg_to_rad = Math::PI / 180.0
583
+
584
+ x = Math.cos(ra * deg_to_rad) * Math.cos(dec * deg_to_rad)
585
+ y = Math.sin(ra * deg_to_rad) * Math.cos(dec * deg_to_rad)
586
+ z = Math.sin(dec * deg_to_rad)
587
+
588
+ return [x, y, z]
589
+ end
590
+
591
+ # Convert rectangular coordinates to equatorial coordinates in degrees.
592
+ def self.rectangular_to_equatorial(x, y, z)
593
+ rad_to_deg = 180.0 / Math::PI
594
+
595
+ ra_rad = Math.atan(y/x)
596
+ dec_rad = Math.asin(z)
597
+
598
+ ra_rad = ra_rad + 2 * Math::PI if ra_rad < 0.0
599
+
600
+ [ra_rad * rad_to_deg, dec_rad * rad_to_deg]
601
+ end
602
+ end
603
+ end
604
+
605
+ end
606
+ end
607
+