PageRenderTime 53ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/WormBase/Model.pm

https://bitbucket.org/tharris/wormbase/
Perl | 1421 lines | 852 code | 208 blank | 361 comment | 112 complexity | 6fd07568843c6985312d97cc1aac2127 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause
  1. package WormBase::Model;
  2. use strict;
  3. use warnings;
  4. use Bio::Graphics::Browser;
  5. use parent qw/Class::Accessor/;
  6. __PACKAGE__->mk_accessors(qw/current_object log/);
  7. our $AUTOLOAD;
  8. sub new {
  9. my ($this,$args) = @_;
  10. my $package = ref($this) || $this;
  11. my $self = bless $args,$package;
  12. $self->log->debug("Instantiating $package...");
  13. # The class arg may either be passed in via the arguments hash in the controller
  14. # or decided dynamically.
  15. # Fetch an object and stash it
  16. my $object = $args->{ace_model}->get_object($args->{class},$args->{request});
  17. $self->current_object($object) if $object;
  18. return $self;
  19. }
  20. =head1
  21. use Moose;
  22. extends 'WormBase';
  23. # Fetch an object and stash it
  24. has 'dbh_ace' => (is => 'ro');
  25. has 'gff_handle' => (is => 'ro');
  26. # , lazy => 1
  27. # , default => \&build_gff_handle);
  28. has 'name' => (is => 'ro');
  29. has 'class' => (is => 'ro');
  30. has 'current_object' => (is => 'ro'
  31. , lazy => 1
  32. , default => \&build_ace_object);
  33. sub build_ace_object {
  34. my $self = shift;
  35. my $dbh = $self->dbh_ace;
  36. my $class = $self->class;
  37. my $name = $self->name;
  38. my $object = $dbh->get_object($class,$name);
  39. return $object;
  40. }
  41. sub current_object {
  42. my $self = shift;
  43. # Fetch an object and stash it
  44. my $object = $self->ace_model->get_object($self->class,$self->request);
  45. $self->current_object($object) if $object;
  46. }
  47. =cut
  48. # Conditionally fetch the correct GFF DBH according to the
  49. # species of the current object.
  50. # dbh_gff should be the GFF ITSELF. Instead it's the model.
  51. # THIS should just return the dbh_gff. It's NOT
  52. sub dbh_gff {
  53. my ($self,$dsn) = @_;
  54. my $object = $self->current_object;
  55. my $species = $self->parsed_species();
  56. my $dbh = $dsn ? $self->{gff_model}->dbh($dsn) : $self->{gff_model}->dbh($species);
  57. return $dbh;
  58. }
  59. # Get a direct handle to the AceDB.
  60. sub dbh_ace { shift->{ace_model}->{dbh}; }
  61. #################################################
  62. #
  63. # COMMON MODEL ELEMENTS
  64. #
  65. # The following items occur in multiple
  66. # places in the ACedb model.
  67. #
  68. # Feel free to override any in a subclass.
  69. # If you do so, it may also be necessary
  70. # to provide a custom template.
  71. #
  72. ################################################
  73. =pod
  74. Mapping fields -> tags for more efficient autoloading:
  75. Approach 1:
  76. Name all "singleton" fields the same as the tag in the database.
  77. Use a hash that maps the tag name to the field name in the template
  78. Approach 2:
  79. =cut
  80. sub name {
  81. my $self = shift;
  82. my $object = $self->current_object;
  83. my $name = $object->name;
  84. return "$name";
  85. }
  86. # common_name defaults to the name of the object
  87. # This can be over-ridden as necessary.
  88. sub common_name {
  89. my $self = shift;
  90. my $name = $self->name;
  91. return "$name";
  92. }
  93. # Parse out species "from a Genus species" string.
  94. # Return g_species, used primarily for dynamically
  95. # selecting a data source based on species identifier.
  96. sub parsed_species {
  97. my ($self) = @_;
  98. my $object = $self->current_object;
  99. my $genus_species = $object->Species;
  100. my ($species) = $genus_species =~ /.* (.*)/;
  101. return lc(substr($genus_species,0,1)) . "_$species";
  102. }
  103. # Ugh. Can't autoload because singular vs plural
  104. sub remarks {
  105. my ($self) = @_;
  106. my $object = $self->current_object;
  107. my $remarks = $object->Remark;
  108. return "$remarks";
  109. }
  110. #################################################
  111. #
  112. # AGGREGATED INFORMATION
  113. #
  114. # The following methods serve to aggregate
  115. # information that occur in multiple Acedb
  116. # classes. They do not correspond to simple
  117. # tags.
  118. #
  119. ################################################
  120. sub reactome_knowledgebase {
  121. my ($self,$proteins) = @_;
  122. my @data;
  123. foreach my $protein (@$proteins) {
  124. my @db_entry = $protein->at('DB_info.Database');
  125. my ($reactome_name,$reactome_id);
  126. foreach (@db_entry) {
  127. next unless $_ eq 'Reactome';
  128. my @fields = $_->row;
  129. my $reactome_id = $fields[2];
  130. # VIEW
  131. # TODO: This needs to be in the template
  132. # push @rows,a({-href=>sprintf($fields[0]->URL_constructor,$reactome_id)},$reactome_id);
  133. push @data,$reactome_id;
  134. }
  135. }
  136. return \@data;
  137. }
  138. # Fetch all of the strains for a given object and extract some pertinent
  139. # info (like is it available from the CGC).
  140. sub strains {
  141. my ($self) = @_;
  142. my $object = $self->current_object;
  143. my @strains;
  144. foreach ($object->Strain(-filled=>1)) {
  145. my $cgc = ($_->Location eq 'CGC') ? 1 : 0;
  146. my @genes = $_->Gene;
  147. my $is_solo = (@genes == 1) ? 1 : 0;
  148. # Boolean flags for CGC availability, is a strain only carrying the gene
  149. push @strains,["$_",$cgc,$is_solo];
  150. }
  151. return \@strains;
  152. }
  153. # This could be generic. See also Variation.
  154. sub alleles {
  155. my ($self) = @_;
  156. my $object = $self->current_object;
  157. # Typically fetching alleles from gene, but might also be from variation
  158. $object = ($object->class eq 'Gene') ? $object : $object->Gene;
  159. my @clean;
  160. foreach ($object->Allele) {
  161. unless ($_->SNP) {
  162. push @clean,"$_";
  163. }
  164. }
  165. return \@clean;
  166. }
  167. sub polymorphisms {
  168. my ($self) = @_;
  169. my $object = $self->current_object;
  170. # Typically fetching alleles from gene, but might also be from variation
  171. $object = ($object->class eq 'Gene') ? $object : $object->Gene;
  172. my @poly;
  173. foreach ($object->Allele) {
  174. if ($_->SNP) {
  175. push @poly,"$_";
  176. }
  177. }
  178. return \@poly;
  179. }
  180. # Map a given ID to a species (This might also be a method instead of an ID)
  181. # Because of recpirocal BLASTing with elegans and briggsae and database XREFs
  182. # always try to use the ID of the hit first when doing identifications
  183. sub id2species {
  184. my ($self,$id) = @_;
  185. # Ordered according to (guesstimated) probability
  186. # It *seems* like this belongs in configuration but
  187. # it requires regexps...
  188. return 'Caenorhabditis briggsae' if ($id =~ /WP\:CBP/i || $id =~ /briggsae/ || $id =~ /^BP/);
  189. return 'Caenorhabditis elegans' if ($id =~ /worm/i || $id =~ /^WP/);
  190. return 'Caenorhabditis remanei' if ($id =~ /^RP\:/i || $id =~ /remanei/) ; # Temporary IDs for C. remanei
  191. return 'Drosophila melanogaster' if ($id =~ /fly.*\:CG/i);
  192. return 'Drosophila pseudoobscura' if ($id =~ /fly.*\:GA/i);
  193. return 'Saccharomyces cerevisiae' if ($id =~ /^SGD/i || $id =~ /yeast/);
  194. return 'Schizosaccharomyces pombe' if ($id =~ /pombe/);
  195. return 'Homo sapiens' if ($id =~ /ensembl/ || $id =~ /ensp\d+/);
  196. return 'Rattus norvegicus' if ($id =~ /rgd/i);
  197. return 'Anopheles gambiae' if ($id =~ /ensang/i);
  198. return 'Apis mellifera' if ($id =~ /ensapmp/i);
  199. return 'Canis familiaris' if ($id =~ /enscafp/i);
  200. return 'Danio rerio' if ($id =~ /ensdarp/i);
  201. return 'Dictyostelium discoideum' if ($id =~ /ddb\:ddb/i);
  202. return 'Fugu rubripes' if ($id =~ /sinfrup/i);
  203. return 'Gallus gallus' if ($id =~ /ensgalp/i);
  204. return 'Mus musculus' if ($id =~ /mgi/i);
  205. return 'Oryza sativa' if ($id =~ /^GR/i);
  206. return 'Pan troglodytes' if ($id =~ /ensptrp/i);
  207. return 'Plasmodium falciparum' if ($id =~ /pfalciparum/);
  208. return 'Tetraodon nigroviridis' if ($id =~ /gstenp/i);
  209. }
  210. # Generically fetch the genetic position for an object
  211. sub genetic_position {
  212. my ($self) = @_;
  213. my $object = $self->current_object;
  214. my ($chromosome,$position,$error);
  215. if ($object->Interpolated_map_position) {
  216. ($chromosome,$position,$error) = $object->Interpolated_map_position(1)->row;
  217. } else {
  218. ($chromosome,undef,$position,undef,$error) = eval{$object->Map(1)->row} or return;
  219. }
  220. my %data = ( chromosome => "$chromosome",
  221. position => "$position",
  222. error => "$error");
  223. return \%data;
  224. }
  225. # Generically fetch the interpolated genetic position for an object
  226. # This *might* be the same as genetic_position above.
  227. sub interpolated_position {
  228. my ($self) = @_;
  229. my $object = $self->current_object;
  230. my ($chrom,$pos,$error);
  231. for my $cds ($object->Corresponding_CDS) {
  232. ($chrom,$pos,$error) = $self->_get_interpolated_position($cds);
  233. last if $chrom;
  234. }
  235. # TODO: Save the formatting for the view
  236. my %data = (chromosome => "$chrom",
  237. position => "$pos",
  238. formatted_position => sprintf("%s:%2.2f",$chrom,$pos));
  239. return \%data;
  240. }
  241. # Provided with a GFF segment, return its genomic coordinates
  242. sub genomic_position {
  243. my ($self,$segment) = @_;
  244. my $abs_start = $segment->abs_start;
  245. my $abs_stop = $segment->abs_stop;
  246. ($abs_start,$abs_stop) = ($abs_stop,$abs_start) if ($abs_start > $abs_stop);
  247. my %data = (
  248. chromosome => $segment->abs_ref,
  249. abs_start => $abs_start,
  250. abs_stop => $abs_stop,
  251. start => $segment->start,
  252. stop => $segment->stop,
  253. );
  254. return \%data;
  255. }
  256. # CONVERTED TO HERE.
  257. # Generically construct a GBrowse img
  258. # Args: Bio::DB::GFF segment,species, [ tracks ], { options }, width
  259. # This is typically called by genomic_environs() ina subclass...
  260. sub build_gbrowse_img {
  261. my ($self,$segment,$tracks,$options,$width) = @_;
  262. my $species = $self->parsed_species();
  263. # open the browser for drawing pictures
  264. my $BROWSER = Bio::Graphics::Browser->new or die;
  265. # NOTE! The path to the configuration directory MUST be supplied by W::M::* adaptor!
  266. $BROWSER->read_configuration($self->{gbrowse_conf_dir}) or die "Couldn't read or find the gbrowse configuration directory";
  267. $BROWSER->source($species);
  268. $BROWSER->width($width || '500');
  269. $BROWSER->config->set('general','empty_tracks' => 'suppress');
  270. $BROWSER->config->set('general','keystyle' => 'none');
  271. my $absref = $segment->abs_ref;
  272. my $absstart = $segment->abs_start;
  273. my $absend = $segment->abs_end;
  274. ($absstart,$absend) = ($absend,$absstart) if $absstart>$absend;
  275. my $length = $segment->length;
  276. # add another 10% to left and right
  277. my $start = int($absstart - 0.1*$length);
  278. my $stop = int($absend + 0.1*$length);
  279. my $db = $segment->factory;
  280. my ($new_segment) = $db->segment(-name=>$absref,
  281. -start=>$start,
  282. -stop=>$stop);
  283. my ($img,$junk) = $BROWSER->render_panels({segment => $new_segment,
  284. options => \%$options,
  285. labels => $tracks,
  286. title => "Genomic segment: $absref:$absstart..$absend",
  287. keystyle => 'between',
  288. do_map => 0,
  289. drag_n_drop => 0,
  290. });
  291. $img =~ s|/Users/todd/Documents/projects/wormbase/website/trunk/root||g;
  292. $img =~ s/border="0"/border="1"/;
  293. $img =~ s/detailed view/browse region/g;
  294. $img =~ s/usemap=\S+//;
  295. my %data = (
  296. img => $img,
  297. start => $start,
  298. stop => $stop,
  299. species => $species,
  300. chromosome => $absref);
  301. return \%data;
  302. }
  303. # Fetch all of the best_blastp_matches for a list of proteins.
  304. # Used for genes and proteins
  305. sub best_blastp_matches {
  306. my ($self,$proteins) = @_;
  307. # current_object might already be a protein. If a gene, it will supply proteins.
  308. push @$proteins,$self->current_object unless @$proteins;
  309. return unless @$proteins;
  310. my ($biggest) = sort {$b->Peptide(2)<=>$a->Peptide(2)} @$proteins;
  311. my @pep_homol = $biggest->Pep_homol;
  312. my $length = $biggest->Peptide(2);
  313. my @hits;
  314. # find the best pep_homol in each category
  315. my %best;
  316. return "" unless @pep_homol;
  317. for my $hit (@pep_homol) {
  318. next if $hit eq $biggest; # Ignore self hits
  319. my ($method,$score) = $hit->row(1) or next;
  320. my $prev_score = (!$best{$method}) ? $score : $best{$method}{score};
  321. $prev_score = ($prev_score =~ /\d+\.\d+/) ? $prev_score .'0' : "$prev_score.0000";
  322. my $curr_score = ($score =~ /\d+\.\d+/) ? $score . '0' : "$score.0000";
  323. $best{$method} = {score=>$score,hit=>$hit,adjusted_score=>$curr_score} if !$best{$method} || $prev_score < $curr_score;
  324. }
  325. foreach (values %best) {
  326. my $covered = $self->_covered($_->{score}->col);
  327. $_->{covered} = $covered;
  328. }
  329. # NOT HANDLED YET
  330. # my $links = Configuration->Protein_links;
  331. my %seen; # Display only one hit / species
  332. # I think the perl glitch on x86_64 actually resides *here*
  333. # in sorting hash values. But I can't replicate this outside of a
  334. # mod_perl environment
  335. # Adding the +0 forces numeric context
  336. foreach (sort {$best{$b}{adjusted_score}+0 <=>$best{$a}{adjusted_score}+0 } keys %best) {
  337. my $method = $_;
  338. my $hit = $best{$_}{hit};
  339. # Try fetching the species first with the identification
  340. # then method then the embedded species
  341. my $species = $self->id2species($hit);
  342. $species ||= $self->id2species($method);
  343. # Not all proteins are populated with the species
  344. $species ||= $best{$method}{hit}->Species;
  345. $species =~ s/^(\w)\w* /$1. /;
  346. my $description = $best{$method}{hit}->Description || $best{$method}{hit}->Gene_name;
  347. if ($method =~ /worm|briggsae/) {
  348. $description ||= eval{$best{$method}{hit}->Corresponding_CDS->Brief_identification};
  349. # Kludge: display a description using the CDS
  350. if (!$description) {
  351. for my $cds (eval { $best{$method}{hit}->Corresponding_CDS }) {
  352. next if $cds->Method eq 'history';
  353. $description ||= "gene $cds";
  354. }
  355. }
  356. }
  357. # Ignore mass spec hits
  358. next if ($hit =~ /^MSP/);
  359. next if ($seen{$species}++);
  360. if ($hit =~ /(\w+):(.+)/) {
  361. my $prefix = $1;
  362. my $accession = $2;
  363. # Try fetching accessions directly from the protein object
  364. my @dbs = $hit->Database;
  365. foreach my $db (@dbs) {
  366. if ($db eq 'FLYBASE') {
  367. foreach my $col ($db->col) {
  368. if ($col eq 'FlyBase_gn') {
  369. $accession = $col->right;
  370. last;
  371. }
  372. }
  373. }
  374. }
  375. # NOT HANDLED YET!
  376. # my $link_rule = $links->{$prefix};
  377. my $link_rule = '%s';
  378. my $url = sprintf($link_rule,$accession);
  379. # TH: 1/2006 - remanei not yet in the database but blast hits available
  380. # Generate links to the remanei browser
  381. # This will not work for mirror sites, of course...
  382. if ($species =~ /remanei/) {
  383. $accession =~ s/^RP://;
  384. $hit = qq{<a href="http://dev.wormbase.org/db/seq/gbrowse/remanei/?name=$accession"</a>$accession</a>};
  385. $hit .= qq{<br><i>Note: <b>C. remanei</b> predictions are based on an early assembly of the genome. Predictions subject to possibly dramatic revision pending final assembly. Sequences available on the <a href="ftp://ftp.wormbase.org/pub/wormbase/genomes/remanei">WormBase FTP site</a>.};
  386. } else {
  387. $hit = qq{<a href="$url" -target="_blank">$hit</a>};
  388. }
  389. }
  390. push @hits,[$species,$hit,$description,
  391. sprintf("%7.3g",10**-$best{$_}{score}),
  392. sprintf("%2.1f%%",100*($best{$_}{covered})/$length)];
  393. }
  394. return \@hits;
  395. }
  396. #################################################
  397. # Phenotypes
  398. # (Was: DisplayPhenotypes, is_NOT_phene, FormatPhenotypeHash, etc)
  399. # (ie used in RNAi, Seq, Variation)
  400. #################################################
  401. # Return a list of phenotypes observed
  402. sub phenotypes_observed {
  403. my ($self) = @_;
  404. my $object = $self->current_object;
  405. my $phenes = $self->_get_phenotypes($object);
  406. return $phenes;
  407. }
  408. # Return a list of phenotypes not observed
  409. sub phenotypes_not_observed {
  410. my ($self) = @_;
  411. my $object = $self->current_object;
  412. my $phenes = $self->_get_phenotypes($object,'NOT');
  413. return $phenes;
  414. }
  415. sub _get_phenotypes {
  416. my ($self,$object,$not) = @_;
  417. my $positives = [];
  418. my $negatives = [];
  419. my (@phenotypes) = $object->Phenotype;
  420. my $data = $self->_parse_hash(\@phenotypes);
  421. ($positives,$negatives) = $self->_is_NOT_phene($data);
  422. my $parsed;
  423. if ($not) {
  424. $parsed = $self->_parse_phenotype_hash($negatives);
  425. } else {
  426. $parsed = $self->_parse_phenotype_hash($positives);
  427. }
  428. return $parsed;
  429. }
  430. # Determine which of a list of Phenotypes are NOTs
  431. # Return a sorted list of positive/not positive phenotypes
  432. sub _is_NOT_phene {
  433. my ($self,$data) = @_;
  434. my $positives = [];
  435. my $negatives = [];
  436. foreach my $entry (@$data) {
  437. if ($entry->{is_not}) {
  438. push @$negatives,$entry;
  439. } else {
  440. push @$positives,$entry;
  441. }
  442. }
  443. return ($positives,$negatives);
  444. }
  445. # Return the best name for a phenotype object. This is really common_name...
  446. # Pick the best display new for new Phenotype-ontology objects
  447. # and append a short name if one exists
  448. sub best_phenotype_name {
  449. my ($self,$phenotype) = @_;
  450. my $name = ($phenotype =~ /WBPheno.*/) ? $phenotype->Primary_name : $phenotype;
  451. $name =~ s/_/ /g;
  452. $name .= ' (' . $phenotype->Short_name . ')' if $phenotype->Short_name;
  453. return $name;
  454. }
  455. # Was: ElegansSubs::AlleleDescription and MutantPhenotype
  456. # TODO: Need to attach the evidence to each Remark
  457. # TODO: This probably should be a function of the VIEW
  458. sub phenotype_remark {
  459. my ($self) = @_;
  460. my $object = $self->current_object;
  461. # Some inconsistency in Ace models here
  462. my @remarks = $object->Remark,
  463. eval { $object->Phenotype_remark },
  464. eval { $object->Phenotype };
  465. my $formatted_remarks = $self->_cross_reference_remarks(\@remarks);
  466. # push @desc,GetEvidenceNew(-object => $allele->Phenotype_remark,
  467. # -format => 'inline',
  468. # -display_label => 1);
  469. # push @desc,GetEvidenceNew(-object => $allele->Remark,
  470. # -format => 'inline',
  471. # -display_label => 1);
  472. # return unless @desc;
  473. # return join(br,@desc); # . '.'; Don't add punctuation, Mary Ann does
  474. return $formatted_remarks;
  475. }
  476. # This was MutantPhenotype
  477. sub _cross_reference_remarks {
  478. my ($self,$remarks) = @_;
  479. # cross-reference laboratories
  480. foreach my $d (@$remarks) {
  481. $d =~ s/;\s+([A-Z]{2})(?=[;\]])
  482. /"; ".ObjectLink($1,undef,'Laboratory')
  483. /exg;
  484. # cross-reference genes
  485. #### $d =~ s/\b([a-z]+-\d+)\b
  486. #### /ObjectLink($1,undef,'Locus')
  487. #### /exg;
  488. # cross-reference other stuff
  489. #### my %xref = map {$_=>$_} @xref;
  490. #### $d =~ s/\b(.+?)\b/$xref{$1} ? ObjectLink($xref{$1}) : $1/gie;
  491. }
  492. return $remarks;
  493. }
  494. # History for the ?Gene class. Too bad this isn't generic...
  495. sub history {
  496. my $self = shift;
  497. my $object = $self->current_object;
  498. my $stash;
  499. my @history = $object->History;
  500. foreach my $history (@history) {
  501. my $type = $history;
  502. $type =~ s/_ / /g;
  503. my @versions = $history->col;
  504. foreach my $version (@versions) {
  505. my ($vers,$date,$curator,$event,$action,$remark,$target_object,$person);
  506. # Let's just display date that comes from Version_change entries (obsolete tags at top-level of ?Gene still exist)
  507. if ($history eq 'Version_change') {
  508. ($vers,$date,$curator,$event,$action,$remark) = $version->row;
  509. # For some cases, the remark is actually a gene object
  510. if ($action eq 'Merged_into' || $action eq 'Acquires_merge'
  511. || $action eq 'Split_from' || $action eq 'Split_into') {
  512. $target_object = $remark;
  513. }
  514. push @{$stash},{ version => $version,
  515. action => $action,
  516. date => $date,
  517. curator => $curator,
  518. target_object => $remark,
  519. type => $type,};
  520. }
  521. }
  522. }
  523. return $stash;
  524. }
  525. # TODO: This should be in SUPER
  526. # TODO: Part of View? Part of Model::Super?
  527. sub fasta {
  528. my ($self,$name,$protein) = @_;
  529. $protein ||= '';
  530. my @markup;
  531. for (my $i=0; $i < length $protein; $i += 10) {
  532. push (@markup,[$i,$i % 80 ? ' ':"\n"]);
  533. }
  534. $self->markup(\$protein,\@markup);
  535. return $protein;
  536. }
  537. # insert HTML tags into a string without disturbing order
  538. sub markup {
  539. my ($self,$string,$markups) = @_;
  540. for my $m (sort {$b->[0]<=>$a->[0]} @$markups) { #insert later tags first so position remains correct
  541. my ($position,$markup) = @$m;
  542. next unless $position <= length $$string;
  543. substr($$string,$position,0) = $markup;
  544. }
  545. }
  546. #################################################
  547. #
  548. # INTERNAL METHODS
  549. #
  550. # The following items occur often enough
  551. # throughout the Model to warrant inclusion here.
  552. #
  553. ################################################
  554. #################################################
  555. # Hash parsing and formatting
  556. # Was: ParseHash FormatEvidenceHash, etc
  557. #
  558. # I'm not sure where this belongs.
  559. # On one hand, we should be able to deliver this
  560. # information via webservices. That is, it shouldn't
  561. # be locked up in the view.
  562. #
  563. # On the other, it seems much more view specific.
  564. #
  565. # 2. I should just have top level categories like
  566. # parse_evidence_hash, parse_molecular_change_hash, etc.
  567. # These would return data structure suitable for display
  568. # in the view.
  569. #
  570. # In fact, these could also be actions and ajax targets (ie /parse_hash/node, maybe)
  571. #
  572. # I'd also like to make the parse_hash method private so that it is
  573. # only called by the parse* methods. Unfortunately, some formatting
  574. # needs just the raw hash -- there is code redundancy with duplication
  575. # in parsing, formatting, view decisions, etc. See for example,the
  576. # RNAi phenoypes section of Gene.pm
  577. #
  578. # To further confuse matters, I have a parse_hash, evidence
  579. # formatting, etc as part of the view. Clearly, this requires
  580. # additional thought.
  581. #
  582. #################################################
  583. =pod
  584. =item _parse_hash(@params)
  585. Generically parse an evidence, paper, or molecular info
  586. hash from a node of an object tree.
  587. Options
  588. -node A node of an object tree (or an array reference of nodes)
  589. If an array reference of nodes is passed, the resulting data structure
  590. will be an array of structures.
  591. Returns
  592. A data structure suitable for further parsing/display or
  593. null if no Evidence hash exists to the right of the provided node.
  594. =cut
  595. #'
  596. sub _parse_hash {
  597. my ($self,$nodes) = @_;
  598. # Mimic the passing of an array reference. Blech.
  599. $nodes = [$nodes] unless ref $nodes eq 'ARRAY';
  600. # The data structure - a hash of hashes, each pointing to an array
  601. my $data = [];
  602. # Collect all the hashes available for each node
  603. foreach my $node (@$nodes) {
  604. # Save all the top level tags as keys in a perl
  605. # hash for easier parsing and formatting
  606. my %hash = map { $_ => $_ } eval { $node->col };
  607. my $is_not = 1 if (defined $hash{Not}); # Keep track if this is a Not Phene annotation
  608. push @{$data},{ node => $node,
  609. hash => \%hash,
  610. is_not => $is_not || 0,
  611. };
  612. }
  613. return $data;
  614. }
  615. # NOT DONE YET!
  616. sub _parse_evidence_hash {
  617. my @p = @_;
  618. my ($data,$format,$display_tag,$link_tag,$display_label,$detail) =
  619. rearrange([qw/DATA FORMAT DISPLAY_TAG LINK_TAG DISPLAY_LABEL DETAIL/],@p);
  620. my @rows; # Each row in the table corresponds to an object (each row is stringified)
  621. my $join = ($format eq 'table') ? '<br>' : ', ';
  622. my $all_evidence = {};
  623. foreach my $entry (@$data) {
  624. my $hash = $entry->{hash};
  625. my $node = $entry->{node};
  626. # Conditionally format the data for each type of evidence
  627. foreach my $key (keys %$hash) {
  628. my $type = $hash->{$key};
  629. # Suppress the display of Curator_confirmed
  630. next if $type eq 'Curator_confirmed';
  631. # Just grab the first level entries for each.
  632. # For the evidence hash, Accession_evidence and Author_evidence
  633. # have additional information
  634. my @sources = eval { $type->col };
  635. # Add appropriate markup for each type of Evidence seen
  636. # Lots of redundancy here - first we parse the data, then add primary formatting
  637. # then secondary formatting (ie table, etc)
  638. # This could all be much cleaner (albeit less flexible) with templates
  639. if ($type eq 'Paper_evidence') {
  640. #!! my @papers = _format_paper_evidence(\@sources);
  641. #!! $data = join($join,@papers);
  642. } elsif ($type eq 'Published_as') {
  643. #!! $data = join($join,map { ObjectLink($_,undef,'_blank') } @sources);
  644. } elsif ($type eq 'Person_evidence' || $type eq 'Curator_confirmed') {
  645. #!! $data = join($join,map {ObjectLink($_->Standard_name,undef,'_blank')} @sources);
  646. } elsif ($type eq 'Author_evidence') {
  647. #!! $data = join($join,map { a({-href=>'/db/misc/author?name='.$_,-target=>'_blank'},$_) } @sources);
  648. } elsif ($type eq 'Accession_evidence') {
  649. foreach my $entry (@sources) {
  650. my ($database,$accession) = $entry->row;
  651. #!! my $accession_links ||= Configuration->Protein_links; # misnomer
  652. #!! my $link_rule = $accession_links->{$database};
  653. #!! $data = $link_rule ? a({-href=>sprintf($link_rule,$accession),
  654. #!! -target=>'_blank'},"$database:$accession")
  655. #!! : ObjectLink($accession,"$database:$accession");
  656. }
  657. } elsif ($type eq 'Protein_id_evidence') {
  658. #!! $data = join($join,map { a({-href=>Configuration->Entrezp},$_) } @sources);
  659. # Lots of generic entries that just need to be linked
  660. } elsif ($type eq 'GO_term_evidence' || $type eq 'Laboratory_evidence') {
  661. #!! $data = join($join,map {ObjectLink($_) } @sources);
  662. } elsif ($type eq 'Expr_pattern_evidence') {
  663. #!! $data = join($join,map {ObjectLink($_) } @sources);
  664. } elsif ($type eq 'Microarray_results_evidence') {
  665. #!! $data = join($join,map {ObjectLink($_) } @sources);
  666. } elsif ($type eq 'RNAi_evidence') {
  667. #!! $data = join($join,map {ObjectLink($_,$_->History_name ? $_ . ' (' . $_->History_name . ')' : $_) } @sources);
  668. } elsif ($type eq 'Gene_regulation_evidence') {
  669. #!! $data = join($join,map {ObjectLink($_) } @sources);
  670. } elsif ($type eq 'CGC_data_submission') {
  671. } elsif ($type =~ /Inferred_automatically/i) {
  672. #!! $data = join($join,map { $_ } @sources);
  673. } elsif ($type eq 'Date_last_updated') {
  674. #!! ($data) = @sources;
  675. #!! $data =~ s/\s00:00:00//;
  676. }
  677. $type =~ s/_/ /g;
  678. # Retain $node again since this is an object
  679. push @{$all_evidence->{$type}},
  680. { type => $type,
  681. data => $data,
  682. node => $node,
  683. };
  684. }
  685. }
  686. # Format the evidence as requested
  687. my $return;
  688. if ($format eq 'table') {
  689. foreach my $tag (keys %$all_evidence) {
  690. my @evidence = @{$all_evidence->{$tag}};
  691. my $table =
  692. start_table()
  693. . TR(th('Evidence type')
  694. . th('Source'));
  695. my $count = 0;
  696. foreach (@evidence) {
  697. my $node = $_->{node};
  698. # Only need to do this for the first iteration
  699. if ($count == 0) {
  700. if ($display_tag) {
  701. $link_tag = 1 if $node eq 'Evidence'; # hack for cases in which evidence is attached
  702. #!! my $description = $link_tag ? $node :
  703. #!! ref $node && $node->class eq 'Gene_name' ?
  704. #!! a({-href=>Object2URL(GeneName2Gene($node))},$node)
  705. #!! : ObjectLink($node);
  706. #!! $return .= $description;
  707. }
  708. $count++;
  709. $return .= h3('Supported by:');
  710. }
  711. my $type = $_->{type};
  712. my $data = $_->{data};
  713. #!! $table .= TR(td({-valign=>'top'},$type),
  714. #!! td($data));
  715. }
  716. #!! $table .= end_table();
  717. #!! $return .= $table;
  718. }
  719. } else {
  720. # Returning stringified form of evidence
  721. my @entries;
  722. foreach my $tag (keys %$all_evidence) {
  723. my @evidence = @{$all_evidence->{$tag}};
  724. my $count = 0;
  725. foreach (@evidence) {
  726. my $node = $_->{node};
  727. if ($count == 0) { # necessary on first iteration only. stoopid, I know
  728. if ($display_tag) {
  729. $link_tag = 1 if $node eq 'Evidence'; # hack for cases in which evidence is attached
  730. #!! my $description = $link_tag ? $node :
  731. #!! ref $node && $node->class eq 'Gene_name' ?
  732. #!! a({-href=>Object2URL(GeneName2Gene($node))},$node)
  733. #!! : ObjectLink($node);
  734. #!! $return .= $description;
  735. }
  736. $count++;
  737. }
  738. my $type = $_->{type};
  739. my $data = $_->{data};
  740. push @entries,($display_label) ? "via " . lc($type) . ': ' . $data : $data;
  741. }
  742. }
  743. $return .= join('; ',@entries);
  744. }
  745. return undef unless $return;
  746. return $return;
  747. }
  748. ## Data is a collection of one or more phenotype
  749. ## hashes with top-level tags already extracted
  750. ## THIS COULD MOVE TO THE VIEW...
  751. sub _parse_phenotype_hash {
  752. my ($self,$data) = @_;
  753. # These tags have a single entry following them
  754. # They should *not* have any evidence hashes, either
  755. # The contents of these entries can be fetched as
  756. # $tag->col
  757. my %evidence_only = map { $_ => 1 }
  758. qw/
  759. Not
  760. Recessive
  761. Semi_dominant
  762. Dominant
  763. Haplo_insufficient
  764. Paternal
  765. /;
  766. my %simple = map { $_ => 1 }
  767. qw/
  768. Quantity_description
  769. /;
  770. my %nested = map { $_ => 1 }
  771. qw/
  772. Penetrance
  773. Quantity
  774. Loss_of_function
  775. Gain_of_function
  776. Other_allele_type
  777. Temperature_sensitive
  778. Maternal
  779. Phenotype_assay
  780. /;
  781. my %is_row = map { $_ => 1 } qw/Quantity Range/;
  782. # Prioritize display of tags
  783. my @tags = qw/
  784. Not
  785. Penetrance Recessive Semi_dominant Dominant
  786. Haplo_insufficient
  787. Loss_of_function
  788. Gain_of_function
  789. Other_allele_type
  790. Temperature_sensitive
  791. Maternal
  792. Paternal
  793. Phenotype_assay
  794. Quantity_description
  795. Quantity
  796. Paper_evidence
  797. Person_evidence
  798. Remark
  799. /;
  800. my $stash = [];
  801. foreach my $entry (@$data) {
  802. my @this_data = ();
  803. my $hash = $entry->{hash};
  804. my $node = $entry->{node}; # Node is the originating object
  805. foreach my $tag_priority (@tags) {
  806. next unless (my $tag = $hash->{$tag_priority});
  807. my $formatted_tag = $tag;
  808. $formatted_tag =~ s/_/ /g;
  809. # Fetch the first entries to the right of each tag
  810. my @sources = eval { $tag->col };
  811. # Add appropriate markup for each tag seen
  812. # Lots of redundancy here - first we parse the data, then add primary formatting
  813. # then secondary formatting (ie table, etc)
  814. if ($tag eq 'Paper_evidence') {
  815. # We will format the papers elswhere
  816. # @sources = _format_paper_evidence(\@sources);
  817. } elsif ($tag eq 'Person_evidence' || $tag eq 'Curator_confirmed') {
  818. } elsif (defined $evidence_only{$tag}) {
  819. @sources = ( $tag );
  820. } elsif ($tag eq 'Remark' || $tag eq 'Quantity_description') {
  821. } elsif ($tag eq 'Phenotype_assay') {
  822. my @parsed;
  823. # Step into the Phenotype_assay object, displaying select tags.
  824. if (@sources) {
  825. my ($cell,$evidence);
  826. foreach my $condition (@sources) {
  827. if ($data) {
  828. # TODO: FETCH THE EVIDENCE FROM $data
  829. }
  830. push @parsed,"$condition: $data";
  831. }
  832. @sources = @parsed;
  833. }
  834. # Handle tags that contain substructure
  835. } elsif (defined $nested{$tag}) {
  836. my @subtags = $tag->col;
  837. @subtags = $tag if $tag eq 'Quantity';
  838. my @cells;
  839. foreach my $subtag (@subtags) {
  840. # Ignore the value if we have an Evidence hash
  841. # to the right. All set to fetch evidence
  842. my ($value,$evi);
  843. if ($subtag->right =~ /evidence/) {
  844. } else {
  845. # HACK - Range and Quantity are rows
  846. if (defined ($is_row{$subtag})) {
  847. my (@values) = $subtag->row;
  848. $value = join("-",$values[1],$values[2]);
  849. $value = '100%' if $value eq '100-100';
  850. } else {
  851. $value = $subtag->right;
  852. }
  853. }
  854. $subtag =~ s/_/ /g;
  855. $formatted_tag = "$formatted_tag: $subtag";
  856. @sources = ($value);
  857. }
  858. }
  859. push @this_data,[$formatted_tag,\@sources];
  860. }
  861. push @{$stash},{ node => $node,
  862. rows => \@this_data };
  863. }
  864. return $stash;
  865. }
  866. # THIS PROBABLY BELONGS AS A COMPONENT OF THE VIEW INSTEAD OF THE MODEL
  867. sub _parse_molecular_change_hash {
  868. my ($self,$entry,$tag) = @_;
  869. # Generically parse the hash
  870. my $data = $self->_parse_hash($entry);
  871. return unless keys %{$data} >= 1; # Nothing to build a table from
  872. my @types = qw/Missense Nonsense Frameshift Silent Splice_site/;
  873. my @locations = qw/Intron Coding_exon Noncoding_exon Promoter UTR_3 UTR_5 Genomic_neighbourhood/;
  874. # Select items that we will try and translate
  875. # Currently, this needs to be
  876. # 1. Affects Predicted_CDS
  877. # 2. A missense or nonsense allele
  878. # 3. Contained in a coding_exon
  879. my %parameters_seen;
  880. my %do_translation = map { $_ => 1 } (qw/Missense Nonsense/);
  881. # Under no circumstances try and translate the following
  882. my %no_translation = map { $_ => 1 } (qw/Frameshift Deletion Insertion/);
  883. # The following entries should be examined for the presence
  884. # of associated Evidence hashes
  885. my @with_evidence =
  886. qw/
  887. Missense
  888. Silent
  889. Nonsense
  890. Splice_site
  891. Frameshift
  892. Intron
  893. Coding_exon
  894. Noncoding_exon
  895. Promoter
  896. UTR_3
  897. UTR_5
  898. Genomic_neighbourhood
  899. /;
  900. my (@protein_effects,@contained_in);
  901. foreach my $entry (@$data) {
  902. my $hash = $entry->{hash};
  903. my $node = $entry->{node};
  904. # Conditionally format the data for each type of evidence
  905. # Curation often has the type of change and its location
  906. # What type of change is this?
  907. foreach my $type (@types) {
  908. my $obj = $hash->{$type};
  909. my @data = eval { $obj->row };
  910. next unless @data;
  911. my $clean_tag = ucfirst($type);
  912. $clean_tag =~ s/_/ /g;
  913. $parameters_seen{$type}++;
  914. my ($pos,$text,$evi,$evi_method,$kind);
  915. if ($type eq 'Missense') {
  916. ($type,$pos,$text,$evi) = @data;
  917. } elsif ($type eq 'Nonsense' || $type eq 'Splice_site') {
  918. ($type,$kind,$text,$evi) = @data;
  919. } elsif ($type eq 'Frameshift') {
  920. ($type,$text,$evi) = @data;
  921. } else {
  922. ($type,$text,$evi) = @data;
  923. }
  924. if ($evi) {
  925. ### ($evi_method) = GetEvidenceNew(-object => $text,
  926. ### -format => 'inline',
  927. ### -display_label => 1,
  928. ### );
  929. }
  930. push @protein_effects,[$clean_tag,$pos || undef,$text,
  931. $evi_method ? " ($evi_method)" : undef];
  932. }
  933. # Where is this change located?
  934. foreach my $location (@locations) {
  935. my $obj = $hash->{$location};
  936. my @data = eval { $obj->col };
  937. next unless @data;
  938. $parameters_seen{$location}++;
  939. #####
  940. ##### my ($evidence) = GetEvidenceNew(-object => $obj,
  941. ##### -format => 'inline',
  942. ##### -display_label => 1
  943. ##### );
  944. my $evidence;
  945. my $clean_tag = ucfirst($location);
  946. $clean_tag =~ s/_/ /g;
  947. push @contained_in,[$clean_tag,undef,undef,
  948. $evidence ? " ($evidence)" : undef];
  949. }
  950. }
  951. my $do_translation;
  952. foreach (keys %parameters_seen) {
  953. $do_translation++ if (defined $do_translation{$_} && !defined $no_translation{$_});
  954. }
  955. return (\@protein_effects,\@contained_in,$do_translation);
  956. }
  957. # get the interpolated position of a sequence on the genetic map
  958. # returns ($chromosome, $position,$error)
  959. # position is in genetic map coordinates
  960. # This MIGHT also be the actual experimental position
  961. sub _get_interpolated_position {
  962. my ($self,$object) = @_;
  963. $object ||= $self->current_object;
  964. if ($object){
  965. if ($object->class eq 'CDS') {
  966. # Is it a query
  967. # wquery/genelist.def:Tag Locus_genomic_seq
  968. # wquery/new_wormpep.def:Tag Locus_genomic_seq
  969. # wquery/wormpep.table.def:Tag Locus_genomic_seq
  970. # wquery/wormpepCE_DNA_Locus_OtherName.def:Tag Locus_genomic_seq
  971. # Fetch the interpolated map position if it exists...
  972. # if (my $m = $object->get('Interpolated_map_position')) {
  973. if (my $m = eval {$object->get('Interpolated_map_position') }) {
  974. #my ($chromosome,$position,$error) = $object->Interpolated_map_position(1)->row;
  975. my ($chromosome,$position) = $m->right->row;
  976. return ($chromosome,$position) if $chromosome;
  977. } elsif (my $l = $object->Gene) {
  978. return $self->_get_interpolated_position($l);
  979. }
  980. } elsif ($object->class eq 'Sequence') {
  981. #my ($chromosome,$position,$error) = $obj->Interpolated_map_position(1)->row;
  982. my $chromosome = $object->get(Interpolated_map_position=>1);
  983. my $position = $object->get(Interpolated_map_position=>2);
  984. return ($chromosome,$position) if $chromosome;
  985. } else {
  986. my $chromosome = $object->get(Map=>1);
  987. my $position = $object->get(Map=>3);
  988. return ($chromosome,$position) if $chromosome;
  989. if (my $m = $object->get('Interpolated_map_position')) {
  990. my ($chromosome,$position,$error) = $object->Interpolated_map_position(1)->row unless $position;
  991. ($chromosome,$position) = $m->right->row unless $position;
  992. return ($chromosome,$position,$error) if $chromosome;
  993. }
  994. }
  995. }
  996. return;
  997. }
  998. # Part of the old Best_BLAST_Hits table
  999. sub _covered {
  1000. my ($self,@starts) = @_;
  1001. # linearize
  1002. my @segs;
  1003. for my $s (@starts) {
  1004. my @ends = $s->col;
  1005. # Major kludge for architecture-dependent Perl bug(?) in interpreting integers as strings
  1006. $s = "$s.0";
  1007. push @segs,map {[$s,"$_.0"]} @ends;
  1008. }
  1009. my @sorted = sort {$a->[0]<=>$b->[0]} @segs;
  1010. my @merged;
  1011. foreach (@sorted) {
  1012. my ($start,$end) = @$_;
  1013. if ($merged[-1] && $merged[-1][1]>$start) {
  1014. $merged[-1][1] = $end if $end > $merged[-1][1];
  1015. } else {
  1016. push @merged,$_;
  1017. }
  1018. }
  1019. my $total = 0;
  1020. foreach my $merged (@merged) {
  1021. $total += $merged->[1]-$merged->[0];
  1022. }
  1023. $total;
  1024. }
  1025. #################################################
  1026. #
  1027. # REFERENCES
  1028. #
  1029. # References occur throughout the model.
  1030. #
  1031. # Note that the top level widget is called references.
  1032. #
  1033. ################################################
  1034. # This does not correspond to references proper in an AceDB model
  1035. # but a Reference or Paper tag for any object.
  1036. # Classes that DON'T use Reference: Interaction, Person, Author, Journal
  1037. # Web app: the references itself pulls in all four reference types by forward()).
  1038. sub get_references {
  1039. my ($self,$filter) = @_;
  1040. my $object = $self->current_object;
  1041. # References are not standardized. They may be under the Reference or Paper tag.
  1042. # Dynamically select the correct tag - this is a kludge until these are defined.
  1043. my $tag = (eval {$object->Reference}) ? 'Reference' : 'Paper';
  1044. my $dbh = $self->dbh_ace;
  1045. my $class = $object->class;
  1046. my @references;
  1047. if ( $filter eq 'all' ) {
  1048. @references = $object->$tag;
  1049. } elsif ( $filter eq 'gazette_abstracts' ) {
  1050. @references = $dbh->fetch(
  1051. -query => "find $class $object; follow $tag WBG_abstract",
  1052. -fill => 1);
  1053. } elsif ( $filter eq 'published_literature' ) {
  1054. @references = $dbh->fetch(
  1055. -query => "find $class $object; follow $tag PMID",
  1056. -fill => 1);
  1057. # @filtered = grep { $_->CGC_name || $_->PMID || $_->Medline_name }
  1058. # @$references;
  1059. } elsif ( $filter eq 'meeting_abstracts' ) {
  1060. @references = $dbh->fetch(
  1061. -query => "find $class $object; follow $tag Meeting_abstract",
  1062. -fill => 1
  1063. );
  1064. } elsif ( $filter eq 'wormbook_abstracts' ) {
  1065. @references = $dbh->fetch(
  1066. -query => "find $class $object; follow $tag WormBook",
  1067. -fill => 1
  1068. );
  1069. # Hmm. I don't know how to do this yet...
  1070. # @filtered = grep { $_->Remark =~ /.*WormBook.*/i } @$references;
  1071. }
  1072. return \@references;
  1073. }
  1074. # This is a convenience method for returning all methods. It
  1075. # isn't a field itself and is not included in the References widget.
  1076. sub all_references {
  1077. my $self = shift;
  1078. my $references = $self->get_references('all');
  1079. return $references;
  1080. }
  1081. sub published_literature {
  1082. my $self = shift;
  1083. my $references = $self->get_references('published_literarture');
  1084. return $references;
  1085. }
  1086. sub meeting_abstracts {
  1087. my $self = shift;
  1088. my $references = $self->get_references('meeting_abstracts');
  1089. return $references;
  1090. }
  1091. sub gazette_abstracts {
  1092. my $self = shift;
  1093. my $references = $self->get_references('gazette_abstracts');
  1094. return $references;
  1095. }
  1096. sub wormbook_abstracts {
  1097. my $self = shift;
  1098. my $references = $self->get_references('wormbook_abstracts');
  1099. return $references;
  1100. }
  1101. #################################################
  1102. #
  1103. # SINGLETON TAGS
  1104. #
  1105. # AUTOLOAD simple methods that access a single
  1106. # tag from the object and do not manipulate
  1107. # the data in any way
  1108. #
  1109. # This corresponds to something like this:
  1110. #
  1111. # sub author {
  1112. # my ($self) = @_;
  1113. # my $object = $self->current_object;
  1114. # return $object->Author;
  1115. # }
  1116. #
  1117. # NOTE: For the web app, you can only rely
  1118. # on AUTOLOAD when the field title corresponds
  1119. # to the object name! If it diverges, AUTOLOAD
  1120. # will fail horribly.
  1121. #
  1122. # To circumvent this, I could have a field2tag mapping
  1123. # hash for presentation
  1124. #
  1125. ################################################
  1126. sub AUTOLOAD {
  1127. my ($self) = @_;
  1128. my $type = ref($self);
  1129. # or croak
  1130. # "AUTOLOAD: $self is not an object. Web app: ensure that field name in config matches tag name in class";
  1131. my $name = $AUTOLOAD;
  1132. $name =~ s/.*://; # Strip qualified portion
  1133. # Not necessary - let's allow everything and capture errors by eval
  1134. # unless (exists $self->{_permitted}->{$name}) {
  1135. # croak "Can't access $name tag in class $type";
  1136. # }
  1137. # TODO: This should also be able to handle accessors
  1138. # This might be an accessor.
  1139. if ($self->{$name}) {
  1140. return $self->{$name};
  1141. } else {
  1142. # Otherwise it's a request for an ace tag.
  1143. # Pull out the ace object
  1144. my $ace_obj = $self->current_object;
  1145. # Now fetch the tag, assuming array context.
  1146. # Does this result in additional template overhead?
  1147. # We will eval, too, just to capture tags that might not
  1148. # exist in the current Class.
  1149. my $method = ucfirst($name);
  1150. my ($data) = eval { $ace_obj->$method };
  1151. return $data;
  1152. }
  1153. }
  1154. =head1 NAME
  1155. WormBase::Model - Model superclass
  1156. =head1 DESCRIPTION
  1157. The WormBase model superclass. Methods that need to be accessed in
  1158. more than a single model belong here.
  1159. =head1 METHODS
  1160. =item $self->genetic_position($object)
  1161. Returns : Hash reference containing keys of chromosome and position
  1162. Widget : location
  1163. Tmpl : generic/genetic_position.tt2
  1164. =item $self->interpolated_position($object)
  1165. Returns : Hash reference containing keys of:
  1166. chromosome
  1167. position
  1168. formatted_position
  1169. Widget : location
  1170. Tmpl : generic/interpolated_position.tt2
  1171. =head1 MIGRATION NOTES
  1172. =head1 AUTHOR
  1173. Todd Harris
  1174. =head1 LICENSE
  1175. This library is free software, you can redistribute it and/or modify
  1176. it under the same terms as Perl itself.
  1177. =cut
  1178. 1;