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,10 @@
1
+ require 'avm/controlled_vocabulary'
2
+
3
+ module AVM
4
+ module ImageQuality
5
+ TERMS = %w{Good Moderate Poor}
6
+
7
+ include AVM::ControlledVocabulary
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ require 'avm/controlled_vocabulary'
2
+
3
+ module AVM
4
+ module ImageType
5
+ TERMS = %w{Observation Artwork Photographic Planetary Simulation Chart Collage}
6
+
7
+ include ControlledVocabulary
8
+ end
9
+ end
10
+
@@ -0,0 +1,28 @@
1
+ require 'delegate'
2
+
3
+ module AVM
4
+ # Delegate of Nokogiri::XML::Node which fixes XPath queries to use the correct namespace prefixes
5
+ class Node < DelegateClass(Nokogiri::XML::Node)
6
+ def initialize(xmp, node)
7
+ @xmp, @node = xmp, node
8
+ super(@node)
9
+ end
10
+
11
+ def at_xpath(path)
12
+ if node = @node.at_xpath(path, @xmp.namespaces)
13
+ self.class.new(@xmp, node)
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ def search(path)
20
+ self.class.from_nodeset(@xmp, @node.search(path, @xmp.namespaces))
21
+ end
22
+
23
+ def self.from_nodeset(xmp, nodeset)
24
+ nodeset.collect { |node| new(xmp, node) }
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,76 @@
1
+ module AVM
2
+ # An individual observation by a single instrument w/ specific settings.
3
+ # Astronomical images are made of one or more Observations.
4
+ class Observation
5
+ AVM_SINGLE_FIELDS = %w{Facility Instrument Spectral.ColorAssignment Spectral.Band Spectral.Bandpass Spectral.CentralWavelength Temporal.StartTime Temporal.IntegrationTime DatasetID}
6
+ AVM_SINGLE_METHODS = [ :facility, :instrument, :color_assignment, :band, :bandpass, :wavelength, :string_start_time, :integration_time, :dataset_id ]
7
+ AVM_SINGLES = AVM_SINGLE_FIELDS.zip(AVM_SINGLE_METHODS)
8
+
9
+ attr_reader :image, :options
10
+
11
+ def initialize(image, options = {})
12
+ @image, @options = image, options
13
+ @options[:start_time] = @options[:string_start_time] || @options[:start_time]
14
+ end
15
+
16
+ def method_missing(method)
17
+ @options[method]
18
+ end
19
+
20
+ def wavelength
21
+ (wavelength = @options[:wavelength]) ? wavelength.to_f : nil
22
+ end
23
+
24
+ def start_time
25
+ (Time.parse(@options[:start_time]) rescue nil)
26
+ end
27
+
28
+ def string_start_time
29
+ start_time ? start_time.strftime('%Y-%m-%dT%H:%M') : nil
30
+ end
31
+
32
+ def to_h
33
+ Hash[@options.keys.reject { |key| key == :string_start_time }.collect { |key| [ key, send(key) ] }]
34
+ end
35
+
36
+ def self.from_xml(image, document)
37
+ observation_parts = {}
38
+
39
+ document.get_refs do |refs|
40
+ AVM_SINGLES.each do |name, method|
41
+ if node = refs[:avm].at_xpath(".//avm:#{name}")
42
+ observation_parts[method] = node.text.split(';').collect(&:strip)
43
+ end
44
+ end
45
+ end
46
+
47
+ begin
48
+ observation = {}
49
+
50
+ observation_parts.each do |method, parts|
51
+ if part = parts.shift
52
+ observation[method] = part if part != '-'
53
+ end
54
+ end
55
+
56
+ image.create_observation(observation) if !observation.empty?
57
+ end while !observation.empty?
58
+ end
59
+
60
+ def self.add_to_document(document, observations)
61
+ field_values = {}
62
+ AVM_SINGLES.each do |name, method|
63
+ observations.each do |observation|
64
+ field_values[name] ||= []
65
+ field_values[name] << (observation.send(method) || '-')
66
+ end
67
+ end
68
+
69
+ document.get_refs do |refs|
70
+ field_values.each do |name, value|
71
+ refs[:avm].add_child(%{<avm:#{name}>#{value.join(',')}</avm:#{name}>})
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,10 @@
1
+ require 'avm/controlled_vocabulary'
2
+
3
+ module AVM
4
+ module SpatialQuality
5
+ TERMS = %w{Full Position}
6
+
7
+ include AVM::ControlledVocabulary
8
+ end
9
+ end
10
+
@@ -0,0 +1,157 @@
1
+ require 'nokogiri'
2
+ require 'avm/node'
3
+
4
+ module AVM
5
+ # An XMP document wrapper, providing namespace handling and document reference assistance.
6
+ class XMP
7
+ PREFIXES = {
8
+ 'dc' => 'Dublin Core',
9
+ 'Iptc4xmpCore' => 'IPTC',
10
+ 'photoshop' => 'Photoshop',
11
+ 'avm' => 'AVM'
12
+ }
13
+
14
+ REQUIRED_NAMESPACES = {
15
+ :x => "adobe:ns:meta/",
16
+ :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
17
+ :dc => "http://purl.org/dc/elements/1.1/",
18
+ :photoshop => "http://ns.adobe.com/photoshop/1.0/",
19
+ :avm => "http://www.communicatingastronomy.org/avm/1.0/",
20
+ :Iptc4xmpCore => "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
21
+ }
22
+
23
+ attr_reader :doc
24
+
25
+ def initialize(doc = nil)
26
+ @doc = doc || empty_xml_doc
27
+
28
+ raise StandardError.new('not a Nokogiri node') if !@doc.kind_of?(::Nokogiri::XML::Node)
29
+
30
+ ensure_namespaces!
31
+ ensure_descriptions_findable!
32
+ end
33
+
34
+ def get_refs
35
+ yield Hash[[ :dublin_core, :iptc, :photoshop, :avm ].collect { |key| [ key, send(key) ] }]
36
+ end
37
+
38
+ def self.from_string(string)
39
+ new(Nokogiri::XML(string))
40
+ end
41
+
42
+ def ensure_xmlns(string)
43
+ string.gsub(%r{([</@])(\w+):}) { |all, matches| $1 + (prefix_map[$2] || $2) + ':' }
44
+ end
45
+
46
+ alias :% :ensure_xmlns
47
+
48
+ def ensure_xpath(path)
49
+ [ ensure_xmlns(path), namespaces ]
50
+ end
51
+
52
+ def search(path, node = doc)
53
+ node.search(*ensure_xpath(path))
54
+ end
55
+
56
+ def at_xpath(path, node = doc)
57
+ node.at_xpath(*ensure_xpath(path))
58
+ end
59
+
60
+ def namespaces
61
+ @namespaces ||= doc.document.collect_namespaces
62
+ end
63
+
64
+ private
65
+ def current_namespaces
66
+ doc.document.collect_namespaces
67
+ end
68
+
69
+ def prefix_map
70
+ @prefix_map ||= Hash[current_namespaces.collect { |prefix, namespace|
71
+ self.class.get_required_namespace(namespace, prefix.gsub('xmlns:', ''))
72
+ }.compact]
73
+ end
74
+
75
+ def self.get_required_namespace(namespace, prefix)
76
+ result = nil
77
+ REQUIRED_NAMESPACES.each do |original_prefix, target_namespace|
78
+ result = [ original_prefix.to_s, prefix ] if namespace == target_namespace
79
+ end
80
+ result
81
+ end
82
+
83
+ def ensure_namespaces!
84
+ existing = current_namespaces
85
+
86
+ REQUIRED_NAMESPACES.each do |namespace, url|
87
+ doc.root.add_namespace_definition(namespace.to_s, url) if !existing.values.include?(url)
88
+ end
89
+ end
90
+
91
+ def ensure_descriptions_findable!
92
+ added = []
93
+
94
+ search('//rdf:Description').each do |description|
95
+ if first_child = description.first_element_child
96
+ if namespace = first_child.namespace
97
+ prefix = namespace.prefix
98
+
99
+ if prefix_description = PREFIXES[prefix_map.index(prefix)]
100
+ description[self % 'rdf:about'] = prefix_description
101
+ added << prefix
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ ensure_rdf!
108
+ ensure_missing_descriptions!(added)
109
+ end
110
+
111
+ def ensure_rdf!
112
+ doc.first_element_child.add_child(self % '<rdf:RDF />') if !at_xpath('//rdf:RDF')
113
+ end
114
+
115
+ def ensure_missing_descriptions!(already_added)
116
+ PREFIXES.each do |prefix, about|
117
+ if !already_added.include?(prefix)
118
+ at_xpath('//rdf:RDF').add_child(self % %{<rdf:Description rdf:about="#{about}" />})
119
+ end
120
+ end
121
+ end
122
+
123
+ def dublin_core
124
+ at_rdf_description "Dublin Core"
125
+ end
126
+
127
+ def iptc
128
+ at_rdf_description "IPTC"
129
+ end
130
+
131
+ def avm
132
+ at_rdf_description "AVM"
133
+ end
134
+
135
+ def photoshop
136
+ at_rdf_description "Photoshop"
137
+ end
138
+
139
+ def at_rdf_description(about)
140
+ AVM::Node.new(self, at_xpath(%{//rdf:Description[@rdf:about="#{about}"]}))
141
+ end
142
+
143
+ def empty_xml_doc
144
+ Nokogiri::XML(<<-XML)
145
+ <x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
146
+ <rdf:RDF>
147
+ <rdf:Description rdf:about="Dublin Core" />
148
+ <rdf:Description rdf:about="IPTC" />
149
+ <rdf:Description rdf:about="Photoshop" />
150
+ <rdf:Description rdf:about="AVM" />
151
+ </rdf:RDF>
152
+ </x:xmpmeta>
153
+ XML
154
+ end
155
+ end
156
+ end
157
+
@@ -0,0 +1,7 @@
1
+ module Ruby
2
+ module Avm
3
+ module Library
4
+ # Your code goes here...
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Ruby
2
+ module Avm
3
+ module Library
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ watch('lib/(.*)\.rb') { |file| reek(file[0]) }
2
+ watch('spec/(.*)_spec\.rb') { |file| reek("lib/#{file[1]}.rb") }
3
+
4
+ def reek(file = nil)
5
+ file ||= Dir['lib/**/*.rb'].join(' ')
6
+ spec_file = file.gsub('lib/', 'spec/').gsub('.rb', '_spec.rb')
7
+
8
+ system %{bundle exec rspec -c #{spec_file}}
9
+ system %{reek #{file}}
10
+ end
11
+
12
+ reek
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ruby-avm-library/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ruby-avm-library"
7
+ s.version = Ruby::Avm::Library::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["John Bintz"]
10
+ s.email = ["bintz@stsci.edu"]
11
+ s.homepage = ""
12
+ s.summary = %q{Library for reading and writing AVM XMP metadata}
13
+ s.description = %q{This library makes working with Astronomy Visualization Metadata (AVM) tags within XMP easier. Reading existing XMP files and generating new ones is made simple through a fully object oriented interface.}
14
+
15
+ s.rubyforge_project = "ruby-avm-library"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'mocha'
24
+
25
+ s.add_dependency 'nokogiri'
26
+ s.add_dependency 'thor'
27
+ end
File without changes
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+ require 'avm/contact'
3
+
4
+ describe AVM::Contact do
5
+ let(:contact) { AVM::Contact.new(contact_info) }
6
+
7
+ subject { contact }
8
+
9
+ let(:contact_info) { {
10
+ :name => name,
11
+ :email => email,
12
+ :telephone => telephone,
13
+ :address => address,
14
+ :city => city,
15
+ :state => state,
16
+ :postal_code => postal_code,
17
+ :country => country
18
+ } }
19
+
20
+ let(:name) { 'John Bintz' }
21
+ let(:email) { 'bintz@stsci.edu' }
22
+ let(:telephone) { '800-555-1234' }
23
+ let(:address) { '3700 San Martin Drive' }
24
+ let(:city) { 'Baltimore' }
25
+ let(:state) { 'Maryland' }
26
+ let(:postal_code) { '21218' }
27
+ let(:country) { 'USA' }
28
+
29
+ its(:name) { should == name }
30
+ its(:email) { should == email }
31
+ its(:telephone) { should == telephone }
32
+ its(:address) { should == address }
33
+ its(:city) { should == city }
34
+ its(:state) { should == state }
35
+ its(:province) { should == state }
36
+ its(:postal_code) { should == postal_code }
37
+ its(:zip) { should == postal_code }
38
+ its(:country) { should == country }
39
+
40
+ its(:to_h) { should == {
41
+ :name => name,
42
+ :email => email,
43
+ :telephone => telephone,
44
+ :address => address,
45
+ :city => city,
46
+ :state => state,
47
+ :postal_code => postal_code,
48
+ :country => country
49
+ } }
50
+
51
+ its(:to_creator_list_element) { should == "<rdf:li>John Bintz</rdf:li>" }
52
+
53
+ describe 'mappings' do
54
+ AVM::Contact::FIELD_MAP.each do |key, value|
55
+ context "#{key} => #{value}" do
56
+ let(:contact_info) { { key => "test" } }
57
+
58
+ its(value) { should == "test" }
59
+ end
60
+ end
61
+ end
62
+
63
+ context '#<=>' do
64
+ let(:second_contact) { AVM::Contact.new(second_contact_info) }
65
+ let(:contacts) { [ contact, second_contact ] }
66
+
67
+ let(:second_name) { 'Aohn Bintz' }
68
+
69
+ let(:second_contact_info) { {
70
+ :name => second_name,
71
+ :email => email,
72
+ :telephone => telephone,
73
+ :address => address,
74
+ :city => city,
75
+ :state => state,
76
+ :postal_code => postal_code,
77
+ :country => country
78
+ } }
79
+
80
+ subject { contacts.sort }
81
+
82
+ context 'primary not set' do
83
+ it { should == [ second_contact, contact ] }
84
+ end
85
+
86
+ context 'primary set' do
87
+ before { contact.primary = true }
88
+
89
+ it { should == [ contact, second_contact ] }
90
+ end
91
+ end
92
+ end
93
+