ruby-avm-library 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.autotest +5 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +5 -0
  4. data/Gemfile +14 -0
  5. data/README.md +56 -0
  6. data/Rakefile +2 -0
  7. data/autotest/discover.rb +2 -0
  8. data/bin/avm2avm +7 -0
  9. data/lib/avm/cli.rb +18 -0
  10. data/lib/avm/contact.rb +40 -0
  11. data/lib/avm/controlled_vocabulary.rb +23 -0
  12. data/lib/avm/coordinate_frame.rb +10 -0
  13. data/lib/avm/coordinate_system_projection.rb +10 -0
  14. data/lib/avm/creator.rb +123 -0
  15. data/lib/avm/image.rb +355 -0
  16. data/lib/avm/image_quality.rb +10 -0
  17. data/lib/avm/image_type.rb +10 -0
  18. data/lib/avm/node.rb +28 -0
  19. data/lib/avm/observation.rb +76 -0
  20. data/lib/avm/spatial_quality.rb +10 -0
  21. data/lib/avm/xmp.rb +157 -0
  22. data/lib/ruby-avm-library.rb +7 -0
  23. data/lib/ruby-avm-library/version.rb +7 -0
  24. data/reek.watchr +12 -0
  25. data/ruby-avm-library.gemspec +27 -0
  26. data/spec/avm/cli_spec.rb +0 -0
  27. data/spec/avm/contact_spec.rb +93 -0
  28. data/spec/avm/creator_spec.rb +268 -0
  29. data/spec/avm/image_spec.rb +350 -0
  30. data/spec/avm/observation_spec.rb +191 -0
  31. data/spec/avm/xmp_spec.rb +154 -0
  32. data/spec/quick_fix_formatter.rb +26 -0
  33. data/spec/sample_files/creator/no_creator.xmp +14 -0
  34. data/spec/sample_files/creator/one_creator.xmp +28 -0
  35. data/spec/sample_files/creator/two_creators.xmp +26 -0
  36. data/spec/sample_files/image/both.xmp +101 -0
  37. data/spec/sample_files/image/light_years.xmp +96 -0
  38. data/spec/sample_files/image/nothing.xmp +18 -0
  39. data/spec/sample_files/image/redshift.xmp +101 -0
  40. data/spec/sample_files/image/single_value_light_years.xmp +96 -0
  41. data/spec/sample_files/observation/none.xmp +5 -0
  42. data/spec/sample_files/observation/one.xmp +17 -0
  43. data/spec/sample_files/observation/two.xmp +17 -0
  44. data/spec/spec_helper.rb +3 -0
  45. metadata +184 -0
