ruby-avm-library 0.0.1

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 (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
+