ols 0.0.1 → 0.1.0
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/.gitignore +8 -1
- data/Gemfile +4 -0
- data/History.txt +6 -0
- data/README.rdoc +48 -57
- data/lib/ols.rb +115 -387
- data/lib/ols/graph.rb +91 -0
- data/lib/ols/term.rb +647 -0
- data/lib/ols/version.rb +4 -1
- data/ols.gemspec +7 -12
- data/script/console +9 -0
- data/test/test_helper.rb +24 -9
- data/test/test_ols.rb +44 -112
- data/test/test_ols_graph.rb +36 -0
- data/test/test_ols_term.rb +484 -0
- metadata +100 -105
data/lib/ols/graph.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module OLS
|
4
|
+
|
5
|
+
# Utility class for representing an ontology graph. You should *NOT* really interact with
|
6
|
+
# instances of this class directly, use OLS.find_by_id etc.
|
7
|
+
#
|
8
|
+
# @author Darren Oakley (https://github.com/dazoakley)
|
9
|
+
class Graph
|
10
|
+
# Creates a new OLS::Graph object
|
11
|
+
def initialize
|
12
|
+
@graph = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Object function used by .clone and .dup to create copies of OLS::Graph objects.
|
16
|
+
def initialize_copy(source)
|
17
|
+
super
|
18
|
+
@graph = {}
|
19
|
+
|
20
|
+
source.raw_graph.each do |term_id,term_details|
|
21
|
+
old_term = term_details[:object]
|
22
|
+
|
23
|
+
new_term = OLS::Term.new( old_term.term_id, old_term.term_id, self )
|
24
|
+
[
|
25
|
+
:@already_fetched_parents,
|
26
|
+
:@already_fetched_children,
|
27
|
+
:@already_fetched_metadata,
|
28
|
+
:@definition,
|
29
|
+
:@synonyms
|
30
|
+
].each do |instance_var|
|
31
|
+
new_term.instance_variable_set(instance_var,old_term.instance_variable_get(instance_var))
|
32
|
+
end
|
33
|
+
|
34
|
+
@graph[term_id] = {
|
35
|
+
:object => new_term,
|
36
|
+
:parents => term_details[:parents].dup,
|
37
|
+
:children => term_details[:children].dup
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Accessor for the internal graph hash
|
43
|
+
# TODO: OLS::Term monkeys around with this in a few places - write methods to handle the access needed so we don't have to expose this
|
44
|
+
def raw_graph
|
45
|
+
@graph
|
46
|
+
end
|
47
|
+
|
48
|
+
# Fetch the object/parents/children hash for a given term.
|
49
|
+
#
|
50
|
+
# @param [String] key The ontology term id
|
51
|
+
# @return [Hash] The object/parents/children hash for the given term
|
52
|
+
def [](key)
|
53
|
+
@graph[key]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fetch the OLS::Term object for a node in the graph.
|
57
|
+
#
|
58
|
+
# @param [String] key The ontology term id
|
59
|
+
# @return [OLS::Term] The OLS::Term object for this term id
|
60
|
+
def find(key)
|
61
|
+
@graph[key][:object] if @graph.has_key? key
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add an OLS::Term object into the graph
|
65
|
+
#
|
66
|
+
# @param [OLS::Term] term The OLS::Term object to add
|
67
|
+
# @raise [TypeError] Raised if +term+ is not an OLS::Term object
|
68
|
+
def add_to_graph(term)
|
69
|
+
raise TypeError, "You must pass an OLS::Term object" unless term.is_a? OLS::Term
|
70
|
+
unless @graph.has_key?(term.term_id)
|
71
|
+
@graph[term.term_id] = { :object => term, :parents => [], :children => [] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Add an edge/relationship to the ontology graph
|
76
|
+
#
|
77
|
+
# @param [OLS::Term] parent The parent OLS::Term
|
78
|
+
# @param [OLS::Term] child The child OLS::Term
|
79
|
+
# @raise [TypeError] Raised if +parent+ or +child+ are not an OLS::Term objects
|
80
|
+
def add_relationship(parent,child)
|
81
|
+
raise TypeError, "You must pass an OLS::Term object" unless parent.is_a? OLS::Term
|
82
|
+
raise TypeError, "You must pass an OLS::Term object" unless child.is_a? OLS::Term
|
83
|
+
|
84
|
+
add_to_graph(parent) if self.find(parent.term_id).nil?
|
85
|
+
add_to_graph(child) if self.find(child.term_id).nil?
|
86
|
+
|
87
|
+
@graph[parent.term_id][:children].push(child.term_id) unless @graph[parent.term_id][:children].include?(child.term_id)
|
88
|
+
@graph[child.term_id][:parents].push(parent.term_id) unless @graph[child.term_id][:parents].include?(parent.term_id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/ols/term.rb
ADDED
@@ -0,0 +1,647 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module OLS
|
4
|
+
|
5
|
+
# Class representing an ontology term
|
6
|
+
#
|
7
|
+
# @author Darren Oakley (https://github.com/dazoakley)
|
8
|
+
class Term
|
9
|
+
attr_reader :term_id, :term_name
|
10
|
+
|
11
|
+
# Creates a new OLS::Term object
|
12
|
+
#
|
13
|
+
# @param [String] term_id The ontology term id
|
14
|
+
# @param [String] term_name The ontology term name
|
15
|
+
def initialize(term_id,term_name,graph=nil)
|
16
|
+
@term_id = term_id
|
17
|
+
@term_name = term_name
|
18
|
+
|
19
|
+
@already_fetched_parents = false
|
20
|
+
@already_fetched_children = false
|
21
|
+
@already_fetched_metadata = false
|
22
|
+
|
23
|
+
if graph.nil?
|
24
|
+
@graph = OLS::Graph.new
|
25
|
+
@graph.add_to_graph(self)
|
26
|
+
else
|
27
|
+
@graph = graph
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Object function used by .clone and .dup to create copies of OLS::Term objects.
|
32
|
+
def initialize_copy(source)
|
33
|
+
super
|
34
|
+
@graph = source.graph.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
# Is this a root node?
|
38
|
+
#
|
39
|
+
# @return [Boolean] +true+/+false+ depending on if this is a root node or not...
|
40
|
+
def is_root?
|
41
|
+
self.parents.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Is this a leaf node?
|
45
|
+
#
|
46
|
+
# @return [Boolean] +true+/+false+ depending on if this is a leaf node or not...
|
47
|
+
def is_leaf?
|
48
|
+
self.children.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Is this ontology term obsolete?
|
52
|
+
#
|
53
|
+
# @return [Boolean] +true+/+false+ depending on if this term is obsolete or not...
|
54
|
+
def is_obsolete?
|
55
|
+
@is_obsolete ||= OLS.request(:is_obsolete) { soap.body = { :termId => self.term_id } }
|
56
|
+
end
|
57
|
+
|
58
|
+
# The ontology term definition
|
59
|
+
#
|
60
|
+
# @return [String] The ontology term definition
|
61
|
+
def definition
|
62
|
+
get_term_metadata
|
63
|
+
@definition
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a hash listing the different types of synonyms known for this term,
|
67
|
+
# keyed by the synonym type
|
68
|
+
#
|
69
|
+
# @return [Hash] a hash listing the different types of synonyms known for this term
|
70
|
+
def synonyms
|
71
|
+
get_term_metadata
|
72
|
+
@synonyms ||= {}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Represent an OLS::Term as a String
|
76
|
+
#
|
77
|
+
# @return [String] A string representation of an OLS::Term
|
78
|
+
def to_s
|
79
|
+
"#{@term_id} - #{@term_name}"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the size of the full ontology graph.
|
83
|
+
#
|
84
|
+
# @return [Integer] The size of the full ontology graph
|
85
|
+
def size
|
86
|
+
self.root.all_children.size + 1
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns depth of this term in its ontology graph. Depth of a node is defined as:
|
90
|
+
#
|
91
|
+
# Depth:: Length of the terms path to its root. Depth of a root term is zero.
|
92
|
+
#
|
93
|
+
# @return [Number] Depth of this node.
|
94
|
+
def level
|
95
|
+
return 0 if self.is_root?
|
96
|
+
1 + parents.first.level
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the root term for this ontology.
|
100
|
+
#
|
101
|
+
# @return [OLS::Term] The root term for this ontology
|
102
|
+
def root
|
103
|
+
root = self
|
104
|
+
root = root.parents.first while !root.is_root?
|
105
|
+
root
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the direct parent terms for this ontology term
|
109
|
+
#
|
110
|
+
# @return [Array] An array of OLS::Term objects
|
111
|
+
def parents(skip_fetch=false)
|
112
|
+
unless @already_fetched_parents || skip_fetch
|
113
|
+
# puts "--- REQUESTING PARENTS (#{self.term_id}) ---"
|
114
|
+
response = OLS.request(:get_term_parents) { soap.body = { :termId => self.term_id } }
|
115
|
+
unless response.nil?
|
116
|
+
if response[:item].is_a? Array
|
117
|
+
response[:item].each do |term|
|
118
|
+
parent = self.find_in_graph(term[:key]) || OLS::Term.new(term[:key],term[:value],@graph)
|
119
|
+
self.add_parent(parent)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
term = response[:item]
|
123
|
+
parent = self.find_in_graph(term[:key]) || OLS::Term.new(term[:key],term[:value],@graph)
|
124
|
+
self.add_parent(parent)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
@already_fetched_parents = true
|
129
|
+
end
|
130
|
+
|
131
|
+
@graph[term_id][:parents].map{ |parent_id| self.find_in_graph(parent_id) }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the term_ids for the direct parents of this term.
|
135
|
+
#
|
136
|
+
# @return [Array] The term_ids for the direct parents of this term
|
137
|
+
def parent_ids
|
138
|
+
parents.map(&:term_id)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns the term_names for the direct parents of this term.
|
142
|
+
#
|
143
|
+
# @return [Array] The term_names for the direct parents of this term
|
144
|
+
def parent_names
|
145
|
+
parents.map(&:term_name)
|
146
|
+
end
|
147
|
+
|
148
|
+
alias :parent_term_ids :parent_ids
|
149
|
+
alias :parent_term_names :parent_names
|
150
|
+
|
151
|
+
# Returns an array of all parent term objects for this ontology term
|
152
|
+
# (all the way to the top of the ontology). The array is ordered
|
153
|
+
# with the root term first and the most direct parent(s) last.
|
154
|
+
#
|
155
|
+
# @return [Array] An array of OLS::Term objects
|
156
|
+
def all_parents
|
157
|
+
return [] if is_root?
|
158
|
+
|
159
|
+
parentage_array = []
|
160
|
+
prev_parents = self.parents
|
161
|
+
while ( !prev_parents.empty? )
|
162
|
+
parentage_array << prev_parents
|
163
|
+
prev_parents = prev_parents.map(&:parents).flatten
|
164
|
+
end
|
165
|
+
|
166
|
+
parentage_array.reverse.flatten
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns an array of all parent term_ids for this ontology term
|
170
|
+
# (all the way to the top of the ontology). The array is ordered
|
171
|
+
# with the root term first and the most direct parent(s) last.
|
172
|
+
# Duplicates are also filtered out.
|
173
|
+
#
|
174
|
+
# @return [Array] An array of ontology term_ids
|
175
|
+
def all_parent_ids
|
176
|
+
all_parents.map(&:term_id).uniq
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns an array of all parent term_names for this ontology term
|
180
|
+
# (all the way to the top of the ontology). The array is ordered
|
181
|
+
# with the root term first and the most direct parent last.
|
182
|
+
# Duplicates are also filtered out.
|
183
|
+
#
|
184
|
+
# @return [Array] An array of ontology term_names
|
185
|
+
def all_parent_names
|
186
|
+
all_parents.map(&:term_name).uniq
|
187
|
+
end
|
188
|
+
|
189
|
+
alias :all_parent_term_ids :all_parent_ids
|
190
|
+
alias :all_parent_term_names :all_parent_names
|
191
|
+
|
192
|
+
# Returns the child terms for this ontology term.
|
193
|
+
#
|
194
|
+
# @return [Array] An array of child OLS::Term objects
|
195
|
+
def children(skip_fetch=false)
|
196
|
+
unless @already_fetched_children || skip_fetch
|
197
|
+
# puts "--- REQUESTING CHILDREN (#{self.term_id}) ---"
|
198
|
+
response = OLS.request(:get_term_children) { soap.body = { :termId => self.term_id, :distance => 1, :relationTypes => [1,2,3,4,5] } }
|
199
|
+
unless response.nil?
|
200
|
+
if response[:item].is_a? Array
|
201
|
+
response[:item].each do |term|
|
202
|
+
child = self.find_in_graph(term[:key]) || OLS::Term.new(term[:key],term[:value],@graph)
|
203
|
+
self.add_child(child)
|
204
|
+
end
|
205
|
+
else
|
206
|
+
term = response[:item]
|
207
|
+
child = self.find_in_graph(term[:key]) || OLS::Term.new(term[:key],term[:value],@graph)
|
208
|
+
self.add_child(child)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
@already_fetched_children = true
|
213
|
+
end
|
214
|
+
|
215
|
+
@graph[term_id][:children].map{ |child_id| self.find_in_graph(child_id) }
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns the term_ids for the direct children of this term.
|
219
|
+
#
|
220
|
+
# @return [Array] The term_ids for the direct children of this term
|
221
|
+
def children_ids
|
222
|
+
children.map(&:term_id)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns the term_names for the direct children of this term.
|
226
|
+
#
|
227
|
+
# @return [Array] The term_names for the direct children of this term
|
228
|
+
def children_names
|
229
|
+
children.map(&:term_name)
|
230
|
+
end
|
231
|
+
|
232
|
+
alias :children_term_ids :children_ids
|
233
|
+
alias :children_term_names :children_names
|
234
|
+
|
235
|
+
# Returns +true+ if the ontology term has any children.
|
236
|
+
#
|
237
|
+
# @return [Boolean] true/false depending on if this term has children or not...
|
238
|
+
def has_children?
|
239
|
+
!self.children.empty?
|
240
|
+
end
|
241
|
+
|
242
|
+
# Convenience method for accessing specific child terms.
|
243
|
+
#
|
244
|
+
# @param [String] term_id The term_id for the child you wish to access
|
245
|
+
# @return [OLS::Term] An OLS::Term object
|
246
|
+
def [](term_id)
|
247
|
+
children_as_a_hash = {}
|
248
|
+
self.children.each do |child|
|
249
|
+
children_as_a_hash[child.term_id] = child
|
250
|
+
end
|
251
|
+
children_as_a_hash[term_id]
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns an array of all child term objects for this ontology term
|
255
|
+
# (all the way down to the bottom of the ontology). The array is NOT
|
256
|
+
# guarenteed to come out in any specific order whatsoever.
|
257
|
+
#
|
258
|
+
# @return [Array] An array of OLS::Term objects
|
259
|
+
def all_children
|
260
|
+
return [] if is_leaf?
|
261
|
+
|
262
|
+
children_array = []
|
263
|
+
prev_children = self.children
|
264
|
+
while ( !prev_children.empty? )
|
265
|
+
children_array << prev_children
|
266
|
+
prev_children = prev_children.map(&:children).flatten
|
267
|
+
end
|
268
|
+
|
269
|
+
children_array.flatten
|
270
|
+
end
|
271
|
+
|
272
|
+
# Returns an array of all child term_ids for this ontology term
|
273
|
+
# (all the way down to the bottom of the ontology). The array is NOT
|
274
|
+
# guarenteed to come out in any specific order whatsoever.
|
275
|
+
#
|
276
|
+
# @return [Array] An array of ontology term_ids
|
277
|
+
def all_child_ids
|
278
|
+
all_children.map(&:term_id)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Returns an array of all child term_names for this ontology term
|
282
|
+
# (all the way down to the bottom of the ontology). The array is NOT
|
283
|
+
# guarenteed to come out in any specific order whatsoever.
|
284
|
+
#
|
285
|
+
# @return [Array] An array of ontology term_names
|
286
|
+
def all_child_names
|
287
|
+
all_children.map(&:term_name)
|
288
|
+
end
|
289
|
+
|
290
|
+
alias :all_child_term_ids :all_child_ids
|
291
|
+
alias :all_child_term_names :all_child_names
|
292
|
+
|
293
|
+
# Merge in another ontology graph that shares the same root. Duplicate nodes (coming from
|
294
|
+
# other_graph) will NOT be overwritten in self.
|
295
|
+
#
|
296
|
+
# @param [OLS::Term] other_graph The other graph to merge with.
|
297
|
+
# @raise [TypeError] This exception is raised if other_graph is not a OLS::Term.
|
298
|
+
# @raise [ArgumentError] This exception is raised if other_graph does not have the same root as self.
|
299
|
+
def merge!( other_graph )
|
300
|
+
raise TypeError, 'You can only merge in another instance of OLS::Term' unless other_graph.is_a?(OLS::Term)
|
301
|
+
raise ArgumentError, 'Unable to merge graphs as they do not share the same root' unless self.root.term_id == other_graph.root.term_id
|
302
|
+
|
303
|
+
self.focus_graph!
|
304
|
+
other_graph.focus_graph!
|
305
|
+
|
306
|
+
merge_graphs( self.root, other_graph.root )
|
307
|
+
end
|
308
|
+
|
309
|
+
# Flesh out and/or focus the ontology graph around this term.
|
310
|
+
#
|
311
|
+
# This will fetch all children and parents for this term, and will also trick each
|
312
|
+
# parent/child object into thinking the ontology graph is fully formed so no further
|
313
|
+
# requests to OLS will be made (to further flesh out the graph). It will also cut down
|
314
|
+
# a much larger ontology graph to just focus on the parents/descendants of this term.
|
315
|
+
#
|
316
|
+
# *NOTE:* This method will totally clobber the existing ontology graph that this term
|
317
|
+
# is part of by removing any term and relationship that is not supposed to be part of this
|
318
|
+
# focused graph. Use #focus_graph to pull out a copy and be non-destructive.
|
319
|
+
#
|
320
|
+
# i.e. This allows us to do the following
|
321
|
+
#
|
322
|
+
# e = OLS.find_by_id('EMAP:3018')
|
323
|
+
# e.focus_graph!
|
324
|
+
# e.root.print_graph
|
325
|
+
#
|
326
|
+
# gives:
|
327
|
+
# * EMAP:0
|
328
|
+
# |---+ EMAP:2636
|
329
|
+
# |---+ EMAP:2822
|
330
|
+
# |---+ EMAP:2987
|
331
|
+
# |---+ EMAP:3018
|
332
|
+
# |---+ EMAP:3022
|
333
|
+
# |---+ EMAP:3023
|
334
|
+
# |---+ EMAP:3024
|
335
|
+
# |---> EMAP:3025
|
336
|
+
# |---> EMAP:3026
|
337
|
+
# |---+ EMAP:3027
|
338
|
+
# |---> EMAP:3029
|
339
|
+
# |---> EMAP:3028
|
340
|
+
# |---+ EMAP:3030
|
341
|
+
# |---> EMAP:3031
|
342
|
+
# |---> EMAP:3032
|
343
|
+
# |---> EMAP:3019
|
344
|
+
# |---+ EMAP:3020
|
345
|
+
# |---> EMAP:3021
|
346
|
+
#
|
347
|
+
# *ALSO NOTE:* without e.focus_graph!, in this case it would print the *complete* EMAP graph (>13,000 terms).
|
348
|
+
#
|
349
|
+
# @see #focus_graph
|
350
|
+
def focus_graph!
|
351
|
+
really_focus_graph_around_term(self)
|
352
|
+
end
|
353
|
+
|
354
|
+
# Flesh out and/or focus the ontology graph around this term.
|
355
|
+
#
|
356
|
+
# This will fetch all children and parents for this term, and will also trick each
|
357
|
+
# parent/child object into thinking the ontology graph is fully formed so no further
|
358
|
+
# requests to OLS will be made (to further flesh out the graph). It will also cut down
|
359
|
+
# a much larger ontology graph to just focus on the parents/descendants of this term.
|
360
|
+
#
|
361
|
+
# *NOTE:* This method does not affect self. It returns a completley new OLS::Term
|
362
|
+
# object containing a completley new internal OLS::Graph. Use #focus_graph! if you
|
363
|
+
# wish to cut down the existing graph.
|
364
|
+
#
|
365
|
+
# i.e. This allows us to do the following
|
366
|
+
#
|
367
|
+
# e = OLS.find_by_id('EMAP:0')
|
368
|
+
# copy = e['EMAP:2636']['EMAP:2822']['EMAP:2987']['EMAP:3018'].focus_graph
|
369
|
+
# copy.root.print_graph
|
370
|
+
#
|
371
|
+
# gives:
|
372
|
+
# * EMAP:0
|
373
|
+
# |---+ EMAP:2636
|
374
|
+
# |---+ EMAP:2822
|
375
|
+
# |---+ EMAP:2987
|
376
|
+
# |---+ EMAP:3018
|
377
|
+
# |---+ EMAP:3022
|
378
|
+
# |---+ EMAP:3023
|
379
|
+
# |---+ EMAP:3024
|
380
|
+
# |---> EMAP:3025
|
381
|
+
# |---> EMAP:3026
|
382
|
+
# |---+ EMAP:3027
|
383
|
+
# |---> EMAP:3029
|
384
|
+
# |---> EMAP:3028
|
385
|
+
# |---+ EMAP:3030
|
386
|
+
# |---> EMAP:3031
|
387
|
+
# |---> EMAP:3032
|
388
|
+
# |---> EMAP:3019
|
389
|
+
# |---+ EMAP:3020
|
390
|
+
# |---> EMAP:3021
|
391
|
+
#
|
392
|
+
# @see #focus_graph!
|
393
|
+
def focus_graph
|
394
|
+
copy = self.dup
|
395
|
+
really_focus_graph_around_term(copy)
|
396
|
+
copy
|
397
|
+
end
|
398
|
+
|
399
|
+
# Utility function for the #focus_graph and #focus_graph! methods. This does the
|
400
|
+
# real work of editing the OLS::Graph.
|
401
|
+
#
|
402
|
+
# @param [OLS::Term] term The term (and graph) to focus around
|
403
|
+
def really_focus_graph_around_term(term)
|
404
|
+
term.all_parents.each { |parent| parent.lock }
|
405
|
+
term.all_children.each { |child| child.lock }
|
406
|
+
|
407
|
+
focus_terms = [term.term_id] + term.all_parent_ids + term.all_child_ids
|
408
|
+
graph = term.graph.raw_graph
|
409
|
+
|
410
|
+
graph.delete_if { |key,val| !focus_terms.include?(key) }
|
411
|
+
graph.each do |key,val|
|
412
|
+
val[:parents].delete_if { |elm| !focus_terms.include?(elm) }
|
413
|
+
val[:children].delete_if { |elm| !focus_terms.include?(elm) }
|
414
|
+
end
|
415
|
+
end
|
416
|
+
private(:really_focus_graph_around_term)
|
417
|
+
|
418
|
+
# Returns a copy of this ontology term object with the parents removed.
|
419
|
+
#
|
420
|
+
# @return [OLS::Term] A copy of this ontology term object with the parents removed
|
421
|
+
def detached_subgraph_copy
|
422
|
+
copy = self.dup
|
423
|
+
copy.parents = []
|
424
|
+
copy.lock_parents
|
425
|
+
copy
|
426
|
+
end
|
427
|
+
|
428
|
+
# Pretty prints the (sub)graph rooted at this ontology term.
|
429
|
+
#
|
430
|
+
# @param [Number] level The indentation level (4 spaces) to start with.
|
431
|
+
def print_graph(level=1)
|
432
|
+
if is_root?
|
433
|
+
print "*"
|
434
|
+
else
|
435
|
+
print(' ' * (level - 1) * 4)
|
436
|
+
print "|---"
|
437
|
+
print( self.has_children? ? "+" : ">" )
|
438
|
+
end
|
439
|
+
|
440
|
+
puts " #{self.term_id}"
|
441
|
+
|
442
|
+
self.children.each { |child| child.print_graph(level + 1)}
|
443
|
+
end
|
444
|
+
|
445
|
+
# Save an image file showing graph structure of all children from the current term.
|
446
|
+
# Requires graphviz to convert the .dot source file to an image file.
|
447
|
+
#
|
448
|
+
# @param [String] filename The filename to save the DOT and image files to - omit the file extension
|
449
|
+
# @param [String] fmt The image format to produce - i.e. png or jpg
|
450
|
+
def write_children_to_graphic_file(filename='graph',fmt='png')
|
451
|
+
dotfile = filename + ".dot"
|
452
|
+
imgfile = filename + "." + fmt
|
453
|
+
|
454
|
+
nodes = [ self ] + self.all_children
|
455
|
+
node_ranks = {}
|
456
|
+
|
457
|
+
nodes.each do |node|
|
458
|
+
node_ranks[node.level] ||= []
|
459
|
+
node_ranks[node.level].push(node.term_id.gsub(':',''))
|
460
|
+
end
|
461
|
+
|
462
|
+
edges = self.all_children.map do |child|
|
463
|
+
child.parents.map { |parent| " #{parent.term_id} -> #{child.term_id}".gsub(':','') }
|
464
|
+
end.flatten.uniq
|
465
|
+
|
466
|
+
write_dot_and_image_file(dotfile,imgfile,fmt,nodes,node_ranks,edges)
|
467
|
+
end
|
468
|
+
|
469
|
+
# Save an image file showing graph structure of all parents for the current term.
|
470
|
+
# Requires graphviz to convert the .dot source file to an image file.
|
471
|
+
#
|
472
|
+
# @param [String] filename The filename to save the DOT and image files to - omit the file extension
|
473
|
+
# @param [String] fmt The image format to produce - i.e. png or jpg
|
474
|
+
def write_parentage_to_graphic_file(filename='graph',fmt='png')
|
475
|
+
dotfile = filename + ".dot"
|
476
|
+
imgfile = filename + "." + fmt
|
477
|
+
|
478
|
+
nodes = self.all_parents + [ self ]
|
479
|
+
node_ranks = {}
|
480
|
+
|
481
|
+
nodes.each do |node|
|
482
|
+
node_ranks[node.level] ||= []
|
483
|
+
node_ranks[node.level].push(node.term_id.gsub(':',''))
|
484
|
+
end
|
485
|
+
|
486
|
+
edges = self.all_parents.map do |parent|
|
487
|
+
parent.children.map { |child| " #{parent.term_id} -> #{child.term_id}".gsub(':','') }
|
488
|
+
end.flatten.uniq
|
489
|
+
|
490
|
+
write_dot_and_image_file(dotfile,imgfile,fmt,nodes,node_ranks,edges)
|
491
|
+
end
|
492
|
+
|
493
|
+
# Image drawing utility function. This is responsible for writing the DOT
|
494
|
+
# source file and converting it into the desired image format.
|
495
|
+
#
|
496
|
+
# @param [String] dotfile The DOT filename
|
497
|
+
# @param [String] imgfile The image filename
|
498
|
+
# @param [String] fmt The image format to produce - i.e. png or jpg
|
499
|
+
# @param [Array] nodes An array of OLS::Term objects to enter as nodes in the graph
|
500
|
+
# @param [Hash] node_ranks A hash of node names grouped by their level
|
501
|
+
# @param [Array] edges An array of edge statements already pre-formatted for DOT format
|
502
|
+
def write_dot_and_image_file(dotfile,imgfile,fmt,nodes,node_ranks,edges)
|
503
|
+
File.open(dotfile,'w') do |f|
|
504
|
+
f << "digraph OntologyTree_#{self.term_id.gsub(':','')} {\n"
|
505
|
+
f << nodes.map { |vert| " #{vert.term_id.gsub(':','')} [label=\"#{vert.term_id}\"]" }.join("\n")
|
506
|
+
f << "\n"
|
507
|
+
f << edges.join("\n")
|
508
|
+
f << "\n"
|
509
|
+
node_ranks.each_value { |nodes| f << " { rank=same; #{nodes.join(' ')} }\n" }
|
510
|
+
f << "}\n"
|
511
|
+
end
|
512
|
+
system( "dot -T#{fmt} #{dotfile} -o #{imgfile}" )
|
513
|
+
end
|
514
|
+
private(:write_dot_and_image_file)
|
515
|
+
|
516
|
+
protected
|
517
|
+
|
518
|
+
# Protected accessor for the term @graph
|
519
|
+
attr_accessor :graph
|
520
|
+
|
521
|
+
# Stop this object from trying to fetch up more parent terms from OLS
|
522
|
+
def lock_parents
|
523
|
+
@already_fetched_parents = true
|
524
|
+
end
|
525
|
+
|
526
|
+
# Stop this object from trying to fetch up more child terms from OLS
|
527
|
+
def lock_children
|
528
|
+
@already_fetched_children = true
|
529
|
+
end
|
530
|
+
|
531
|
+
# Stop this object from looking up anymore parent/child terms from OLS
|
532
|
+
#
|
533
|
+
# @see #lock_parents
|
534
|
+
# @see #lock_children
|
535
|
+
def lock
|
536
|
+
lock_parents
|
537
|
+
lock_children
|
538
|
+
end
|
539
|
+
|
540
|
+
# Graph access function. Allows you to reset the parentage for a given term.
|
541
|
+
#
|
542
|
+
# @param [Array] parents An array of OLS::Term objects to set as the parents
|
543
|
+
# @raise [ArgumentError] Raised if an Array is not passed
|
544
|
+
# @raise [TypeError] Raised if the parents array does not contain OLS::Term objects
|
545
|
+
def parents=(parents)
|
546
|
+
raise ArgumentError, "You must pass an array" unless parents.is_a?(Array)
|
547
|
+
parents.each { |p| raise TypeError, "You must pass an array of OLS::Term objects" unless p.is_a?(OLS::Term) }
|
548
|
+
@graph[self.term_id][:parents] = parents.map(&:term_id)
|
549
|
+
end
|
550
|
+
|
551
|
+
# Graph access function. Adds a parent relationship (for this term) to the graph.
|
552
|
+
#
|
553
|
+
# @param [OLS::Term] parent The OLS::Term to add as a parent
|
554
|
+
# @raise [TypeError] Raised if parent is not an OLS::Term object
|
555
|
+
def add_parent(parent)
|
556
|
+
raise TypeError, "You must pass an OLS::Term object" unless parent.is_a?(OLS::Term)
|
557
|
+
@graph.add_relationship(parent,self)
|
558
|
+
end
|
559
|
+
|
560
|
+
# Graph access function. Adds a child relationship (for this term) to the graph.
|
561
|
+
#
|
562
|
+
# @param [OLS::Term] child The OLS::Term to add as a child
|
563
|
+
# @raise [TypeError] Raised if child is not an OLS::Term object
|
564
|
+
def add_child(child)
|
565
|
+
raise TypeError, "You must pass an OLS::Term object" unless child.is_a?(OLS::Term)
|
566
|
+
@graph.add_relationship(self,child)
|
567
|
+
end
|
568
|
+
|
569
|
+
# Graph access function. Finds an OLS::Term object in the graph.
|
570
|
+
#
|
571
|
+
# @param [String] term_id The term id to look up
|
572
|
+
def find_in_graph(term_id)
|
573
|
+
@graph.find(term_id)
|
574
|
+
end
|
575
|
+
|
576
|
+
private
|
577
|
+
|
578
|
+
# Utility function to hit the :get_term_metadata soap endpoint and extract the
|
579
|
+
# given metadata for an ontology term.
|
580
|
+
def get_term_metadata
|
581
|
+
unless @already_fetched_metadata
|
582
|
+
meta = [ OLS.request(:get_term_metadata) { soap.body = { :termId => self.term_id } }[:item] ].flatten
|
583
|
+
meta.each do |meta_item|
|
584
|
+
case meta_item[:key]
|
585
|
+
when 'definition'
|
586
|
+
@definition = meta_item[:value]
|
587
|
+
when /synonym/
|
588
|
+
syn_match = /^(.+)_synonym/.match( meta_item[:key] )
|
589
|
+
@synonyms ||= {}
|
590
|
+
@synonyms[ syn_match[1].to_sym ] ||= []
|
591
|
+
@synonyms[ syn_match[1].to_sym ] << meta_item[:value]
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
@already_fetched_metadata = true
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
# Utility function to recursivley merge two ontology (sub)graphs.
|
600
|
+
#
|
601
|
+
# @param [OLS::Term] graph1 The target ontology graph to merge into.
|
602
|
+
# @param [OLS::Term] graph2 The donor ontology graph (that will be merged into target).
|
603
|
+
# @return [OLS::Term] The merged ontology graph.
|
604
|
+
def merge_graphs( graph1, graph2 )
|
605
|
+
names1 = graph1.children.map(&:term_id)
|
606
|
+
names2 = graph2.children.map(&:term_id)
|
607
|
+
|
608
|
+
names_to_merge = names2 - names1
|
609
|
+
names_to_merge.each do |name|
|
610
|
+
# puts "--- MERGING #{name} INTO #{graph1.term_id} ---"
|
611
|
+
new_child = graph2[name].detached_subgraph_copy
|
612
|
+
|
613
|
+
# replace the new_child's graph
|
614
|
+
graph1_graph = graph1.graph
|
615
|
+
new_child_old_raw_graph = new_child.graph.raw_graph
|
616
|
+
new_child.graph = graph1_graph
|
617
|
+
|
618
|
+
# insert new_child into the graph1_graph
|
619
|
+
graph1_graph.add_to_graph(new_child)
|
620
|
+
graph1.add_child(new_child)
|
621
|
+
|
622
|
+
# add new_child's children into the graph1_graph
|
623
|
+
new_child_old_raw_graph.each do |child_graph_id,child_graph_details|
|
624
|
+
next if graph1_graph.find(child_graph_id)
|
625
|
+
term_to_add = child_graph_details[:object]
|
626
|
+
term_to_add.graph = graph1_graph
|
627
|
+
graph1_graph.add_to_graph(term_to_add)
|
628
|
+
end
|
629
|
+
|
630
|
+
# add the new_child relationships into the graph1_graph
|
631
|
+
new_child_old_raw_graph.each do |child_graph_id,child_graph_details|
|
632
|
+
child_graph_term = graph1_graph.find(child_graph_id)
|
633
|
+
child_graph_details[:parents].each { |parent_id| child_graph_term.add_parent( graph1_graph.find(parent_id) ) }
|
634
|
+
child_graph_details[:children].each { |child_id| child_graph_term.add_child( graph1_graph.find(child_id) ) }
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
# rinse and repeat for the children of graph1
|
639
|
+
graph1.children.each do |child|
|
640
|
+
merge_graphs( child, graph2[child.term_id] ) unless graph2[child.term_id].nil?
|
641
|
+
child.lock
|
642
|
+
end
|
643
|
+
|
644
|
+
return graph1
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|