@@ -0,0 +1,5 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ at.add_mapping(%r{^spec/sample_files/([^/]+)/.*}, true) { |_, m|
3
+ "spec/avm/#{m[1]}_spec.rb"
4
+ }
5
+ end
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .quickfix.txt
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ -c
2
+ --require ./spec/quick_fix_formatter.rb
3
+ --format progress
4
+ --format RSpec::Core::Formatters::QuickFixFormatter
5
+ --out .quickfix.txt
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ruby-avm-library.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'autotest'
8
+ end
9
+
10
+ group :mac do
11
+ gem 'autotest-fsevent'
12
+ gem 'autotest-growl'
13
+ end
14
+
@@ -0,0 +1,56 @@
1
+ ## The Ruby AVM Library
2
+
3
+ The Astronomy Visualization Metadata (AVM) standard is an extension of the Adobe XMP format. This
4
+ extension adds information to an astronomical image that describes the scientific data and methods
5
+ of collection that went in to producing the image. This Ruby library assists in reading the metadata from
6
+ XMP documents and writing out AVM data as a new XMP file.
7
+
8
+ ## Installing the library
9
+
10
+ ### From Bundler
11
+
12
+ In your Gemfile:
13
+
14
+ gem 'ruby-avm-library'
15
+
16
+ To use the current development version:
17
+
18
+ gem 'ruby-avm-library', :git => 'git://github.com/johnbintz/ruby-avm-library.git'
19
+
20
+ ### From RubyGems
21
+
22
+ gem install ruby-avm-library
23
+
24
+ ## Basic usage
25
+
26
+ ### Reading an XMP file
27
+
28
+ require 'avm/image'
29
+
30
+ image = AVM::Image.from_xml(File.read('my-file.xmp'))
31
+
32
+ puts image.title #=> "The title of the image"
33
+
34
+ ### Writing XML data
35
+
36
+ image.to_xml #=> <xmp data in xml format />
37
+
38
+ ### Creating an Image from scratch
39
+
40
+ image = AVM::Image.new
41
+ image.title = "The title of the image"
42
+
43
+ observation = image.create_observation(:instrument => 'HST', :color_assignment => 'Green')
44
+ contact = image.creator.create_contact(:name => 'John Bintz')
45
+
46
+ ## Command line tool
47
+
48
+ `avm2avm` currently performs one function: take an XMP file from stdin and pretty print the image as a Hash:
49
+
50
+ avm2avm < my-file.xmp
51
+
52
+ ## More resources
53
+
54
+ * RDoc: [http://rdoc.info/github/johnbintz/ruby-avm-library/frames](http://rdoc.info/github/johnbintz/ruby-avm-library/frames)
55
+ * AVM Standard: [http://www.virtualastronomy.org/avm_metadata.php](http://www.virtualastronomy.org/avm_metadata.php)
56
+
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,2 @@
1
+ Autotest.add_discovery { "rspec2" }
2
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thor'
4
+ require 'avm/cli'
5
+
6
+ AVM::CLI.start
7
+
@@ -0,0 +1,18 @@
1
+ require 'thor'
2
+ require 'avm/image'
3
+ require 'pp'
4
+
5
+ module AVM
6
+ # The CLI interface
7
+ class CLI < ::Thor
8
+ default_task :convert
9
+
10
+ desc 'convert', "Convert a file from one format to another"
11
+ def convert
12
+ data = $stdin.read
13
+
14
+ pp AVM::Image.from_xml(data).to_h
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,40 @@
1
+ module AVM
2
+ # A contributor to an image
3
+ class Contact
4
+ FIELD_MAP = {
5
+ :zip => :postal_code,
6
+ :state => :state_province,
7
+ :province => :state_province
8
+ }
9
+
10
+ HASH_FIELDS = [ :name, :email, :telephone, :address, :city, :state, :postal_code, :country ]
11
+
12
+ attr_accessor :primary
13
+
14
+ def initialize(info)
15
+ @info = Hash[info.collect { |key, value| [ FIELD_MAP[key] || key, value ] }]
16
+ @primary = false
17
+ end
18
+
19
+ def method_missing(key)
20
+ @info[FIELD_MAP[key] || key]
21
+ end
22
+
23
+ def <=>(other)
24
+ return -1 if primary?
25
+ self.name <=> other.name
26
+ end
27
+
28
+ def to_creator_list_element
29
+ %{<rdf:li>#{self.name}</rdf:li>}
30
+ end
31
+
32
+ def primary?
33
+ @primary
34
+ end
35
+
36
+ def to_h
37
+ Hash[HASH_FIELDS.collect { |key| [ key, send(key) ] } ]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module AVM
2
+ # Build a ControlledVocabulary set of classes for use with CV fields
3
+ module ControlledVocabulary
4
+ class << self
5
+ def included(klass)
6
+ klass::TERMS.each do |type|
7
+ new_klass = Class.new do
8
+ def to_s
9
+ self.class.to_s.split('::').last
10
+ end
11
+
12
+ def ==(other)
13
+ self.to_s == other.to_s
14
+ end
15
+ end
16
+
17
+ klass.const_set(type.to_sym, new_klass)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,10 @@
1
+ require 'avm/controlled_vocabulary'
2
+
3
+ module AVM
4
+ module CoordinateFrame
5
+ TERMS = %w{ICRS FK5 FK4 ECL GAL SGAL}
6
+
7
+ include ControlledVocabulary
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ require 'avm/controlled_vocabulary'
2
+
3
+ module AVM
4
+ module CoordinateSystemProjection
5
+ TERMS = %w{TAN SIN ARC AIT CAR CEA}
6
+
7
+ include ControlledVocabulary
8
+ end
9
+ end
10
+
@@ -0,0 +1,123 @@
1
+ require 'avm/contact'
2
+ require 'nokogiri'
3
+
4
+ module AVM
5
+ # A container for Contacts (contributors to an image)
6
+ class Creator
7
+ attr_reader :contacts, :image
8
+
9
+ IPTC_CORE_FIELDS = [ :address, :city, :state, :zip, :country ]
10
+ PRIMARY_CONTACT_FIELDS = IPTC_CORE_FIELDS + [ :province, :postal_code ]
11
+ IPTC_MULTI_FIELD_MAP = [ [ :telephone, 'CiTelWork' ], [ :email, 'CiEmailWork' ] ]
12
+ IPTC_CORE_FIELD_ELEMENT_NAMES = %w{CiAdrExtadr CiAdrCity CiAdrRegion CiAdrPcode CiAdrCtry}
13
+ IPTC_CORE_FIELDS_AND_NAMES = IPTC_CORE_FIELDS.zip(IPTC_CORE_FIELD_ELEMENT_NAMES)
14
+
15
+ def initialize(image, given_contacts = [])
16
+ @options = {}
17
+ @contacts = given_contacts
18
+ @image = image
19
+ end
20
+
21
+ def merge!(hash)
22
+ @options.merge!(hash)
23
+ end
24
+
25
+ def length
26
+ contacts.length
27
+ end
28
+
29
+ def [](which)
30
+ contacts[which]
31
+ end
32
+
33
+ def to_a
34
+ contacts.sort.collect(&:to_h)
35
+ end
36
+
37
+ def method_missing(key, *opts)
38
+ if (key_to_s = key.to_s)[-1..-1] == '='
39
+ @options[key_to_s[0..-2].to_sym] = opts.first
40
+ else
41
+ if PRIMARY_CONTACT_FIELDS.include?(key)
42
+ primary_contact_field key
43
+ else
44
+ @options[key]
45
+ end
46
+ end
47
+ end
48
+
49
+ def add_to_document(document)
50
+ document.get_refs do |refs|
51
+ creator = refs[:dublin_core].add_child('<dc:creator><rdf:Seq></rdf:Seq></dc:creator>')
52
+
53
+ list = creator.at_xpath('.//rdf:Seq')
54
+ contact_info = refs[:iptc].add_child('<Iptc4xmpCore:CreatorContactInfo rdf:parseType="Resource" />').first
55
+
56
+ contacts.sort.each do |contact|
57
+ list.add_child(contact.to_creator_list_element)
58
+ end
59
+
60
+ if primary_contact
61
+ IPTC_MULTI_FIELD_MAP.each do |key, element_name|
62
+ contact_info.add_child "<Iptc4xmpCore:#{element_name}>#{contacts.sort.collect(&key).join(',')}</Iptc4xmpCore:#{element_name}>"
63
+ end
64
+
65
+ iptc_namespace = document.doc.root.namespace_scopes.find { |ns| ns.prefix == 'Iptc4xmpCore' }
66
+
67
+ IPTC_CORE_FIELDS_AND_NAMES.each do |key, element_name|
68
+ node = contact_info.document.create_element(element_name, primary_contact.send(key))
69
+ node.namespace = iptc_namespace
70
+ contact_info.add_child node
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def from_xml(image, document)
77
+ contacts = []
78
+ document.get_refs do |refs|
79
+ refs[:dublin_core].search('.//dc:creator//rdf:li').each do |name|
80
+ contacts << { :name => name.text.strip }
81
+ end
82
+
83
+ IPTC_MULTI_FIELD_MAP.each do |key, element_name|
84
+ if node = refs[:iptc].at_xpath(".//Iptc4xmpCore:#{element_name}")
85
+ node.text.split(',').collect(&:strip).each_with_index do |value, index|
86
+ contacts[index][key] = value
87
+ end
88
+ end
89
+ end
90
+
91
+ IPTC_CORE_FIELDS_AND_NAMES.each do |key, element_name|
92
+ if node = refs[:iptc].at_xpath("//Iptc4xmpCore:#{element_name}")
93
+ contacts.each { |contact| contact[key] = node.text.strip }
94
+ end
95
+ end
96
+ end
97
+
98
+ if !(@contacts = contacts.collect { |contact| Contact.new(contact) }).empty?
99
+ @contacts.first.primary = true
100
+ end
101
+ end
102
+
103
+ def primary_contact
104
+ @contacts.find(&:primary) || @contacts.sort.first
105
+ end
106
+
107
+ def create_contact(info)
108
+ contact = Contact.new(info)
109
+ contacts << contact
110
+ contact
111
+ end
112
+
113
+ private
114
+ def primary_contact_field(field)
115
+ if contact = primary_contact
116
+ contact.send(field)
117
+ else
118
+ nil
119
+ end
120
+ end
121
+ end
122
+ end
123
+
@@ -0,0 +1,355 @@
1
+ require 'avm/creator'
2
+ require 'avm/xmp'
3
+ require 'avm/image_type'
4
+ require 'avm/image_quality'
5
+ require 'avm/spatial_quality'
6
+ require 'avm/coordinate_system_projection'
7
+ require 'avm/coordinate_frame'
8
+ require 'avm/observation'
9
+
10
+ module AVM
11
+ # A single image, which has Observations, Contacts, and other metadata
12
+ class Image
13
+ DUBLIN_CORE_FIELDS = [ :title, :description ]
14
+
15
+ PHOTOSHOP_SINGLE_FIELDS = [
16
+ 'Headline',
17
+ 'DateCreated',
18
+ 'Credit'
19
+ ]
20
+
21
+ PHOTOSHOP_SINGLE_METHODS = [
22
+ :headline,
23
+ :date,
24
+ :credit
25
+ ]
26
+
27
+ PHOTOSHOP_SINGLES_MESSAGES = [
28
+ :headline,
29
+ :string_date,
30
+ :credit
31
+ ]
32
+
33
+ PHOTOSHOP_SINGLES_FOR_METHODS = PHOTOSHOP_SINGLE_FIELDS.zip(PHOTOSHOP_SINGLE_METHODS)
34
+ PHOTOSHOP_SINGLES_FOR_MESSAGES = PHOTOSHOP_SINGLE_FIELDS.zip(PHOTOSHOP_SINGLES_MESSAGES)
35
+
36
+ AVM_SINGLE_FIELDS = [
37
+ 'Distance.Notes',
38
+ 'Spectral.Notes',
39
+ 'ReferenceURL',
40
+ 'ID',
41
+ 'Type',
42
+ 'Image.ProductQuality',
43
+ 'Spatial.Equinox',
44
+ 'Spatial.Rotation',
45
+ 'Spatial.Notes',
46
+ 'Spatial.FITSheader',
47
+ 'Spatial.Quality',
48
+ 'Spatial.CoordsystemProjection',
49
+ 'Spatial.CDMatrix',
50
+ 'Spatial.Scale',
51
+ 'Spatial.ReferencePixel',
52
+ 'Spatial.ReferenceDimension',
53
+ 'Spatial.ReferenceValue',
54
+ 'Spatial.Equinox',
55
+ 'Spatial.CoordinateFrame',
56
+ 'Publisher',
57
+ 'PublisherID',
58
+ 'ResourceID',
59
+ 'ResourceURL',
60
+ 'RelatedResources',
61
+ 'MetadataDate',
62
+ 'MetadataVersion',
63
+ 'Subject.Category',
64
+ ]
65
+
66
+ AVM_SINGLE_METHODS = [
67
+ :distance_notes,
68
+ :spectral_notes,
69
+ :reference_url,
70
+ :id,
71
+ :type,
72
+ :quality,
73
+ :spatial_equinox,
74
+ :spatial_rotation,
75
+ :spatial_notes,
76
+ :fits_header,
77
+ :spatial_quality,
78
+ :coordinate_system_projection,
79
+ :spatial_cd_matrix,
80
+ :spatial_scale,
81
+ :reference_pixel,
82
+ :reference_dimension,
83
+ :reference_value,
84
+ :equinox,
85
+ :coordinate_frame,
86
+ :publisher,
87
+ :publisher_id,
88
+ :resource_id,
89
+ :resource_url,
90
+ :related_resources,
91
+ :metadata_date,
92
+ :metadata_version,
93
+ :categories
94
+ ]
95
+
96
+ AVM_SINGLE_MESSAGES = [
97
+ :distance_notes,
98
+ :spectral_notes,
99
+ :reference_url,
100
+ :id,
101
+ :image_type,
102
+ :image_quality,
103
+ :spatial_equinox,
104
+ :spatial_rotation,
105
+ :spatial_notes,
106
+ :fits_header,
107
+ :spatial_quality,
108
+ :coordinate_system_projection,
109
+ :spatial_cd_matrix,
110
+ :spatial_scale,
111
+ :reference_pixel,
112
+ :reference_dimension,
113
+ :reference_value,
114
+ :equinox,
115
+ :coordinate_frame,
116
+ :publisher,
117
+ :publisher_id,
118
+ :resource_id,
119
+ :resource_url,
120
+ :related_resources,
121
+ :string_metadata_date,
122
+ :metadata_version,
123
+ :categories
124
+ ]
125
+
126
+ AVM_SINGLES = AVM_SINGLE_FIELDS.zip(AVM_SINGLE_METHODS)
127
+ AVM_SINGLES_FOR_MESSAGES = AVM_SINGLE_FIELDS.zip(AVM_SINGLE_MESSAGES)
128
+
129
+ AVM_TO_FLOAT = [
130
+ :spatial_rotation,
131
+ :spatial_cd_matrix,
132
+ :spatial_scale,
133
+ :reference_pixel,
134
+ :reference_dimension,
135
+ :reference_value
136
+ ]
137
+
138
+ HASH_FIELDS = [ :title, :headline, :description, :distance_notes,
139
+ :spectral_notes, :reference_url, :credit, :date,
140
+ :id, :image_type, :image_quality, :coordinate_frame,
141
+ :equinox, :reference_value, :reference_dimension, :reference_pixel,
142
+ :spatial_scale, :spatial_rotation, :coordinate_system_projection, :spatial_quality,
143
+ :spatial_notes, :fits_header, :spatial_cd_matrix, :distance,
144
+ :publisher, :publisher_id, :resource_id, :resource_url,
145
+ :related_resources, :metadata_date, :metadata_version, :subject_names, :categories
146
+ ]
147
+
148
+ attr_reader :creator, :observations
149
+
150
+ def initialize(options = {})
151
+ @creator = AVM::Creator.new(self)
152
+ @options = options
153
+
154
+ AVM_TO_FLOAT.each do |field|
155
+ @options[field] = case (value = @options[field])
156
+ when Array
157
+ value.collect(&:to_f)
158
+ else
159
+ value ? value.to_f : nil
160
+ end
161
+ end
162
+
163
+
164
+ @observations = []
165
+ end
166
+
167
+ def valid?
168
+ self.title && self.credit
169
+ end
170
+
171
+ def create_observation(options)
172
+ observation = Observation.new(self, options)
173
+ @observations << observation
174
+ observation
175
+ end
176
+
177
+ def to_xml
178
+ document = AVM::XMP.new
179
+
180
+ creator.add_to_document(document)
181
+ Observation.add_to_document(document, observations)
182
+
183
+ document.get_refs do |refs|
184
+ DUBLIN_CORE_FIELDS.each do |field|
185
+ refs[:dublin_core].add_child(%{<dc:#{field}>#{alt_li_tag(send(field))}</dc:#{field}>})
186
+ end
187
+
188
+ PHOTOSHOP_SINGLES_FOR_MESSAGES.each do |tag, message|
189
+ refs[:photoshop].add_child(%{<photoshop:#{tag}>#{send(message)}</photoshop:#{tag}>})
190
+ end
191
+
192
+ AVM_SINGLES_FOR_MESSAGES.each do |tag, message|
193
+ if value = send(message)
194
+ case value
195
+ when Array
196
+ container_tag = (message == :related_resources) ? 'Bag' : 'Seq'
197
+ value = "<rdf:#{container_tag}>" + value.collect { |v| "<rdf:li>#{v.to_s}</rdf:li>" }.join + "</rdf:#{container_tag}>"
198
+ else
199
+ value = value.to_s
200
+ end
201
+
202
+ refs[:avm].add_child(%{<avm:#{tag}>#{value}</avm:#{tag}>})
203
+ end
204
+ end
205
+
206
+ distance_nodes = []
207
+ distance_nodes << rdf_li(light_years) if light_years
208
+ if redshift
209
+ distance_nodes << rdf_li('-') if distance_nodes.empty?
210
+ distance_nodes << rdf_li(redshift)
211
+ end
212
+
213
+ if !distance_nodes.empty?
214
+ refs[:avm].add_child(%{<avm:Distance><rdf:Seq>#{distance_nodes.join}</rdf:Seq></avm:Distance>})
215
+ end
216
+ end
217
+
218
+ document.doc
219
+ end
220
+
221
+ def id
222
+ @options[:id]
223
+ end
224
+
225
+ def image_type
226
+ cv_class_instance_for(AVM::ImageType, :type)
227
+ end
228
+
229
+ def image_quality
230
+ cv_class_instance_for(AVM::ImageQuality, :quality)
231
+ end
232
+
233
+ def spatial_quality
234
+ cv_class_instance_for(AVM::SpatialQuality, :spatial_quality)
235
+ end
236
+
237
+ def coordinate_frame
238
+ cv_class_instance_for(AVM::CoordinateFrame, :coordinate_frame)
239
+ end
240
+
241
+ def coordinate_system_projection
242
+ cv_class_instance_for(AVM::CoordinateSystemProjection, :coordinate_system_projection)
243
+ end
244
+
245
+ def date
246
+ date_or_nil(:date)
247
+ end
248
+
249
+ def metadata_date
250
+ date_or_nil(:metadata_date)
251
+ end
252
+
253
+ def string_date
254
+ string_date_or_nil(:date)
255
+ end
256
+
257
+ def string_metadata_date
258
+ string_date_or_nil(:metadata_date)
259
+ end
260
+
261
+ def distance
262
+ [ light_years, redshift ]
263
+ end
264
+
265
+ def self.from_xml(string)
266
+ document = AVM::XMP.from_string(string)
267
+
268
+ options = {}
269
+
270
+ document.get_refs do |refs|
271
+ DUBLIN_CORE_FIELDS.each do |field|
272
+ if node = refs[:dublin_core].at_xpath(".//dc:#{field}//rdf:li[1]")
273
+ options[field] = node.text
274
+ end
275
+ end
276
+
277
+ if node = refs[:dublin_core].at_xpath(".//dc:subject/rdf:Bag")
278
+ options[:subject_names] = node.search('./rdf:li').collect(&:text)
279
+ end
280
+
281
+ AVM_SINGLES.each do |tag, field|
282
+ if node = refs[:avm].at_xpath("./avm:#{tag}")
283
+ if field == :categories
284
+ options[field] = node.text.split(";").collect(&:strip)
285
+ else
286
+ if !(list_items = node.search('.//rdf:li')).empty?
287
+ options[field] = list_items.collect(&:text)
288
+ else
289
+ options[field] = node.text
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ PHOTOSHOP_SINGLES_FOR_METHODS.each do |tag, field|
296
+ if node = refs[:photoshop].at_xpath("./photoshop:#{tag}")
297
+ options[field] = node.text
298
+ end
299
+ end
300
+
301
+ if node = refs[:avm].at_xpath('./avm:Distance')
302
+ list_values = node.search('.//rdf:li').collect { |li| li.text }
303
+
304
+ case list_values.length
305
+ when 0
306
+ options[:light_years] = node.text
307
+ when 1
308
+ options[:light_years] = list_values.first
309
+ when 2
310
+ options[:light_years] = (list_values.first == '-') ? nil : list_values.first
311
+ options[:redshift] = list_values.last
312
+ end
313
+ end
314
+ end
315
+
316
+ image = new(options)
317
+ image.creator.from_xml(self, document)
318
+ Observation.from_xml(image, document)
319
+ image
320
+ end
321
+
322
+ def to_h
323
+ hash = Hash[HASH_FIELDS.collect { |key| [ key, send(key) ] }]
324
+ hash[:creator] = creator.to_a
325
+ hash[:observations] = observations.collect(&:to_h)
326
+ hash
327
+ end
328
+
329
+ def method_missing(method)
330
+ @options[method]
331
+ end
332
+
333
+ private
334
+ def date_or_nil(field)
335
+ (Time.parse(@options[field]) rescue nil)
336
+ end
337
+
338
+ def string_date_or_nil(field)
339
+ (value = send(field)) ? value.strftime('%Y-%m-%d') : nil
340
+ end
341
+
342
+ def alt_li_tag(text)
343
+ %{<rdf:Alt><rdf:li xml:lang="x-default">#{text}</rdf:li></rdf:Alt>}
344
+ end
345
+
346
+ def rdf_li(text)
347
+ %{<rdf:li>#{text}</rdf:li>}
348
+ end
349
+
350
+ def cv_class_instance_for(mod, field)
351
+ (mod.const_get(@options[field].to_sym).new rescue nil)
352
+ end
353
+ end
354
+ end
355
+