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.
- data/.autotest +5 -0
- data/.gitignore +5 -0
- data/.rspec +5 -0
- data/Gemfile +14 -0
- data/README.md +56 -0
- data/Rakefile +2 -0
- data/autotest/discover.rb +2 -0
- data/bin/avm2avm +7 -0
- data/lib/avm/cli.rb +18 -0
- data/lib/avm/contact.rb +40 -0
- data/lib/avm/controlled_vocabulary.rb +23 -0
- data/lib/avm/coordinate_frame.rb +10 -0
- data/lib/avm/coordinate_system_projection.rb +10 -0
- data/lib/avm/creator.rb +123 -0
- data/lib/avm/image.rb +355 -0
- data/lib/avm/image_quality.rb +10 -0
- data/lib/avm/image_type.rb +10 -0
- data/lib/avm/node.rb +28 -0
- data/lib/avm/observation.rb +76 -0
- data/lib/avm/spatial_quality.rb +10 -0
- data/lib/avm/xmp.rb +157 -0
- data/lib/ruby-avm-library.rb +7 -0
- data/lib/ruby-avm-library/version.rb +7 -0
- data/reek.watchr +12 -0
- data/ruby-avm-library.gemspec +27 -0
- data/spec/avm/cli_spec.rb +0 -0
- data/spec/avm/contact_spec.rb +93 -0
- data/spec/avm/creator_spec.rb +268 -0
- data/spec/avm/image_spec.rb +350 -0
- data/spec/avm/observation_spec.rb +191 -0
- data/spec/avm/xmp_spec.rb +154 -0
- data/spec/quick_fix_formatter.rb +26 -0
- data/spec/sample_files/creator/no_creator.xmp +14 -0
- data/spec/sample_files/creator/one_creator.xmp +28 -0
- data/spec/sample_files/creator/two_creators.xmp +26 -0
- data/spec/sample_files/image/both.xmp +101 -0
- data/spec/sample_files/image/light_years.xmp +96 -0
- data/spec/sample_files/image/nothing.xmp +18 -0
- data/spec/sample_files/image/redshift.xmp +101 -0
- data/spec/sample_files/image/single_value_light_years.xmp +96 -0
- data/spec/sample_files/observation/none.xmp +5 -0
- data/spec/sample_files/observation/one.xmp +17 -0
- data/spec/sample_files/observation/two.xmp +17 -0
- data/spec/spec_helper.rb +3 -0
- metadata +184 -0
data/lib/avm/node.rb
ADDED
@@ -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
|
data/lib/avm/xmp.rb
ADDED
@@ -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
|
+
|
data/reek.watchr
ADDED
@@ -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
|
+
|