X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FText%2FTradition%2FCollation.pm;h=d4395a73492d9865b262e27ae2c22f585269feee;hb=869a1ada82eb48bc46f2298823fa1ef6f417c671;hp=b246c7ed5894ff22a9a761dac8cea198575bc7d0;hpb=91a190c3a6b64157c90d57ab69845814884510c9;p=scpubgit%2Fstemmatology.git diff --git a/lib/Text/Tradition/Collation.pm b/lib/Text/Tradition/Collation.pm index b246c7e..d4395a7 100644 --- a/lib/Text/Tradition/Collation.pm +++ b/lib/Text/Tradition/Collation.pm @@ -274,8 +274,10 @@ See L for the available options. sub BUILD { my $self = shift; $self->_set_relations( Text::Tradition::Collation::RelationshipStore->new( 'collation' => $self ) ); - $self->_set_start( $self->add_reading( { 'collation' => $self, 'is_start' => 1 } ) ); - $self->_set_end( $self->add_reading( { 'collation' => $self, 'is_end' => 1 } ) ); + $self->_set_start( $self->add_reading( + { 'collation' => $self, 'is_start' => 1, 'init' => 1 } ) ); + $self->_set_end( $self->add_reading( + { 'collation' => $self, 'is_end' => 1, 'init' => 1 } ) ); } ### Reading construct/destruct functions @@ -284,6 +286,13 @@ sub add_reading { my( $self, $reading ) = @_; unless( ref( $reading ) eq 'Text::Tradition::Collation::Reading' ) { my %args = %$reading; + if( $args{'init'} ) { + # If we are initializing an empty collation, don't assume that we + # have set a tradition. + delete $args{'init'}; + } elsif( $self->tradition->has_language && !exists $args{'language'} ) { + $args{'language'} = $self->tradition->language; + } $reading = Text::Tradition::Collation::Reading->new( 'collation' => $self, %args ); @@ -366,11 +375,23 @@ is( $c->reading('n21p0')->text, 'unto', "Reading n21p0 merged correctly" ); sub merge_readings { my $self = shift; + # Sanity check + my( $kept_obj, $del_obj, $combine, $combine_char ) = $self->_objectify_args( @_ ); + my $mergemeta = $kept_obj->is_meta; + throw( "Cannot merge meta and non-meta reading" ) + unless ( $mergemeta && $del_obj->is_meta ) + || ( !$mergemeta && !$del_obj->is_meta ); + if( $mergemeta ) { + throw( "Cannot merge with start or end node" ) + if( $kept_obj eq $self->start || $kept_obj eq $self->end + || $del_obj eq $self->start || $del_obj eq $self->end ); + } # We only need the IDs for adding paths to the graph, not the reading # objects themselves. - my( $kept, $deleted, $combine, $combine_char ) = $self->_stringify_args( @_ ); + my $kept = $kept_obj->id; + my $deleted = $del_obj->id; $self->_graphcalc_done(0); - + # The kept reading should inherit the paths and the relationships # of the deleted reading. foreach my $path ( $self->sequence->edges_at( $deleted ) ) { @@ -384,18 +405,23 @@ sub merge_readings { @wits{keys %$fwits} = values %$fwits; $self->sequence->set_edge_attributes( @vector, \%wits ); } - $self->relations->merge_readings( $kept, $deleted, $combine_char ); + $self->relations->merge_readings( $kept, $deleted, $combine ); # Do the deletion deed. if( $combine ) { - my $kept_obj = $self->reading( $kept ); - my $del_obj = $self->reading( $deleted ); + # Combine the text of the readings my $joinstr = $combine_char; unless( defined $joinstr ) { $joinstr = '' if $kept_obj->join_next || $del_obj->join_prior; $joinstr = $self->wordsep unless defined $joinstr; } $kept_obj->alter_text( join( $joinstr, $kept_obj->text, $del_obj->text ) ); + $kept_obj->normal_form( + join( $joinstr, $kept_obj->normal_form, $del_obj->normal_form ) ); + # Combine the lexemes present in the readings + if( $kept_obj->has_lexemes && $del_obj->has_lexemes ) { + $kept_obj->add_lexeme( $del_obj->lexemes ); + } } $self->del_reading( $deleted ); } @@ -557,7 +583,8 @@ sub as_svg { throw( "Need GraphViz installed to output SVG" ) unless File::Which::which( 'dot' ); my $want_subgraph = exists $opts->{'from'} || exists $opts->{'to'}; - $self->calculate_ranks() unless( $self->_graphcalc_done || $opts->{'nocalc'} ); + $self->calculate_ranks() + unless( $self->_graphcalc_done || $opts->{'nocalc'} || !$self->linear ); if( !$self->has_cached_svg || $opts->{'recalc'} || $want_subgraph ) { my @cmd = qw/dot -Tsvg/; my( $svg, $err ); @@ -639,15 +666,15 @@ sub as_dot { # Output substitute start/end readings if necessary if( $startrank ) { - $dot .= "\t\"#SUBSTART#\" [ label=\"...\" ];\n"; + $dot .= "\t\"__SUBSTART__\" [ label=\"...\",id=\"__START__\" ];\n"; } if( $endrank ) { - $dot .= "\t\"#SUBEND#\" [ label=\"...\" ];\n"; + $dot .= "\t\"__SUBEND__\" [ label=\"...\",id=\"__END__\" ];\n"; } if( $STRAIGHTENHACK ) { ## HACK part 1 - my $startlabel = $startrank ? 'SUBSTART' : 'START'; - $dot .= "\tsubgraph { rank=same \"#$startlabel#\" \"#SILENT#\" }\n"; + my $startlabel = $startrank ? '__SUBSTART__' : '__START__'; + $dot .= "\tsubgraph { rank=same \"$startlabel\" \"#SILENT#\" }\n"; $dot .= "\t\"#SILENT#\" [ shape=diamond,color=white,penwidth=0,label=\"\" ];" } my %used; # Keep track of the readings that actually appear in the graph @@ -669,6 +696,7 @@ sub as_dot { $label = "-$label" if $reading->join_prior; $label =~ s/\"/\\\"/g; $rattrs->{'label'} = $label; + $rattrs->{'id'} = $reading->id; $rattrs->{'fillcolor'} = '#b3f36d' if $reading->is_common && $color_common; $dot .= sprintf( "\t\"%s\" %s;\n", $reading->id, _dot_attr_string( $rattrs ) ); } @@ -708,28 +736,33 @@ sub as_dot { $dot .= sprintf( "\t\"%s\" -> \"%s\" %s;\n", $edge->[0], $edge->[1], $varopts ); } elsif( $used{$edge->[0]} ) { - $subend{$edge->[0]} = 1; + $subend{$edge->[0]} = $edge->[1]; } elsif( $used{$edge->[1]} ) { - $substart{$edge->[1]} = 1; + $substart{$edge->[1]} = $edge->[0]; } } # Add substitute start and end edges if necessary foreach my $node ( keys %substart ) { - my $witstr = $self->_path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) ); + my $witstr = $self->_path_display_label ( $self->path_witnesses( $substart{$node}, $node ) ); my $variables = { %edge_attrs, 'label' => $witstr }; + my $nrdg = $self->reading( $node ); + if( $nrdg->has_rank && $nrdg->rank > $startrank ) { + # Substart is actually one lower than $startrank + $variables->{'minlen'} = $nrdg->rank - ( $startrank - 1 ); + } my $varopts = _dot_attr_string( $variables ); - $dot .= "\t\"#SUBSTART#\" -> \"$node\" $varopts;"; + $dot .= "\t\"__SUBSTART__\" -> \"$node\" $varopts;\n"; } foreach my $node ( keys %subend ) { - my $witstr = $self->_path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) ); + my $witstr = $self->_path_display_label ( $self->path_witnesses( $node, $subend{$node} ) ); my $variables = { %edge_attrs, 'label' => $witstr }; my $varopts = _dot_attr_string( $variables ); - $dot .= "\t\"$node\" -> \"#SUBEND#\" $varopts;"; + $dot .= "\t\"$node\" -> \"__SUBEND__\" $varopts;\n"; } # HACK part 2 if( $STRAIGHTENHACK ) { - my $endlabel = $endrank ? 'SUBEND' : 'END'; - $dot .= "\t\"#$endlabel#\" -> \"#SILENT#\" [ color=white,penwidth=0 ];\n"; + my $endlabel = $endrank ? '__SUBEND__' : '__END__'; + $dot .= "\t\"$endlabel\" -> \"#SILENT#\" [ color=white,penwidth=0 ];\n"; } $dot .= "}\n"; @@ -813,6 +846,7 @@ sub _path_display_label { # See if we are in a majority situation. my $maj = scalar( $self->tradition->witnesses ) * 0.6; + $maj = $maj > 5 ? $maj : 5; if( scalar keys %wits > $maj ) { unshift( @disp_ac, 'majority' ); return join( ', ', @disp_ac ); @@ -883,6 +917,13 @@ is( scalar $st->collation->readings, $READINGS, "Reparsed collation has all read is( scalar $st->collation->paths, $PATHS, "Reparsed collation has all paths" ); is( scalar $st->collation->relationships, 3, "Reparsed collation has new relationships" ); +# Now add a stemma, write to GraphML, and parse again. +my $stemma = $tradition->add_stemma( 'dotfile' => 't/data/florilegium.dot' ); +is( ref( $stemma ), 'Text::Tradition::Stemma', "Parsed dotfile into stemma" ); +is( $tradition->stemmata, 1, "Tradition now has the stemma" ); +$graphml = $c->as_graphml; +like( $graphml, qr/digraph/, "Digraph declaration exists in GraphML" ); + =end testing =cut @@ -926,6 +967,7 @@ sub as_graphml { 'Str' => 'string', 'Int' => 'int', 'Bool' => 'boolean', + 'ReadingID' => 'string', 'RelationshipType' => 'string', 'RelationshipScope' => 'string', ); @@ -952,6 +994,8 @@ sub as_graphml { next unless $save_types{$attr->type_constraint->name}; $graph_attributes{$attr->name} = $save_types{$attr->type_constraint->name}; } + # Extra custom key for the tradition stemma(ta) + $graph_attributes{'stemmata'} = 'string'; foreach my $datum ( sort keys %graph_attributes ) { $graph_data_keys{$datum} = 'dg'.$gdi++; @@ -971,6 +1015,9 @@ sub as_graphml { next unless $save_types{$attr->type_constraint->name}; $reading_attributes{$attr->name} = $save_types{$attr->type_constraint->name}; } + # Extra custom key for the reading morphology + $reading_attributes{'lexemes'} = 'string'; + my %node_data_keys; my $ndi = 0; foreach my $datum ( sort keys %reading_attributes ) { @@ -1022,11 +1069,16 @@ sub as_graphml { $sgraph->setAttribute( 'parse.nodes', 0 ); # fill in later $sgraph->setAttribute( 'parse.order', 'nodesfirst' ); - # Collation attribute data + # Tradition/collation attribute data foreach my $datum ( keys %graph_attributes ) { my $value; if( $datum eq 'version' ) { - $value = '3.1'; + $value = '3.2'; + } elsif( $datum eq 'stemmata' ) { + my @stemstrs; + map { push( @stemstrs, $_->editable( {linesep => ''} ) ) } + $self->tradition->stemmata; + $value = join( "\n", @stemstrs ); } elsif( $gattr_from{$datum} eq 'Tradition' ) { $value = $self->tradition->$datum; } else { @@ -1049,6 +1101,15 @@ sub as_graphml { $node_el->setAttribute( 'id', $node_xmlid ); foreach my $d ( keys %reading_attributes ) { my $nval = $n->$d; + # Custom serialization + if( $d eq 'lexemes' ) { + # If nval is a true value, we have lexemes so we need to + # serialize them. Otherwise set nval to undef so that the + # key is excluded from this reading. + $nval = $nval ? $n->_serialize_lexemes : undef; + } elsif( $d eq 'normal_form' && $n->normal_form eq $n->text ) { + $nval = undef; + } if( $rankoffset && $d eq 'rank' && $n ne $self->start ) { # Adjust the ranks within the subgraph. $nval = $n eq $self->end ? $end->rank - $rankoffset + 1 @@ -1386,21 +1447,13 @@ sub path_text { $start = $self->start unless $start; $end = $self->end unless $end; my @path = grep { !$_->is_meta } $self->reading_sequence( $start, $end, $wit ); - return $self->_text_from_path( @path ); -} - -# Utility function so that we can cheat and use it when we need both the path -# and its text. -sub _text_from_path { - my( $self, @path ) = @_; my $pathtext = ''; my $last; foreach my $r ( @path ) { - if( $r->join_prior || !$last || $last->join_next ) { - $pathtext .= $r->text; - } else { - $pathtext .= ' ' . $r->text; - } + unless ( $r->join_prior || !$last || $last->join_next ) { + $pathtext .= ' '; + } + $pathtext .= $r->text; $last = $r; } return $pathtext; @@ -1499,14 +1552,8 @@ sub calculate_ranks { # Do the rankings based on the relationship equivalence graph, starting # with the start node. - my $topo_start = $self->equivalence( $self->start->id ); - my $node_ranks = { $topo_start => 0 }; - my @curr_origin = ( $topo_start ); - # A little iterative function. - while( @curr_origin ) { - @curr_origin = _assign_rank( $self->equivalence_graph, - $node_ranks, @curr_origin ); - } + my ( $node_ranks, $rank_nodes ) = $self->relations->equivalence_ranks(); + # Transfer our rankings from the topological graph to the real one. foreach my $r ( $self->readings ) { if( defined $node_ranks->{$self->equivalence( $r->id )} ) { @@ -1536,41 +1583,6 @@ sub calculate_ranks { $self->_graphcalc_done(1); } -sub _assign_rank { - my( $graph, $node_ranks, @current_nodes ) = @_; - # Look at each of the children of @current_nodes. If all the child's - # parents have a rank, assign it the highest rank + 1 and add it to - # @next_nodes. Otherwise skip it; we will return when the highest-ranked - # parent gets a rank. - my @next_nodes; - foreach my $c ( @current_nodes ) { - warn "Current reading $c has no rank!" - unless exists $node_ranks->{$c}; - # print STDERR "Looking at child of node $c, rank " - # . $node_ranks->{$c} . "\n"; - foreach my $child ( $graph->successors( $c ) ) { - next if exists $node_ranks->{$child}; - my $highest_rank = -1; - my $skip = 0; - foreach my $parent ( $graph->predecessors( $child ) ) { - if( exists $node_ranks->{$parent} ) { - $highest_rank = $node_ranks->{$parent} - if $highest_rank <= $node_ranks->{$parent}; - } else { - $skip = 1; - last; - } - } - next if $skip; - my $c_rank = $highest_rank + 1; - # print STDERR "Assigning rank $c_rank to node $child \n"; - $node_ranks->{$child} = $c_rank; - push( @next_nodes, $child ); - } - } - return @next_nodes; -} - sub _clear_cache { my $self = shift; $self->wipe_svg if $self->has_cached_svg; @@ -1593,8 +1605,17 @@ sub flatten_ranks { next unless $rdg->has_rank; my $key = $rdg->rank . "||" . $rdg->text; if( exists $unique_rank_rdg{$key} ) { + # Make sure they don't have different grammatical forms + my $ur = $unique_rank_rdg{$key}; + if( $rdg->disambiguated && $ur->disambiguated ) { + my $rform = join( '//', map { $_->form->to_string } $rdg->lexemes ); + my $uform = join( '//', map { $_->form->to_string } $ur->lexemes ); + next unless $rform eq $uform; + } elsif( $rdg->disambiguated xor $ur->disambiguated ) { + next; + } # Combine! - # print STDERR "Combining readings at same rank: $key\n"; + #print STDERR "Combining readings at same rank: $key\n"; $changed = 1; $self->merge_readings( $unique_rank_rdg{$key}, $rdg ); # TODO see if this now makes a common point. @@ -1722,12 +1743,12 @@ my $c = $t->collation; is( $c->common_predecessor( 'n24', 'n23' )->id, 'n20', "Found correct common predecessor" ); is( $c->common_successor( 'n24', 'n23' )->id, - '#END#', "Found correct common successor" ); + '__END__', "Found correct common successor" ); is( $c->common_predecessor( 'n19', 'n17' )->id, 'n16', "Found correct common predecessor for readings on same path" ); is( $c->common_successor( 'n21', 'n10' )->id, - '#END#', "Found correct common successor for readings on same path" ); + '__END__', "Found correct common successor for readings on same path" ); =end testing