ols 0.0.1 → 0.1.0

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