diff --git a/README.md b/README.md
index 2f4633f..b9d1af8 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,56 @@
-ruby-avm-library
+## The Ruby AVM Library
+
+The Astronomy Visualization Metadata (AVM) standard is an extension of the Adobe XMP format. This
+extension adds information to an astronomical image that describes the scientific data and methods
+of collection that went in to producing the image. This Ruby library assists in reading the metadata from
+XMP documents and writing out AVM data as a new XMP file.
+
+## Installing the library
+
+### From Bundler
+
+In your Gemfile:
+
+ gem 'ruby-avm-library'
+
+To use the current development version:
+
+ gem 'ruby-avm-library', :git => ''
+
+### From RubyGems
+
+ gem install ruby-avm-library
+
+## Basic usage
+
+### Reading an XMP file
+
+ require 'avm/image'
+
+ image = AVM::Image.from_xml(File.read('my-file.xmp'))
+
+ puts image.title #=> "The title of the image"
+
+### Writing XML data
+
+ image.to_xml #=>
+
+### Creating an Image from scratch
+
+ image = AVM::Image.new
+ image.title = "The title of the image"
+
+ observation = image.create_observation(:instrument => 'HST', :color_assignment => 'Green')
+ contact = image.creator.create_contact(:name => 'John Bintz')
+
+## Command line tool
+
+`avm2avm` currently performs one function: take an XMP file from stdin and pretty print the image as a Hash:
+
+ avm2avm < my-file.xmp
+
+## More resources
+
+* RDoc: http://ruby-docs.info/blah
+* AVM Standard: http://www.virtualastronomy.org/
+
diff --git a/lib/avm/cli.rb b/lib/avm/cli.rb
index bf18cee..4823fe3 100644
--- a/lib/avm/cli.rb
+++ b/lib/avm/cli.rb
@@ -3,6 +3,7 @@ require 'avm/image'
require 'pp'
module AVM
+ # The CLI interface
class CLI < ::Thor
default_task :convert
diff --git a/lib/avm/contact.rb b/lib/avm/contact.rb
index 852098a..8691242 100644
--- a/lib/avm/contact.rb
+++ b/lib/avm/contact.rb
@@ -1,4 +1,5 @@
module AVM
+ # A contributor to an image
class Contact
FIELD_MAP = {
:zip => :postal_code,
diff --git a/lib/avm/controlled_vocabulary.rb b/lib/avm/controlled_vocabulary.rb
index 34f5113..e820398 100644
--- a/lib/avm/controlled_vocabulary.rb
+++ b/lib/avm/controlled_vocabulary.rb
@@ -1,4 +1,5 @@
module AVM
+ # Build a ControlledVocabulary set of classes for use with CV fields
module ControlledVocabulary
class << self
def included(klass)
diff --git a/lib/avm/creator.rb b/lib/avm/creator.rb
index cadea35..3336f2a 100644
--- a/lib/avm/creator.rb
+++ b/lib/avm/creator.rb
@@ -2,6 +2,7 @@ require 'avm/contact'
require 'nokogiri'
module AVM
+ # A container for Contacts (contributors to an image)
class Creator
attr_reader :contacts, :image
@@ -34,8 +35,8 @@ module AVM
end
def method_missing(key, *opts)
- if key.to_s[-1..-1] == '='
- @options[key.to_s[0..-2].to_sym] = opts.first
+ if (key_to_s = key.to_s)[-1..-1] == '='
+ @options[key_to_s[0..-2].to_sym] = opts.first
else
if PRIMARY_CONTACT_FIELDS.include?(key)
primary_contact_field key
diff --git a/lib/avm/image.rb b/lib/avm/image.rb
index 1e95fb3..6cb1a4a 100644
--- a/lib/avm/image.rb
+++ b/lib/avm/image.rb
@@ -8,6 +8,7 @@ require 'avm/coordinate_frame'
require 'avm/observation'
module AVM
+ # A single image, which has Observations, Contacts, and other metadata
class Image
DUBLIN_CORE_FIELDS = [ :title, :description ]
@@ -151,16 +152,15 @@ module AVM
@options = options
AVM_TO_FLOAT.each do |field|
- if @options[field]
- case @options[field]
- when Array
- @options[field].collect!(&:to_f)
- else
- @options[field] = @options[field].to_f
- end
- end
+ @options[field] = case (value = @options[field])
+ when Array
+ value.collect(&:to_f)
+ else
+ value ? value.to_f : nil
+ end
end
+
@observations = []
end
@@ -336,11 +336,9 @@ module AVM
end
def string_date_or_nil(field)
- return nil if !send(field)
- send(field).strftime('%Y-%m-%d')
+ (value = send(field)) ? value.strftime('%Y-%m-%d') : nil
end
-
def alt_li_tag(text)
%{#{text}}
end
diff --git a/lib/avm/node.rb b/lib/avm/node.rb
index 465a265..0f9e98d 100644
--- a/lib/avm/node.rb
+++ b/lib/avm/node.rb
@@ -1,6 +1,7 @@
require 'delegate'
module AVM
+ # Delegate of Nokogiri::XML::Node which fixes XPath queries to use the correct namespace prefixes
class Node < DelegateClass(Nokogiri::XML::Node)
def initialize(xmp, node)
@xmp, @node = xmp, node
diff --git a/lib/avm/observation.rb b/lib/avm/observation.rb
index e5ccea9..a893319 100644
--- a/lib/avm/observation.rb
+++ b/lib/avm/observation.rb
@@ -1,4 +1,6 @@
module AVM
+ # An individual observation by a single instrument w/ specific settings.
+ # Astronomical images are made of one or more Observations.
class Observation
AVM_SINGLE_FIELDS = %w{Facility Instrument Spectral.ColorAssignment Spectral.Band Spectral.Bandpass Spectral.CentralWavelength Temporal.StartTime Temporal.IntegrationTime DatasetID}
AVM_SINGLE_METHODS = [ :facility, :instrument, :color_assignment, :band, :bandpass, :wavelength, :string_start_time, :integration_time, :dataset_id ]
@@ -7,9 +9,8 @@ module AVM
attr_reader :image, :options
def initialize(image, options = {})
- options[:start_time] = options[:string_start_time] if options[:string_start_time]
-
@image, @options = image, options
+ @options[:start_time] = @options[:string_start_time] || @options[:start_time]
end
def method_missing(method)
@@ -17,7 +18,7 @@ module AVM
end
def wavelength
- @options[:wavelength] ? @options[:wavelength].to_f : nil
+ (wavelength = @options[:wavelength]) ? wavelength.to_f : nil
end
def start_time
@@ -25,11 +26,7 @@ module AVM
end
def string_start_time
- if start_time
- start_time.strftime('%Y-%m-%dT%H:%M')
- else
- nil
- end
+ start_time ? start_time.strftime('%Y-%m-%dT%H:%M') : nil
end
def to_h
diff --git a/lib/avm/xmp.rb b/lib/avm/xmp.rb
index 50d763f..533356a 100644
--- a/lib/avm/xmp.rb
+++ b/lib/avm/xmp.rb
@@ -2,6 +2,7 @@ require 'nokogiri'
require 'avm/node'
module AVM
+ # An XMP document wrapper, providing namespace handling and document reference assistance.
class XMP
PREFIXES = {
'dc' => 'Dublin Core',
@@ -58,20 +59,26 @@ module AVM
end
private
- def prefix_map
- @prefix_map ||= Hash[doc.document.collect_namespaces.collect { |prefix, namespace|
- prefix = prefix.gsub('xmlns:', '')
- result = nil
+ def current_namespaces
+ doc.document.collect_namespaces
+ end
- REQUIRED_NAMESPACES.each do |original_prefix, target_namespace|
- result = [ original_prefix.to_s, prefix ] if namespace == target_namespace
- end
- result
+ def prefix_map
+ @prefix_map ||= Hash[current_namespaces.collect { |prefix, namespace|
+ self.class.get_required_namespace(namespace, prefix.gsub('xmlns:', ''))
}.compact]
end
+
+ def self.get_required_namespace(namespace, prefix)
+ result = nil
+ REQUIRED_NAMESPACES.each do |original_prefix, target_namespace|
+ result = [ original_prefix.to_s, prefix ] if namespace == target_namespace
+ end
+ result
+ end
def ensure_namespaces!
- existing = doc.document.collect_namespaces
+ existing = current_namespaces
REQUIRED_NAMESPACES.each do |namespace, url|
doc.root.add_namespace_definition(namespace.to_s, url) if !existing.values.include?(url)
@@ -83,8 +90,8 @@ module AVM
search('//rdf:Description').each do |description|
if first_child = description.first_element_child
- if first_child.namespace
- prefix = first_child.namespace.prefix
+ if namespace = first_child.namespace
+ prefix = namespace.prefix
if prefix_description = PREFIXES[prefix_map.index(prefix)]
description[self % 'rdf:about'] = prefix_description
@@ -94,13 +101,17 @@ module AVM
end
end
- if !at_xpath('//rdf:RDF')
- doc.first_element_child.add_child(self % '')
- end
+ ensure_rdf!
+ ensure_missing_descriptions!(added)
+ end
+ def ensure_rdf!
+ doc.first_element_child.add_child(self % '') if !at_xpath('//rdf:RDF')
+ end
+ def ensure_missing_descriptions!(already_added)
PREFIXES.each do |prefix, about|
- if !added.include?(prefix)
+ if !already_added.include?(prefix)
at_xpath('//rdf:RDF').add_child(self % %{})
end
end
diff --git a/reek.watchr b/reek.watchr
new file mode 100644
index 0000000..0a6a073
--- /dev/null
+++ b/reek.watchr
@@ -0,0 +1,12 @@
+watch('lib/(.*)\.rb') { |file| reek(file[0]) }
+watch('spec/(.*)_spec\.rb') { |file| reek("lib/#{file[1]}.rb") }
+
+def reek(file = nil)
+ file ||= Dir['lib/**/*.rb'].join(' ')
+ spec_file = file.gsub('lib/', 'spec/').gsub('.rb', '_spec.rb')
+
+ system %{bundle exec rspec -c #{spec_file}}
+ system %{reek #{file}}
+end
+
+reek
diff --git a/spec/avm/image_spec.rb b/spec/avm/image_spec.rb
index 2d6bc30..2a36719 100644
--- a/spec/avm/image_spec.rb
+++ b/spec/avm/image_spec.rb
@@ -290,15 +290,15 @@ describe AVM::Image do
it "should have the spatial tags" do
xpath_text(avm, './avm:Spatial.CoordinateFrame').should == coordinate_frame
xpath_text(avm, './avm:Spatial.Equinox').should == equinox
- xpath_list(avm, './avm:Spatial.ReferenceValue').should == reference_value.collect(&:to_s)
- xpath_list(avm, './avm:Spatial.ReferenceDimension').should == reference_dimension.collect(&:to_s)
- xpath_list(avm, './avm:Spatial.ReferencePixel').should == reference_pixel.collect(&:to_s)
- xpath_list(avm, './avm:Spatial.Scale').should == spatial_scale.collect(&:to_s)
+ xpath_list(avm, './avm:Spatial.ReferenceValue').should == reference_value.collect { |v| v.to_f.to_s }
+ xpath_list(avm, './avm:Spatial.ReferenceDimension').should == reference_dimension.collect { |v| v.to_f.to_s }
+ xpath_list(avm, './avm:Spatial.ReferencePixel').should == reference_pixel.collect { |v| v.to_f.to_s }
+ xpath_list(avm, './avm:Spatial.Scale').should == spatial_scale.collect { |v| v.to_f.to_s }
xpath_text(avm, './avm:Spatial.CoordsystemProjection').should == coordinate_system_projection
xpath_text(avm, './avm:Spatial.Quality').should == spatial_quality
xpath_text(avm, './avm:Spatial.Notes').should == spatial_notes
xpath_text(avm, './avm:Spatial.FITSheader').should == fits_header
- xpath_list(avm, './avm:Spatial.CDMatrix').should == spatial_cd_matrix.collect(&:to_s)
+ xpath_list(avm, './avm:Spatial.CDMatrix').should == spatial_cd_matrix.collect { |v| v.to_f.to_s }
end
it "should have the publisher tags" do