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.
@@ -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