ols 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|