sync last commit
[bioperl-live.git] / Bio / Map / Mappable.pm
blobe3e2e6c99431485cec502edbff56957355d553ef
1 # $Id$
3 # BioPerl module for Bio::Map::Mappable
5 # Cared for by Sendu Bala <bix@sendu.me.uk>
7 # Copyright Sendu Bala
8 #
9 # You may distribute this module under the same terms as perl itself
11 # POD documentation - main docs before the code
13 =head1 NAME
15 Bio::Map::Mappable - An object representing a generic map element
16 that can have multiple locations in several maps.
18 =head1 SYNOPSIS
20 # a map element in two different positions on the same map
21 $map1 = Bio::Map::SimpleMap->new();
22 $position1 = Bio::Map::Position->new(-map => $map1, -value => 100);
23 $position2 = Bio::Map::Position->new(-map => $map1, -value => 200);
24 $mappable = Bio::Map::Mappable->new(-positions => [$position1, $position2] );
26 # add another position on a different map
27 $map2 = Bio::Map::SimpleMap->new();
28 $position3 = Bio::Map::Position->new(-map => $map2, $value => 50);
29 $mappable->add_position($position3);
31 # get all the places our map element is found, on a particular map of interest
32 foreach $pos ($mappable->get_positions($map1)) {
33 print $pos->value, "\n";
36 =head1 DESCRIPTION
38 This object handles the notion of a generic map element. Mappables are
39 entities with one or more positions on one or more maps.
41 This object is a pure perl implementation of L<Bio::Map::MappableI>. That
42 interface implements some of its own methods so check the docs there for
43 those.
45 =head1 FEEDBACK
47 =head2 Mailing Lists
49 User feedback is an integral part of the evolution of this and other
50 Bioperl modules. Send your comments and suggestions preferably to the
51 Bioperl mailing list. Your participation is much appreciated.
53 bioperl-l@bioperl.org - General discussion
54 http://bioperl.org/wiki/Mailing_lists - About the mailing lists
56 =head2 Reporting Bugs
58 Report bugs to the Bioperl bug tracking system to help us keep track
59 of the bugs and their resolution. Bug reports can be submitted via the
60 web:
62 http://bugzilla.open-bio.org/
64 =head1 AUTHOR - Sendu Bala
66 Email bix@sendu.me.uk
68 =head1 APPENDIX
70 The rest of the documentation details each of the object methods.
71 Internal methods are usually preceded with a _
73 =cut
75 # Let the code begin...
77 package Bio::Map::Mappable;
78 use strict;
79 use Bio::Map::Relative;
80 use Bio::Map::Position;
82 use base qw(Bio::Root::Root Bio::Map::MappableI);
84 =head2 new
86 Title : new
87 Usage : my $mappable = Bio::Map::Mappable->new();
88 Function: Builds a new Bio::Map::Mappable object
89 Returns : Bio::Map::Mappable
90 Args : -name => string : name of the mappable element
91 -id => string : id of the mappable element
93 =cut
95 sub new {
96 my ($class, @args) = @_;
97 my $self = $class->SUPER::new(@args);
99 my ($name, $id) = $self->_rearrange([qw(NAME ID)], @args);
100 $self->name($name) if $name;
101 $self->id($id) if $id;
103 return $self;
106 =head2 name
108 Title : name
109 Usage : $mappable->name($new_name);
110 my $name = $mappable->name();
111 Function: Get/Set the name for this Mappable
112 Returns : A scalar representing the current name of this Mappable
113 Args : none to get
114 string to set
116 =cut
118 sub name {
119 my $self = shift;
120 if (@_) { $self->{_name} = shift }
121 return $self->{_name} || '';
124 =head2 id
126 Title : id
127 Usage : my $id = $mappable->id();
128 $mappable->id($new_id);
129 Function: Get/Set the id for this Mappable.
130 Returns : A scalar representing the current id of this Mappable
131 Args : none to get
132 string to set
134 =cut
136 sub id {
137 my $self = shift;
138 if (@_) { $self->{_id} = shift }
139 return $self->{_id} || return;
142 =head2 in_map
144 Title : in_map
145 Usage : if ($mappable->in_map($map)) {...}
146 Function: Tests if this mappable is found on a specific map
147 Returns : boolean
148 Args : L<Bio::Map::MapI>
150 =cut
152 sub in_map {
153 my ($self, $query_map) = @_;
154 $self->throw("Must supply an argument") unless $query_map;
155 $self->throw("This is [$query_map], not an object") unless ref($query_map);
156 $self->throw("This is [$query_map], not a Bio::Map::MapI object") unless $query_map->isa('Bio::Map::MapI');
158 foreach my $map ($self->known_maps) {
159 ($map eq $query_map) && return 1;
162 return 0;
165 =head2 Comparison methods
167 =cut
169 =head2 equals
171 Title : equals
172 Usage : if ($mappable->equals($other_mappable)) {...}
173 my @equal_positions = $mappable->equals($other_mappable);
174 Function: Finds the positions in this mappable that are equal to any
175 comparison positions.
176 Returns : array of L<Bio::Map::PositionI> objects
177 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
178 this one to (mandatory)
179 arg #2 = optionally, one or more of the key => value pairs below
180 -map => MapI : a Bio::Map::MapI to only consider positions
181 on the given map
182 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
183 of each Position's relative position to the
184 thing described by that Relative
186 =cut
188 sub equals {
189 my $self = shift;
190 return $self->_compare('equals', @_);
193 =head2 less_than
195 Title : less_than
196 Usage : if ($mappable->less_than($other_mappable)) {...}
197 my @lesser_positions = $mappable->less_than($other_mappable);
198 Function: Finds the positions in this mappable that are less than all
199 comparison positions.
200 Returns : array of L<Bio::Map::PositionI> objects
201 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
202 this one to (mandatory)
203 arg #2 = optionally, one or more of the key => value pairs below
204 -map => MapI : a Bio::Map::MapI to only consider positions
205 on the given map
206 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
207 of each Position's relative position to the
208 thing described by that Relative
210 =cut
212 sub less_than {
213 my $self = shift;
214 return $self->_compare('less_than', @_);
217 =head2 greater_than
219 Title : greater_than
220 Usage : if ($mappable->greater_than($other_mappable)) {...}
221 my @greater_positions = $mappable->greater_than($other_mappable);
222 Function: Finds the positions in this mappable that are greater than all
223 comparison positions.
224 Returns : array of L<Bio::Map::PositionI> objects
225 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
226 this one to (mandatory)
227 arg #2 = optionally, one or more of the key => value pairs below
228 -map => MapI : a Bio::Map::MapI to only consider positions
229 on the given map
230 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
231 of each Position's relative position to the
232 thing described by that Relative
234 =cut
236 sub greater_than {
237 my $self = shift;
238 return $self->_compare('greater_than', @_);
241 =head2 overlaps
243 Title : overlaps
244 Usage : if ($mappable->overlaps($other_mappable)) {...}
245 my @overlapping_positions = $mappable->overlaps($other_mappable);
246 Function: Finds the positions in this mappable that overlap with any
247 comparison positions.
248 Returns : array of L<Bio::Map::PositionI> objects
249 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
250 this one to (mandatory)
251 arg #2 = optionally, one or more of the key => value pairs below
252 -map => MapI : a Bio::Map::MapI to only consider positions
253 on the given map
254 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
255 of each Position's relative position to the
256 thing described by that Relative
258 =cut
260 sub overlaps {
261 my $self = shift;
262 return $self->_compare('overlaps', @_);
265 =head2 contains
267 Title : contains
268 Usage : if ($mappable->contains($other_mappable)) {...}
269 my @container_positions = $mappable->contains($other_mappable);
270 Function: Finds the positions in this mappable that contain any comparison
271 positions.
272 Returns : array of L<Bio::Map::PositionI> objects
273 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
274 this one to (mandatory)
275 arg #2 = optionally, one or more of the key => value pairs below
276 -map => MapI : a Bio::Map::MapI to only consider positions
277 on the given map
278 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
279 of each Position's relative position to the
280 thing described by that Relative
282 =cut
284 sub contains {
285 my $self = shift;
286 return $self->_compare('contains', @_);
289 =head2 overlapping_groups
291 Title : overlapping_groups
292 Usage : my @groups = $mappable->overlapping_groups($other_mappable);
293 my @groups = Bio::Map::Mappable->overlapping_groups(\@mappables);
294 Function: Look at all the positions of all the supplied mappables and group
295 them according to overlap.
296 Returns : array of array refs, each ref containing the Bio::Map::PositionI
297 objects that overlap with each other
298 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
299 this one to, or an array ref of such objects (mandatory)
300 arg #2 = optionally, one or more of the key => value pairs below
301 -map => MapI : a Bio::Map::MapI to only consider positions
302 on the given map
303 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
304 of each Position's relative position to the
305 thing described by that Relative
306 -min_pos_num => int : the minimum number of positions that must
307 be in a group before it will be returned
308 [default is 1]
309 -min_mappables_num => int : the minimum number of different
310 mappables represented by the
311 positions in a group before it
312 will be returned [default is 1]
313 -min_mappables_percent => number : as above, but the minimum
314 percentage of input mappables
315 [default is 0]
316 -min_map_num => int : the minimum number of different
317 maps represented by the positions
318 in a group before it will be
319 returned [default is 1]
320 -min_map_percent => number : as above, but the minimum
321 percentage of maps known by the
322 input mappables [default is 0]
323 -require_self => 1|0 : require that at least one of the
324 calling object's positions be in
325 each group [default is 1, has no
326 effect when the second usage form
327 is used]
328 -required => \@mappables : require that at least one position
329 for each mappable supplied in this
330 array ref be in each group
332 =cut
334 sub overlapping_groups {
335 my $self = shift;
336 return $self->_compare('overlapping_groups', @_);
339 =head2 disconnected_intersections
341 Title : disconnected_intersections
342 Usage : @positions = $mappable->disconnected_intersections($other_mappable);
343 @positions = Bio::Map::Mappable->disconnected_intersections(\@mappables);
344 Function: Make the positions that are at the intersection of each group of
345 overlapping positions, considering all the positions of the supplied
346 mappables.
347 Returns : new Bio::Map::Mappable who's positions on maps are the calculated
348 disconnected unions
349 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
350 this one to, or an array ref of such objects (mandatory)
351 arg #2 = optionally, one or more of the key => value pairs below
352 -map => MapI : a Bio::Map::MapI to only consider positions
353 on the given map
354 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
355 of each Position's relative position to the
356 thing described by that Relative
357 -min_pos_num => int : the minimum number of positions that must
358 be in a group before the intersection will
359 be calculated and returned [default is 1]
360 -min_mappables_num => int : the minimum number of different
361 mappables represented by the
362 positions in a group before the
363 intersection will be calculated
364 and returned [default is 1]
365 -min_mappables_percent => number : as above, but the minimum
366 percentage of input mappables
367 [default is 0]
368 -min_map_num => int : the minimum number of different
369 maps represented by the positions
370 in a group before the intersection
371 will be calculated and returned
372 [default is 1]
373 -min_map_percent => number : as above, but the minimum
374 percentage of maps known by the
375 input mappables [default is 0]
376 -require_self => 1|0 : require that at least one of the
377 calling object's positions be in
378 each group [default is 1, has no
379 effect when the second usage form
380 is used]
381 -required => \@mappables : require that at least one position
382 for each mappable supplied in this
383 array ref be in each group
385 =cut
387 sub disconnected_intersections {
388 my $self = shift;
389 return $self->_compare('intersection', @_);
392 =head2 disconnected_unions
394 Title : disconnected_unions
395 Usage : my @positions = $mappable->disconnected_unions($other_mappable);
396 my @positions = Bio::Map::Mappable->disconnected_unions(\@mappables);
397 Function: Make the positions that are the union of each group of overlapping
398 positions, considering all the positions of the supplied mappables.
399 Returns : new Bio::Map::Mappable who's positions on maps are the calculated
400 disconnected unions
401 Args : arg #1 = L<Bio::Map::MappableI> OR L<Bio::Map::PositionI> to compare
402 this one to, or an array ref of such objects (mandatory)
403 arg #2 = optionally, one or more of the key => value pairs below
404 -map => MapI : a Bio::Map::MapI to only consider positions
405 on the given map
406 -relative => RelativeI : a Bio::Map::RelativeI to calculate in terms
407 of each Position's relative position to the
408 thing described by that Relative
409 -min_pos_num => int : the minimum number of positions that must
410 be in a group before the union will be
411 calculated and returned [default is 1]
412 -min_mappables_num => int : the minimum number of different
413 mappables represented by the
414 positions in a group before the
415 union will be calculated and
416 returned [default is 1]
417 -min_mappables_percent => number : as above, but the minimum
418 percentage of input mappables
419 [default is 0]
420 -min_map_num => int : the minimum number of different
421 maps represented by the positions
422 in a group before the union will
423 be calculated and returned
424 [default is 1]
425 -min_map_percent => number : as above, but the minimum
426 percentage of maps known by the
427 input mappables [default is 0]
428 -require_self => 1|0 : require that at least one of the
429 calling object's positions be in
430 each group [default is 1, has no
431 effect when the second usage form
432 is used]
433 -required => \@mappables : require that at least one position
434 for each mappable supplied in this
435 array ref be in each group
437 =cut
439 sub disconnected_unions {
440 my $self = shift;
441 return $self->_compare('union', @_);
444 # do a RangeI-related comparison by calling the corresponding PositionI method
445 # on all the requested Positions of our Mappables
446 sub _compare {
447 my ($self, $method, $input, @extra_args) = @_;
448 $self->throw("Must supply an object or array ref of them") unless ref($input);
449 $self->throw("Wrong number of extra args (should be key => value pairs)") unless @extra_args % 2 == 0;
450 my @compares = ref($input) eq 'ARRAY' ? @{$input} : ($input);
452 my %args = (-map => undef, -relative => undef, -min_pos_num => 1,
453 -min_mappables_num => 1, -min_mappables_percent => 0,
454 -min_map_num => 1, -min_map_percent => 0,
455 -require_self => 0, -required => undef, -min_overlap_percent => 0, @extra_args);
456 my $map = $args{-map};
457 my $rel = $args{-relative};
458 my $overlap = $args{-min_overlap_percent};
459 my $min_pos_num = $args{-min_pos_num};
460 my $min_pables_num = $args{-min_mappables_num};
461 if ($args{-min_mappables_percent}) {
462 my $mn = (@compares + (ref($self) ? 1 : 0)) / 100 * $args{-min_mappables_percent};
463 if ($mn > $min_pables_num) {
464 $min_pables_num = $mn;
467 my $min_map_num = $args{-min_map_num};
468 if ($args{-min_map_percent}) {
469 my %known_maps;
470 foreach my $pable (@compares, ref($self) ? ($self) : ()) {
471 foreach my $known ($pable->known_maps) {
472 $known_maps{$known->unique_id} = 1;
475 my $mn = scalar(keys %known_maps) / 100 * $args{-min_map_percent};
476 if ($mn > $min_map_num) {
477 $min_map_num = $mn;
480 my %required = map { $_ => 1 } $args{-required} ? @{$args{-required}} : ();
481 my (@mine, @yours);
483 if (ref($self)) {
484 @mine = $self->get_positions($map);
485 if ($args{-require_self}) {
486 @mine > 0 or return;
487 $required{$self} = 1;
490 my @required = keys %required;
492 foreach my $compare (@compares) {
493 if ($compare->isa('Bio::Map::PositionI')) {
494 push(@yours, $compare);
496 elsif ($compare->isa('Bio::Map::MappableI')) {
497 push(@yours, $compare->get_positions($map));
499 else {
500 $self->throw("This is [$compare], not a Bio::Map::MappableI or Bio::Map::PositionI");
503 @yours > 0 or return;
505 my @ok;
506 SWITCH: for ($method) {
507 /equals|overlaps|contains/ && do {
508 @mine > 0 or return;
509 foreach my $my_pos (@mine) {
510 foreach my $your_pos (@yours) {
511 if ($my_pos->$method($your_pos, undef, $rel)) {
512 push(@ok, $my_pos);
513 last;
517 last SWITCH;
519 /less_than|greater_than/ && do {
520 @mine > 0 or return;
521 if ($method eq 'greater_than') {
522 @mine = map { $_->[1] }
523 sort { $b->[0] <=> $a->[0] }
524 map { [$_->end($_->absolute_relative), $_] }
525 @mine;
526 @yours = map { $_->[1] }
527 sort { $b->[0] <=> $a->[0] }
528 map { [$_->end($_->absolute_relative), $_] }
529 @yours;
531 my $test_pos = shift(@yours);
533 foreach my $my_pos (@mine) {
534 if ($my_pos->$method($test_pos, $rel)) {
535 push(@ok, $my_pos);
537 else {
538 last;
542 if ($method eq 'greater_than') {
543 @ok = map { $_->[1] }
544 sort { $a->[0] <=> $b->[0] }
545 map { [$_->sortable, $_] }
546 @ok;
549 last SWITCH;
551 /overlapping_groups|intersection|union/ && do {
552 my @positions = (@mine, @yours);
553 my $start_pos = shift(@positions);
555 my $dr_able = $start_pos->disconnected_ranges(\@positions, $rel, $overlap) || return;
556 my @disconnected_ranges = $dr_able->get_positions;
558 #print "got ", scalar(@disconnected_ranges), " disconnected_ranges, first has range ", $disconnected_ranges[0]->toString, "\n";
560 #use Benchmark qw(:all);
561 #my $t0 = new Benchmark;
563 my %all_groups;
564 my %done_ranges;
565 for my $i (0..$#disconnected_ranges) {
566 my $range = $disconnected_ranges[$i];
567 my $range_string = $range->toString;
568 next if $done_ranges{$range_string};
569 $done_ranges{$range_string} = 1;
571 foreach my $pos ($start_pos, @positions) {
572 if ($pos->overlaps($range, undef, $rel)) {
573 $all_groups{$range_string}->{$pos} = $pos;
578 #my $t1 = new Benchmark;
579 #my $td = timediff($t1, $t0);
580 #print "grouping took: ",timestr($td),"\n";
582 # purge the temporary working (not $dr_able->purge_positions since
583 # that removes the element from each position, but leaves it on
584 # the map. *** need complete purge that removes position from
585 # memory...
586 foreach my $pos (@disconnected_ranges) {
587 my $map = $pos->map || next;
588 $map->purge_positions($pos);
591 my @groups;
592 GROUPS: foreach my $group_range (keys %all_groups) { # sort keys %all_groups might help, but causes test fails
593 my $group = $all_groups{$group_range};
594 my @group = values %{$group};
595 #print "* in group $group_range, there are ", scalar(@group), " members\n";
597 @group >= $min_pos_num or next;
598 @group >= $min_pables_num or next; # shortcut before having to work it out properly
599 @group >= $min_map_num or next; # shortcut before having to work it out properly
601 my %mappables;
602 foreach my $pos (@group) {
603 my $mappable = $pos->element || next;
604 $mappables{$mappable} = 1;
606 keys %mappables >= $min_pables_num or next;
608 my %maps;
609 foreach my $pos (@group) {
610 my $map = $pos->map || next;
611 $maps{$map->unique_id} = 1;
613 keys %maps >= $min_map_num or next;
615 foreach my $required (@required) {
616 exists $mappables{$required} or next GROUPS;
619 my @sorted = map { $_->[1] }
620 sort { $a->[0] <=> $b->[0] }
621 map { [$_->sortable, $_] }
622 @group;
623 push(@groups, \@sorted);
626 if ($method eq 'overlapping_groups') {
627 return @groups;
629 else {
630 foreach my $group (@groups) {
631 my $start_pos = shift(@{$group});
633 unless (@{$group}) {
634 # we'll consider the 'intersection' or 'union' of just
635 # one position as the position itself
636 push(@ok, Bio::Map::Position->new(-map => $start_pos->map,
637 -start => $start_pos->start,
638 -end => $start_pos->end));
640 else {
641 my @rel_arg = $method eq 'intersection' ? (undef, $rel) : ($rel);
642 my $result = $start_pos->$method($group, @rel_arg) || next;
643 push(@ok, $result->get_positions);
647 # assign all the positions to a result mappable
648 my $result = $self->new();
649 $result->add_position(@ok) if @ok; # add_position can actually take a list
651 return $result;
654 last SWITCH;
657 $self->throw("Unknown method '$method'");
660 return @ok;
663 =head2 tuple
665 Title : tuple
666 Usage : Do Not Use!
667 Function: tuple was supposed to be a private method; this method no longer
668 does anything
669 Returns : warning
670 Args : none
671 Status : deprecated, will be removed in next version
673 =cut
675 sub tuple {
676 my $self = shift;
677 $self->warn("The tuple method was supposed to be a private method, don't call it!");
680 =head2 annotation
682 Title : annotation
683 Usage : $mappable->annotation($an_col);
684 my $an_col = $mappable->annotation();
685 Function: Get the annotation collection (see Bio::AnnotationCollectionI)
686 for this annotatable object.
687 Returns : a Bio::AnnotationCollectionI implementing object, or undef
688 Args : none to get, OR
689 a Bio::AnnotationCollectionI implementing object to set
691 =cut
693 sub annotation {
694 my $self = shift;
695 if (@_) { $self->{_annotation} = shift }
696 return $self->{_annotation} || return;