incorporate user auth functionality
Tara L Andrews [Wed, 11 Jul 2012 19:33:51 +0000 (21:33 +0200)]
14 files changed:
lib/stemmaweb/Controller/Relation.pm
lib/stemmaweb/Controller/Root.pm
lib/stemmaweb/Controller/Stexaminer.pm
lib/stemmaweb/View/JSON.pm
root/css/relationship.css
root/css/stexaminer.css
root/css/style.css
root/js/componentload.js
root/js/relationship.js
root/js/stexaminer.js
root/src/index.tt
root/src/relate.tt
root/src/relatehelp.tt
root/src/stexaminer.tt

index 0082624..0842124 100644 (file)
@@ -1,5 +1,6 @@
 package stemmaweb::Controller::Relation;
 use Moose;
+use Module::Load;
 use namespace::autoclean;
 use TryCatch;
 
@@ -29,19 +30,6 @@ sub index :Path :Args(0) {
        $c->stash->{'template'} = 'relate.tt';
 }
 
-=head2 help
-
- GET relation/help
-
-Returns the help window HTML.
-
-=cut
-
-sub help :Local :Args(0) {
-       my( $self, $c ) = @_;
-       $c->stash->{'template'} = 'relatehelp.tt';
-}
-
 =head2 definitions
 
  GET relation/definitions
@@ -70,6 +58,12 @@ sub text :Chained('/') :PathPart('relation') :CaptureArgs(1) {
        my( $self, $c, $textid ) = @_;
        # If the tradition has more than 500 ranks or so, split it up.
        my $tradition = $c->model('Directory')->tradition( $textid );
+    # Account for a bad interaction between FastCGI and KiokuDB
+    unless( $tradition->collation->tradition ) {
+        $c->log->warn( "Fixing broken tradition link" );
+        $tradition->collation->_set_tradition( $tradition );
+        $c->model('Directory')->save( $tradition );
+    }
        # See how big the tradition is. Edges are more important than nodes
        # when it comes to rendering difficulty.
        my $numnodes = scalar $tradition->collation->readings;
@@ -100,7 +94,6 @@ sub text :Chained('/') :PathPart('relation') :CaptureArgs(1) {
                        push( @{$c->stash->{'textsegments'}}, $seg );
                }
        }
-       $DB::single = 1;
        $c->stash->{'textid'} = $textid;
        $c->stash->{'tradition'} = $tradition;
 }
@@ -129,9 +122,37 @@ sub main :Chained('text') :PathPart('') :Args(0) {
        $c->stash->{'startseg'} = $startseg if defined $startseg;
        $c->stash->{'svg_string'} = $svg_str;
        $c->stash->{'text_title'} = $tradition->name;
+       $c->stash->{'text_lang'} = $tradition->language;
        $c->stash->{'template'} = 'relate.tt';
 }
 
+=head2 help
+
+ GET relation/help/$language
+
+Returns the help window HTML.
+
+=cut
+
+sub help :Local :Args(1) {
+       my( $self, $c, $lang ) = @_;
+       # Display the morphological help for the language if it is defined.
+       if( $lang && $lang ne 'Default' ) {
+               my $mod = 'Text::Tradition::Language::' . $lang;
+               try {
+                       load( $mod );
+               } catch {
+                       $c->log->debug("Warning: could not load $mod");
+               }
+               my $has_mod = $mod->can('morphology_tags');
+               if( $has_mod ) {
+                       my $tagset = &$has_mod;
+                       $c->stash->{'tagset'} = $tagset;
+               }
+       }
+       $c->stash->{'template'} = 'relatehelp.tt';
+}
+
 =head2 relationships
 
  GET relation/$textid/relationships
@@ -159,6 +180,7 @@ sub relationships :Chained('text') :PathPart :Args(0) {
                foreach my $p ( @pairs ) {
                        my $relobj = $collation->relations->get_relationship( @$p );
                        next if $relobj->type eq 'collated'; # Don't show these
+                       next if $p->[0] eq $p->[1]; # HACK until bugfix
                        my $relhash = { source => $p->[0], target => $p->[1], 
                                  type => $relobj->type, scope => $relobj->scope };
                        $relhash->{'note'} = $relobj->annotation if $relobj->has_annotation;
@@ -198,8 +220,142 @@ sub relationships :Chained('text') :PathPart :Args(0) {
                }       
        }
        $c->forward('View::JSON');
-}              
-               
+}
+
+=head2 readings
+
+ GET relation/$textid/readings
+
+Returns the list of readings defined for this text along with their metadata.
+
+=cut
+
+my %read_write_keys = (
+       'id' => 0,
+       'text' => 0,
+       'is_meta' => 0,
+       'grammar_invalid' => 1,
+       'is_nonsense' => 1,
+       'normal_form' => 1,
+);
+
+sub _reading_struct {
+       my( $reading ) = @_;
+       # Return a JSONable struct of the useful keys.  Keys meant to be writable
+       # have a true value; read-only keys have a false value.
+       my $struct = {};
+       map { $struct->{$_} = $reading->$_ } keys( %read_write_keys );
+       # Special case
+       $struct->{'lexemes'} = [ $reading->lexemes ];
+       # Look up any words related via spelling or orthography
+       my $sameword = sub { 
+               my $t = $_[0]->type;
+               return $t eq 'spelling' || $t eq 'orthographic';
+       };
+       my @variants;
+       foreach my $sr ( $reading->related_readings( $sameword ) ) {
+               push( @variants, $sr->text );
+       }
+       $struct->{'variants'} = \@variants;
+       return $struct;
+}
+
+sub readings :Chained('text') :PathPart :Args(0) {
+       my( $self, $c ) = @_;
+       my $tradition = delete $c->stash->{'tradition'};
+       my $collation = $tradition->collation;
+       my $m = $c->model('Directory');
+       if( $c->request->method eq 'GET' ) {
+               my $rdginfo = {};
+               foreach my $rdg ( $collation->readings ) {
+                       $rdginfo->{$rdg->id} = _reading_struct( $rdg );
+               }
+               $c->stash->{'result'} = $rdginfo;
+       }
+       $c->forward('View::JSON');
+}
+
+=head2 reading
+
+ GET relation/$textid/reading/$id
+
+Returns the list of readings defined for this text along with their metadata.
+
+ POST relation/$textid/reading/$id { request }
+Alters the reading according to the values in request. Returns 403 Forbidden if
+the alteration isn't allowed.
+
+=cut
+
+sub reading :Chained('text') :PathPart :Args(1) {
+       my( $self, $c, $reading_id ) = @_;
+       my $tradition = delete $c->stash->{'tradition'};
+       my $collation = $tradition->collation;
+       my $rdg = $collation->reading( $reading_id );
+       my $m = $c->model('Directory');
+       if( $c->request->method eq 'GET' ) {
+               $c->stash->{'result'} = $rdg ? _reading_struct( $rdg )
+                       : { 'error' => "No reading with ID $reading_id" };
+       } elsif ( $c->request->method eq 'POST' ) {
+               my $errmsg;
+               # Are we re-lemmatizing?
+               if( $c->request->param('relemmatize') ) {
+                       my $nf = $c->request->param('normal_form');
+                       # TODO throw error unless $nf
+                       $rdg->normal_form( $nf );
+                       # TODO throw error if lemmatization fails
+                       # TODO skip this if normal form hasn't changed
+                       $rdg->lemmatize();
+               } else {
+                       # Set all the values that we have for the reading.
+                       # TODO error handling
+                       foreach my $p ( keys %{$c->request->params} ) {
+                               if( $p =~ /^morphology_(\d+)$/ ) {
+                                       # Set the form on the correct lexeme
+                                       my $morphval = $c->request->param( $p );
+                                       next unless $morphval;
+                                       my $midx = $1;
+                                       my $lx = $rdg->lexeme( $midx );
+                                       my $strrep = $rdg->language . ' // ' . $morphval;
+                                       my $idx = $lx->has_form( $strrep );
+                                       unless( defined $idx ) {
+                                               # Make the word form and add it to the lexeme.
+                                               try {
+                                                       $idx = $lx->add_matching_form( $strrep ) - 1;
+                                               } catch( Text::Tradition::Error $e ) {
+                                                       $c->response->status( '403' );
+                                                       $errmsg = $e->message;
+                                               } catch {
+                                                       # Something else went wrong, probably a Moose error
+                                                       $c->response->status( '403' );
+                                                       $errmsg = 'Something went wrong with the request';      
+                                               }
+                                       }
+                                       $lx->disambiguate( $idx ) if defined $idx;
+                               } elsif( $read_write_keys{$p} ) {
+                                       my $val = _clean_booleans( $rdg, $p, $c->request->param( $p ) );
+                                       $rdg->$p( $val );
+                               }
+                       }               
+               }
+               $m->save( $rdg );
+               $c->stash->{'result'} = $errmsg ? { 'error' => $errmsg }
+                       : _reading_struct( $rdg );
+
+       }
+       $c->forward('View::JSON');
+
+}
+
+sub _clean_booleans {
+       my( $rdg, $param, $val ) = @_;
+       if( $rdg->meta->get_attribute( $param )->type_constraint->name eq 'Bool' ) {
+               $val = 1 if $val eq 'true';
+               $val = undef if $val eq 'false';
+       } 
+       return $val;
+}
 
 =head2 end
 
index 6e26299..90f9e8a 100644 (file)
@@ -125,7 +125,7 @@ sub stemma :Local :Args(1) {
        }
        
        $c->stash->{'result'} = $tradition->stemma_count
-               ? $tradition->stemma(0)->as_svg
+               ? $tradition->stemma(0)->as_svg( { size => [ 500, 375 ] } )
                : '';
        $c->forward('View::SVG');
 }
index a0f4fcb..e556e32 100644 (file)
@@ -1,9 +1,12 @@
 package stemmaweb::Controller::Stexaminer;
 use Moose;
 use namespace::autoclean;
+use Encode qw/ decode_utf8 /;
 use File::Temp;
 use JSON;
 use Text::Tradition::Analysis qw/ run_analysis wit_stringify /;
+use Text::Tradition::Collation;
+use Text::Tradition::Stemma;
 
 BEGIN { extends 'Catalyst::Controller' }
 
@@ -18,12 +21,12 @@ The stemma analysis tool with the pretty colored table.
 
 =head1 METHODS
 
+=head2 index
+
  GET stexaminer/$textid
  
 Renders the application for the text identified by $textid.
 
-=head2 index
-
 =cut
 
 sub index :Path :Args(1) {
@@ -32,8 +35,8 @@ sub index :Path :Args(1) {
        my $tradition = $m->tradition( $textid );
        if( $tradition->stemma_count ) {
                my $stemma = $tradition->stemma(0);
-               # TODO Think about caching the stemma in a session 
-               $c->stash->{svg} = $stemma->as_svg;
+               $c->stash->{svg} = $stemma->as_svg( { size => [ 600, 350 ] } );
+               $c->stash->{graphdot} = $stemma->editable({ linesep => ' ' });
                $c->stash->{text_title} = $tradition->name;
                $c->stash->{template} = 'stexaminer.tt'; 
                # TODO Run the analysis as AJAX from the loaded page.
@@ -45,6 +48,11 @@ sub index :Path :Args(1) {
                        foreach my $rhash ( @{$loc->{'readings'}} ) {
                                my $gst = wit_stringify( $rhash->{'group'} );
                                $rhash->{'group'} = $gst;
+                               my $roots = join( ', ', @{$rhash->{'independent_occurrence'}} );
+                               $rhash->{'independent_occurrence'} = $roots;
+                               unless( $rhash->{'text'} ) {
+                                       $rhash->{'text'} = $rhash->{'readingid'};
+                               }
                        }
                }
                # Values for TT rendering
@@ -60,6 +68,34 @@ sub index :Path :Args(1) {
        }
 }
 
+=head2 graphsvg
+
+  POST stexaminer/graphsvg
+       dot: <stemmagraph dot string> 
+       layerwits: [ <a.c. witnesses ] 
+  
+Returns an SVG string of the given graph, extended to include the given 
+layered witnesses.
+
+=cut
+
+sub graphsvg :Local {
+       my( $self, $c ) = @_;
+       my $dot = $c->request->param('dot');
+       my @layerwits = $c->request->param('layerwits[]');
+       open my $stemma_fh, '<', \$dot;
+       binmode( $stemma_fh, ':encoding(UTF-8)' );
+       my $emptycoll = Text::Tradition::Collation->new();
+       my $tempstemma = Text::Tradition::Stemma->new( 
+               collation => $emptycoll, 'dot' => $stemma_fh );
+       my $svgopts = { size => [ 600, 350 ] };
+       if( @layerwits ) {
+               $svgopts->{'layerwits'} = \@layerwits;
+       }
+       $c->stash->{'result'} = $tempstemma->as_svg( $svgopts );
+       $c->forward('View::SVG');
+}
+
 =head2 end
 
 Attempt to render a view, if needed.
index e8a9284..347c034 100644 (file)
@@ -3,6 +3,16 @@ package stemmaweb::View::JSON;
 use strict;
 use base 'Catalyst::View::JSON';
 
+use JSON::XS ();
+
+sub encode_json {
+       my( $self, $c, $data ) = @_;
+       my $json = JSON::XS->new->utf8->convert_blessed(1);
+       $json->encode( $data );
+}
+
+1;
+
 =head1 NAME
 
 stemmaweb::View::JSON - Catalyst JSON View
@@ -23,7 +33,3 @@ Tara Andrews
 
 This library is free software, you can redistribute it and/or modify
 it under the same terms as Perl itself.
-
-=cut
-
-1;
index 9b778b6..d64acab 100644 (file)
@@ -200,3 +200,34 @@ span.apimore {
     list-style: none;
     margin-bottom: 3px;
 }
+
+.draggable {
+       cursor:pointer;
+}
+
+.noselect {
+-webkit-touch-callout: none;
+-webkit-user-select: none;
+-khtml-user-select: none;
+-moz-user-select: none;
+-ms-user-select: none;
+user-select: none;
+}
+
+#normalization {
+       float: left;
+       padding: 10px;
+}
+#relemmatize_pending {
+       float: left;
+       padding: 10px;
+       display: none;
+}
+#morph_outer {
+       clear: both;
+       float: left;
+}
+#morphology {
+       text-align: right;
+       margin: 10px;
+}
index 59c523d..760fbb2 100644 (file)
@@ -1,16 +1,16 @@
 #variants_table {
     float: left;
     width: 90%;
-    height: 90px;
+    height: 190px;
     border: 1px #c6dcf1 solid;
     margin-bottom: 20px;
     overflow: auto;
 }
 #stemma_graph {
-    height: 450px;
+    height: 350px;
     clear: both;
     float: left;
-    width: 700px;
+    width: 600px;
     text-align: center;
     border: 1px #c6dcf1 solid;
 }
     position: relative;
     top: -15px;
 }
-#stats_template {
+.reading_statistics {
+       margin: 7pt;
+       border-bottom: 1px solid #488dd2;
+}
+.readinglabel {
+       font-weight: bold;
+}
+.readingroots {
+       font-weight: bold;
+       color: #488dd2;
+}
+.reading_copied {
+       color: #33dd33;
+}
+.reading_changed {
+       color: #dd3333;
+}
+.template {
        display: none;
 }
 .genealogical {
index 264e6de..a3047f9 100644 (file)
@@ -121,10 +121,9 @@ div.button:hover span {
 }
 #stexaminer_button {
        bottom: 0;
-       margin-top: 5px;
+       margin-top: 13px;
 }
 #relater_button {
        float: left;
-       margin-left: 83px;
-    margin-top:  7px;
+    margin-left:  100px;
 }
index e881f84..87cc0ef 100644 (file)
@@ -6,21 +6,11 @@ function loadTradition( textid, textname ) {
     };
     var imghtml = '<img src="' + basepath + '/images/ajax-loader.gif" alt="Loading SVG..."/>'
     $('#stemma_graph').empty();
-    $('#variant_graph').empty();
     $('#stemma_graph').append( imghtml );
-    $('#variant_graph').append( imghtml );
     // Then get and load the actual content.
     // TODO: scale #stemma_grpah both horizontally and vertically
     // TODO: load svgs from SVG.Jquery (to make scaling react in Safari)
        $('#stemma_graph').load( basepath + "/stemma/" + textid );
-    $('#variant_graph').load( basepath + "/variantgraph/" + textid , function() {
-       var variant_svg_element = $('#variant_graph svg').svg().svg('get').root();
-       var svg_height = variant_svg_element.height.baseVal.value;
-       var svg_width = variant_svg_element.width.baseVal.value;
-       var container_height = $('#variant_graph').height();
-       variant_svg_element.height.baseVal.value = container_height;
-       variant_svg_element.width.baseVal.value = (svg_width/svg_height * container_height);
-       });
        
        // Then populate the various elements with the right text name/ID.
        // Stemma and variant graph titles
index ed1b150..cd3d867 100644 (file)
@@ -1,5 +1,12 @@
+var MARGIN=30;
+var svg_root = null;
+var svg_root_element = null;
+var start_element_height = 0;
+var reltypes = {};
+var readingdata = {};
+
 function getTextPath() {
-    var currpath = window.location.pathname
+    var currpath = window.location.pathname;
     // Get rid of trailing slash
     if( currpath.lastIndexOf('/') == currpath.length - 1 ) { 
        currpath = currpath.slice( 0, currpath.length - 1) 
@@ -20,11 +27,147 @@ function getRelativePath() {
        return path_parts[0];
 }
 
-function getRelationshipURL() {
+function getTextURL( which ) {
+       var path_parts = getTextPath();
+       return path_parts[0] + '/' + path_parts[1] + '/' + which;
+}
+
+function getReadingURL( reading_id ) {
        var path_parts = getTextPath();
-       return path_parts[0] + '/' + path_parts[1] + '/relationships';
+       return path_parts[0] + '/' + path_parts[1] + '/reading/' + reading_id;
+}
+
+// Make an XML ID into a valid selector
+function jq(myid) { 
+       return '#' + myid.replace(/(:|\.)/g,'\\$1');
+}
+
+// Actions for opening the reading panel
+function node_dblclick_listener( evt ) {
+       // Open the reading dialogue for the given node.
+       // First get the reading info
+       var reading_id = $(this).attr('id');
+       var reading_info = readingdata[reading_id];
+       // and then populate the dialog box with it.
+       // Set the easy properties first
+       $('#reading-form').dialog( 'option', 'title', 'Reading information for "' + reading_info['text'] + '"' );
+       $('#reading_id').val( reading_id );
+       toggle_checkbox( $('#reading_is_nonsense'), reading_info['is_nonsense'] );
+       toggle_checkbox( $('#reading_grammar_invalid'), reading_info['grammar_invalid'] );
+       // Use .text as a backup for .normal_form
+       var normal_form = reading_info['normal_form'];
+       if( !normal_form ) {
+               normal_form = reading_info['text'];
+       }
+       var nfboxsize = 10;
+       if( normal_form.length > 9 ) {
+               nfboxsize = normal_form.length + 1;
+       }
+       $('#reading_normal_form').attr( 'size', nfboxsize )
+       $('#reading_normal_form').val( normal_form );
+       // Now do the morphological properties.
+       morphology_form( reading_info['lexemes'] );
+       // and then open the dialog.
+       $('#reading-form').dialog("open");
+}
+
+function toggle_checkbox( box, value ) {
+       if( value == null ) {
+               value = false;
+       }
+       box.attr('checked', value );
+}
+
+function morphology_form ( lexlist ) {
+       $('#morphology').empty();
+       $.each( lexlist, function( idx, lex ) {
+               var morphoptions = [];
+               if( 'wordform_matchlist' in lex ) {
+                       $.each( lex['wordform_matchlist'], function( tdx, tag ) {
+                               var tagstr = stringify_wordform( tag );
+                               morphoptions.push( tagstr );
+                       });
+               }
+               var formtag = 'morphology_' + idx;
+               var formstr = '';
+               if( 'form' in lex ) {
+                       formstr = stringify_wordform( lex['form'] );
+               } 
+               var form_morph_elements = morph_elements( 
+                       formtag, lex['string'], formstr, morphoptions );
+               $.each( form_morph_elements, function( idx, el ) {
+                       $('#morphology').append( el );
+               });
+       });
+}
+
+function stringify_wordform ( tag ) {
+       if( tag ) {
+               var elements = tag.split(' // ');
+               return elements[1] + ' // ' + elements[2];
+       }
+       return ''
+}
+
+function morph_elements ( formtag, formtxt, currform, morphoptions ) {
+       var clicktag = '(Click to select)';
+       if ( !currform ) {
+               currform = clicktag;
+       }
+       var formlabel = $('<label/>').attr( 'id', 'label_' + formtag ).attr( 
+               'for', 'reading_' + formtag ).text( formtxt + ': ' );
+       var forminput = $('<input/>').attr( 'id', 'reading_' + formtag ).attr( 
+               'name', 'reading_' + formtag ).attr( 'size', '50' ).attr(
+               'class', 'reading_morphology' ).val( currform );
+       forminput.autocomplete({ source: morphoptions, minLength: 0     });
+       forminput.focus( function() { 
+               if( $(this).val() == clicktag ) {
+                       $(this).val('');
+               }
+               $(this).autocomplete('search', '') 
+       });
+       var morphel = [ formlabel, forminput, $('<br/>') ];
+       return morphel;
 }
 
+function color_inactive ( el ) {
+       var reading_id = $(el).parent().attr('id');
+       var reading_info = readingdata[reading_id];
+       // If the reading info has any non-disambiguated lexemes, color it yellow;
+       // otherwise color it green.
+       $(el).attr( {stroke:'green', fill:'#b3f36d'} );
+       if( reading_info ) {
+               $.each( reading_info['lexemes'], function ( idx, lex ) {
+                       if( !lex['is_disambiguated'] || lex['is_disambiguated'] == 0 ) {
+                               $(el).attr( {stroke:'orange', fill:'#fee233'} );
+                       }
+               });
+       }
+}
+
+function relemmatize () {
+       // Send the reading for a new lemmatization and reopen the form.
+       $('#relemmatize_pending').show();
+       var reading_id = $('#reading_id').val()
+       ncpath = getReadingURL( reading_id );
+       form_values = { 
+               'normal_form': $('#reading_normal_form').val(), 
+               'relemmatize': 1 };
+       var jqjson = $.post( ncpath, form_values, function( data ) {
+               // Update the form with the return
+               if( 'id' in data ) {
+                       // We got back a good answer. Stash it
+                       readingdata[reading_id] = data;
+                       // and regenerate the morphology form.
+                       morphology_form( data['lexemes'] );
+               } else {
+                       alert("Could not relemmatize as requested: " + data['error']);
+               }
+               $('#relemmatize_pending').hide();
+       });
+}
+
+// Initialize the SVG once it exists
 function svgEnlargementLoaded() {
        //Give some visual evidence that we are working
        $('#loading_overlay').show();
@@ -37,11 +180,28 @@ function svgEnlargementLoaded() {
                { 'top': lo_height / 2 - $("#loading_message").height() / 2,
                  'left': lo_width / 2 - $("#loading_message").width() / 2 });
     //Set viewbox widht and height to widht and height of $('#svgenlargement svg').
+    $('#update_workspace_button').data('locked', false);
+    $('#update_workspace_button').css('background-position', '0px 44px');
     //This is essential to make sure zooming and panning works properly.
-    $('#svgenlargement ellipse').attr( {stroke:'green', fill:'#b3f36d'} );
+       var rdgpath = getTextURL( 'readings' );
+               $.getJSON( rdgpath, function( data ) {
+               readingdata = data;
+           $('#svgenlargement ellipse').each( function( i, el ) { color_inactive( el ) });
+       });
+    $('#svgenlargement ellipse').parent().dblclick( node_dblclick_listener );
     var graph_svg = $('#svgenlargement svg');
     var svg_g = $('#svgenlargement svg g')[0];
+    if (!svg_g) return;
     svg_root = graph_svg.svg().svg('get').root();
+
+    // Find the real root and ignore any text nodes
+    for (i = 0; i < svg_root.childNodes.length; ++i) {
+        if (svg_root.childNodes[i].nodeName != '#text') {
+               svg_root_element = svg_root.childNodes[i];
+               break;
+          }
+    }
+
     svg_root.viewBox.baseVal.width = graph_svg.attr( 'width' );
     svg_root.viewBox.baseVal.height = graph_svg.attr( 'height' );
     //Now set scale and translate so svg height is about 150px and vertically centered in viewbox.
@@ -55,13 +215,13 @@ function svgEnlargementLoaded() {
     var transform = 'rotate(0) scale(' + scale + ') translate(4 ' + translate + ')';
     svg_g.setAttribute('transform', transform);
     //used to calculate min and max zoom level:
-    start_element_height = $("#svgenlargement .node title:contains('START#')").siblings('ellipse')[0].getBBox().height;
+    start_element_height = $('#__START__').children('ellipse')[0].getBBox().height;
     add_relations( function() { $('#loading_overlay').hide(); });
 }
 
 function add_relations( callback_fn ) {
        var basepath = getRelativePath();
-       var textrelpath = getRelationshipURL();
+       var textrelpath = getTextURL( 'relationships' );
     $.getJSON( basepath + '/definitions', function(data) {
         var rel_types = data.types.sort();
         $.getJSON( textrelpath,
@@ -89,9 +249,7 @@ function add_relations( callback_fn ) {
 }
 
 function get_ellipse( node_id ) {
-  return $('#svgenlargement .node').children('title').filter( function(index) {
-    return $(this).text() == node_id;
-  }).siblings('ellipse');
+       return $( jq( node_id ) + ' ellipse');
 }
 
 function get_node_obj( node_id ) {
@@ -102,12 +260,6 @@ function get_node_obj( node_id ) {
     return node_ellipse.data( 'node_obj' );
 }
 
-function get_edge( edge_id ) {
-  return $('#svgenlargement .edge').filter( function(index) {
-    return $(this).children( 'title' ).text() == $('<div/>').html(edge_id).text() ;
-  });
-}
-
 function node_obj(ellipse) {
   this.ellipse = ellipse;
   var self = this;
@@ -119,7 +271,7 @@ function node_obj(ellipse) {
   this.node_elements = node_elements_for(self.ellipse);
 
   this.get_id = function() {
-    return self.ellipse.siblings('title').text()
+    return $(self.ellipse).parent().attr('id')
   }
   
   this.set_draggable = function( draggable ) {
@@ -130,8 +282,8 @@ function node_obj(ellipse) {
       self.ellipse.siblings('text').attr('class', 'noselect draggable');
     } else {
       self.ellipse.siblings('text').attr('class', '');
-      $(self.ellipse).parent().unbind('mouseenter').unbind('mouseleave').unbind('mousedown');     
-      $(self.ellipse).attr( {stroke:'green', fill:'#b3f36d'} );
+         $(self.ellipse).parent().unbind( 'mouseenter' ).unbind( 'mouseleave' ).unbind( 'mousedown' );     
+      color_inactive( self.ellipse );
     }
   }
 
@@ -158,9 +310,9 @@ function node_obj(ellipse) {
 
   this.mouseup_listener = function(evt) {    
     if( $('ellipse[fill="#ffccff"]').size() > 0 ) {
-        var source_node_id = self.ellipse.siblings('title').text();
+        var source_node_id = $(self.ellipse).parent().attr('id');
         var source_node_text = self.ellipse.siblings('text').text();
-        var target_node_id = $('ellipse[fill="#ffccff"]').siblings("title").text();
+        var target_node_id = $('ellipse[fill="#ffccff"]').parent().attr('id');
         var target_node_text = $('ellipse[fill="#ffccff"]').siblings("text").text();
         $('#source_node_id').val( source_node_id );
         $('#source_node_text').val( source_node_text );
@@ -174,7 +326,7 @@ function node_obj(ellipse) {
     $(self.ellipse).parent().hover( self.enter_node, self.leave_node );
     self.reset_elements();
   }
-
+  
   this.cpos = function() {
     return { x: self.ellipse.attr('cx'), y: self.ellipse.attr('cy') };
   }
@@ -280,7 +432,7 @@ function node_elements_for( ellipse ) {
 
 function get_edge_elements_for( ellipse ) {
   edge_elements = new Array();
-  node_id = ellipse.siblings('title').text();
+  node_id = ellipse.parent().attr('id');
   edge_in_pattern = new RegExp( node_id + '$' );
   edge_out_pattern = new RegExp( '^' + node_id );
   $.each( $('#svgenlargement .edge,#svgenlargement .relation').children('title'), function(index) {
@@ -309,12 +461,8 @@ function relation_factory() {
     this.relation_colors = [ "#5CCCCC", "#67E667", "#F9FE72", "#6B90D4", "#FF7673", "#E467B3", "#AA67D5", "#8370D8", "#FFC173" ];
 
     this.create_temporary = function( source_node_id, target_node_id ) {
-        var relation = $('#svgenlargement .relation').filter( function(index) {
-            var relation_id = $(this).children('title').text();
-            if( ( relation_id == ( source_node_id + '->' + target_node_id ) ) || ( relation_id == ( target_node_id + '->' + source_node_id ) ) ) {
-                return true;
-            } 
-        } );
+       var relation_id = get_relation_id( source_node_id, target_node_id );
+        var relation = $( jq( relation_id ) );
         if( relation.size() == 0 ) { 
             draw_relation( source_node_id, target_node_id, self.temp_color );
         } else {
@@ -342,16 +490,16 @@ function relation_factory() {
         return relation;
     }
     this.toggle_active = function( relation_id ) {
-        var relation = $("#svgenlargement .relation:has(title:contains('" + relation_id + "'))");
+        var relation = $( jq( relation_id ) );
         var relation_path = relation.children('path');
         if( !relation.data( 'active' ) ) {
             relation_path.css( {'cursor':'pointer'} );
             relation_path.mouseenter( function(event) { 
                 outerTimer = setTimeout( function() { 
                     timer = setTimeout( function() { 
-                        var title = relation.children('title').text();
-                        var source_node_id = title.substring( 0, title.indexOf( "->" ) );
-                        var target_node_id = title.substring( (title.indexOf( "->" ) + 2) );
+                        var related_nodes = get_related_nodes( relation_id );
+                        var source_node_id = related_nodes[0];
+                        var target_node_id = related_nodes[1];
                         $('#delete_source_node_id').val( source_node_id );
                         $('#delete_target_node_id').val( target_node_id );
                         self.showinfo(relation); 
@@ -384,7 +532,7 @@ function relation_factory() {
         var p = svg_root.createSVGPoint();
         p.x = xs + ((xe-xs)*1.1);
         p.y = ye - ((ye-ys)/2);
-        var ctm = svg_root.children[0].getScreenCTM();
+        var ctm = svg_root_element.getScreenCTM();
         var nx = p.matrixTransform(ctm).x;
         var ny = p.matrixTransform(ctm).y;
         var dialog_aria = $ ("div[aria-labelledby='ui-dialog-title-delete-form']");
@@ -392,14 +540,28 @@ function relation_factory() {
         dialog_aria.offset({ left: nx, top: ny });
     }
     this.remove = function( relation_id ) {
-        var relation = $("#svgenlargement .relation:has(title:contains('" + relation_id + "'))");
+        var relation = $( jq( relation_id ) );
         relation.remove();
     }
 }
 
+// Utility function to create/return the ID of a relation link between
+// a source and target.
+function get_relation_id( source_id, target_id ) {
+       var idlist = [ source_id, target_id ];
+       idlist.sort();
+       return 'relation-' + idlist[0] + '-...-' + idlist[1];
+}
+
+function get_related_nodes( relation_id ) {
+       var srctotarg = relation_id.substr( 9 );
+       return srctotarg.split('-...-');
+}
+
 function draw_relation( source_id, target_id, relation_color ) {
     var source_ellipse = get_ellipse( source_id );
     var target_ellipse = get_ellipse( target_id );
+    var relation_id = get_relation_id( source_id, target_id );
     var svg = $('#svgenlargement').children('svg').svg().svg('get');
     var path = svg.createPath(); 
     var sx = parseInt( source_ellipse.attr('cx') );
@@ -407,7 +569,8 @@ function draw_relation( source_id, target_id, relation_color ) {
     var sy = parseInt( source_ellipse.attr('cy') );
     var ex = parseInt( target_ellipse.attr('cx') );
     var ey = parseInt( target_ellipse.attr('cy') );
-    var relation = svg.group( $("#svgenlargement svg g"), {'class':'relation'} );
+    var relation = svg.group( $("#svgenlargement svg g"), 
+       { 'class':'relation', 'id':relation_id } );
     svg.title( relation, source_id + '->' + target_id );
     svg.path( relation, path.move( sx, sy ).curveC( sx + (2*rx), sy, ex + (2*rx), ey, ex, ey ), {fill: 'none', stroke: relation_color, strokeWidth: 4});
     var relation_element = $('#svgenlargement .relation').filter( ':last' );
@@ -428,11 +591,13 @@ $(document).ready(function () {
         .data('x', event.clientX)
         .data('y', event.clientY)
         .data('scrollLeft', this.scrollLeft)
-        stateTf = svg_root.children[0].getCTM().inverse();
+        stateTf = svg_root_element.getCTM().inverse();
         var p = svg_root.createSVGPoint();
         p.x = event.clientX;
         p.y = event.clientY;
         stateOrigin = p.matrixTransform(stateTf);
+        event.returnValue = false;
+        event.preventDefault();
         return false;
   }).mouseup(function (event) {
         $(this).data('down', false);
@@ -445,22 +610,28 @@ $(document).ready(function () {
         p = p.matrixTransform(stateTf);
         var matrix = stateTf.inverse().translate(p.x - stateOrigin.x, p.y - stateOrigin.y);
         var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
-        svg_root.children[0].setAttribute("transform", s);
+        svg_root_element.setAttribute("transform", s);
     }
+    event.returnValue = false;
+    event.preventDefault();
   }).mousewheel(function (event, delta) {
     event.returnValue = false;
     event.preventDefault();
     if ( $('#update_workspace_button').data('locked') == false ) {
+        if (!delta || delta == null || delta == 0) delta = event.originalEvent.wheelDelta;
+        if (!delta || delta == null || delta == 0) delta = -1 * event.originalEvent.detail;
         if( delta < -9 ) { delta = -9 }; 
         var z = 1 + delta/10;
-        var g = svg_root.children[0];
-        if( (z<1 && (g.getScreenCTM().a * start_element_height) > 4.0) || (z>1 && (g.getScreenCTM().a * start_element_height) < 100) ) {
+        z = delta > 0 ? 1 : -1;
+        var g = svg_root_element;
+        if (g && ((z<1 && (g.getScreenCTM().a * start_element_height) > 4.0) || (z>=1 && (g.getScreenCTM().a * start_element_height) < 100))) {
             var root = svg_root;
             var p = root.createSVGPoint();
-            p.x = event.clientX;
-            p.y = event.clientY;
+            p.x = event.originalEvent.clientX;
+            p.y = event.originalEvent.clientY;
             p = p.matrixTransform(g.getCTM().inverse());
-            var k = root.createSVGMatrix().translate(p.x, p.y).scale(z).translate(-p.x, -p.y);
+            var scaleLevel = 1+(z/20);
+            var k = root.createSVGMatrix().translate(p.x, p.y).scale(scaleLevel).translate(-p.x, -p.y);
             var matrix = g.getCTM().multiply(k);
             var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
             g.setAttribute("transform", s);
@@ -478,19 +649,24 @@ $(document).ready(function () {
     width: 290,
     modal: true,
     buttons: {
-      "Ok": function() {
+      "Ok": function( evt ) {
+       $(evt.target).button("disable");
         $('#status').empty();
-        form_values = $('#collapse_node_form').serialize()
-        ncpath = getRelationshipURL();
-        $(':button :contains("Ok")').attr("disabled", true);
+        form_values = $('#collapse_node_form').serialize();
+        ncpath = getTextURL( 'relationships' );
         var jqjson = $.post( ncpath, form_values, function(data) {
             $.each( data, function(item, source_target) { 
-                var relation = relation_manager.create( source_target[0], source_target[1], $('#rel_type').attr('selectedIndex') );
-                relation.data( 'type', $('#rel_type :selected').text()  );
-                relation.data( 'scope', $('#scope :selected').text()  );
-                relation.data( 'note', $('#note').val()  );
-                relation_manager.toggle_active( relation.children('title').text() );
-            });
+               var source_found = get_ellipse( source_target[0] );
+               var target_found = get_ellipse( source_target[1] );
+               if( source_found.size() && target_found.size() ) {
+                    var relation = relation_manager.create( source_target[0], source_target[1], $('#rel_type')[0].selectedIndex-1 );
+                                       relation.data( 'type', $('#rel_type :selected').text()  );
+                                       relation.data( 'scope', $('#scope :selected').text()  );
+                                       relation.data( 'note', $('#note').val()  );
+                                       relation_manager.toggle_active( relation.attr('id') );
+                               }
+                               $(evt.target).button("enable");
+           });
             $( "#dialog-form" ).dialog( "close" );
         }, 'json' );
       },
@@ -505,12 +681,12 @@ $(document).ready(function () {
         var jqjson = $.getJSON( basepath + '/definitions', function(data) {
             var types = data.types.sort();
             $.each( types, function(index, value) {   
-                 $('#rel_type').append( $('<option>').attr( "value", value ).text(value) ); 
+                 $('#rel_type').append( $('<option />').attr( "value", value ).text(value) ); 
                  $('#keymaplist').append( $('<li>').css( "border-color", relation_manager.relation_colors[index] ).text(value) ); 
             });
             var scopes = data.scopes;
             $.each( scopes, function(index, value) {   
-                 $('#scope').append( $('<option>').attr( "value", value ).text(value) ); 
+                 $('#scope').append( $('<option />').attr( "value", value ).text(value) ); 
             });
         });        
     },
@@ -528,10 +704,12 @@ $(document).ready(function () {
         $("#dialog_overlay").hide();
     }
   }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
-      if( ( ajaxSettings.type == 'POST' ) && jqXHR.status == 403 ) {
+      if( ajaxSettings.url == getTextURL('relationships') 
+       && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) {
          var errobj = jQuery.parseJSON( jqXHR.responseText );
           $('#status').append( '<p class="error">Error: ' + errobj.error + '</br>The relationship cannot be made.</p>' );
       }
+         $(event.target).parent().find('.ui-button').button("enable");
   } );
 
   $( "#delete-form" ).dialog({
@@ -544,11 +722,11 @@ $(document).ready(function () {
             $( this ).dialog( "close" );
         },
         Delete: function() {
-          form_values = $('#delete_relation_form').serialize()
-          ncpath = getRelationshipURL()
+          form_values = $('#delete_relation_form').serialize();
+          ncpath = getTextURL( 'relationships' );
           var jqjson = $.ajax({ url: ncpath, data: form_values, success: function(data) {
               $.each( data, function(item, source_target) { 
-                  relation_manager.remove( source_target[0] + '->' + source_target[1] );
+                  relation_manager.remove( get_relation_id( source_target[0], source_target[1] ) );
               });
               $( "#delete-form" ).dialog( "close" );
           }, dataType: 'json', type: 'DELETE' });
@@ -572,9 +750,79 @@ $(document).ready(function () {
     }
   });
 
+  // function for reading form dialog should go here; for now hide the element
+  $('#reading-form').dialog({
+       autoOpen: false,
+       height: 400,
+       width: 600,
+       modal: true,
+       buttons: {
+               Cancel: function() {
+                       $( this ).dialog( "close" );
+               },
+               Update: function( evt ) {
+                       // Disable the button
+                       $(evt.target).button("disable");
+                       $('#reading_status').empty();
+                       var reading_id = $('#reading_id').val()
+                       form_values = {
+                               'id' : reading_id,
+                               'is_nonsense': $('#reading_is_nonsense').is(':checked'),
+                               'grammar_invalid': $('#reading_grammar_invalid').is(':checked'),
+                               'normal_form': $('#reading_normal_form').val() };
+                       // Add the morphology values
+                       $('.reading_morphology').each( function() {
+                               if( $(this).val() != '(Click to select)' ) {
+                                       var rmid = $(this).attr('id');
+                                       rmid = rmid.substring(8);
+                                       form_values[rmid] = $(this).val();
+                               }
+                       });
+                       // Make the JSON call
+                       ncpath = getReadingURL( reading_id );
+                       var reading_element = readingdata[reading_id];
+                       // $(':button :contains("Update")').attr("disabled", true);
+                       var jqjson = $.post( ncpath, form_values, function(data) {
+                               $.each( data, function(key, value) { 
+                                       reading_element[key] = value;
+                               });
+                               if( $('#update_workspace_button').data('locked') == false ) {
+                                       color_inactive( get_ellipse( reading_id ) );
+                               }
+                               $(evt.target).button("enable");
+                               $( "#reading-form" ).dialog( "close" );
+                       });
+                       // Re-color the node if necessary
+                       return false;
+               }
+       },
+       create: function() {
+       },
+       open: function() {
+        $(".ui-widget-overlay").css("background", "none");
+        $("#dialog_overlay").show();
+        $('#reading_status').empty();
+        $("#dialog_overlay").height( $("#enlargement_container").height() );
+        $("#dialog_overlay").width( $("#enlargement_container").innerWidth() );
+        $("#dialog_overlay").offset( $("#enlargement_container").offset() );
+        $("#reading-form").parent().find('.ui-button').button("enable");
+       },
+       close: function() {
+               $("#dialog_overlay").hide();
+       }
+  }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
+      if( ajaxSettings.url.lastIndexOf( getReadingURL('') ) > -1
+       && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) {
+         var errobj = jQuery.parseJSON( jqXHR.responseText );
+          $('#reading_status').append( '<p class="error">Error: ' + errobj.error + '</p>' );
+      }
+         $(event.target).parent().find('.ui-button').button("enable");
+  });
+  
+
   $('#update_workspace_button').click( function() {
      var svg_enlargement = $('#svgenlargement').svg().svg('get').root();
-     mouse_scale = svg_root.children[0].getScreenCTM().a;
+     mouse_scale = svg_root_element.getScreenCTM().a;
      if( $(this).data('locked') == true ) {
          $('#svgenlargement ellipse' ).each( function( index ) {
              if( $(this).data( 'node_obj' ) != null ) {
@@ -590,7 +838,7 @@ $(document).ready(function () {
      } else {
          var left = $('#enlargement').offset().left;
          var right = left + $('#enlargement').width();
-         var tf = svg_root.children[0].getScreenCTM().inverse(); 
+         var tf = svg_root_element.getScreenCTM().inverse(); 
          var p = svg_root.createSVGPoint();
          p.x=left;
          p.y=100;
@@ -628,11 +876,57 @@ $(document).ready(function () {
       $('#svgenlargement .relation').find( "title:contains('" + node_id +  "')" ).each( function(index) {
           matchid = new RegExp( "^" + node_id );
           if( $(this).text().match( matchid ) != null ) {
-              relation_manager.toggle_active( $(this).text() );
+                 var relation_id = $(this).parent().attr('id');
+              relation_manager.toggle_active( relation_id );
           };
       });
   }
 
+  expandFillPageClients();
+  $(window).resize(function() {
+    expandFillPageClients();
+  });
+
 });
 
 
+function expandFillPageClients() {
+       $('.fillPage').each(function () {
+               $(this).height($(window).height() - $(this).offset().top - MARGIN);
+       });
+}
+
+function loadSVG(svgData) {
+       var svgElement = $('#svgenlargement');
+
+       $(svgElement).svg('destroy');
+
+       $(svgElement).svg({
+               loadURL: svgData,
+               onLoad : svgEnlargementLoaded
+       });
+}
+
+
+/*     OS Gadget stuff
+
+function svg_select_callback(topic, data, subscriberData) {
+       svgData = data;
+       loadSVG(svgData);
+}
+
+function loaded() {
+       var prefs = new gadgets.Prefs();
+       var preferredHeight = parseInt(prefs.getString('height'));
+       if (gadgets.util.hasFeature('dynamic-height')) gadgets.window.adjustHeight(preferredHeight);
+       expandFillPageClients();
+}
+
+if (gadgets.util.hasFeature('pubsub-2')) {
+       gadgets.HubSettings.onConnect = function(hum, suc, err) {
+               subId = gadgets.Hub.subscribe("interedition.svg.selected", svg_select_callback);
+               loaded();
+       };
+}
+else gadgets.util.registerOnLoadHandler(loaded);
+*/
index aa83fbc..2a9e10e 100644 (file)
@@ -1,5 +1,26 @@
 var colors = ['#ffeeaa','#afc6e9','#d5fff6','#ffccaa','#ffaaaa','#e5ff80','#e5d5ff','#ffd5e5'];
 var row_triggered = false;
+var original_svg;
+
+function handle_row_click( row ) {
+       var ridx = row.parent().parent().index()
+       var rs = readingstats[ridx];
+    var imghtml = '<img src="../images/ajax-loader.gif" alt="Loading SVG..."/>'
+    $('#stemma_graph').empty();
+    $('#stemma_graph').append( imghtml );
+       if( rs.layerwits ) {
+               var stemma_form = { 'dot': graphdot, 'layerwits': rs.layerwits };
+               $('#stemma_graph').load( 'graphsvg', stemma_form, function() {
+                       color_row( row );
+                       show_stats( rs );
+               });
+       } else {
+               $('#stemma_graph').empty();
+               $('#stemma_graph').append( original_svg );
+               color_row( row );
+               show_stats( rs );
+       }
+}
 
 function color_row( row ) {
     row_triggered = true;
@@ -37,3 +58,41 @@ function color_nodes( column_index, arr_node_ids, arr_greynode_ids ) {
       });
   });
 }
+
+function show_stats( rs ) {
+       var rshtml = $('#stats_template').clone();
+       rshtml.find('#statrank').append( rs.id );
+       $.each( rs.readings, function( idx, rdghash ) {
+               var rdgstats = $('#reading_template').clone();
+               rdgstats.find('.readinglabel').append( rdghash.text );
+               rdgstats.find('.reading_copied').append( rdghash.followed );
+               rdgstats.find('.reading_changed').append( rdghash.not_followed );
+               rdgstats.find('.reading_unclear').append( rdghash.follow_unknown );
+               rdgstats.find('.readingroots').append( rdghash.independent_occurrence );
+               if( ! $.isEmptyObject( rdghash.reading_parents ) ) {
+                       var parentstats = $('#reading_parent_template').clone();
+                       $.each( rdghash.reading_parents, function( parentid, pdata ) {
+                               var parentdesc = pdata.label;
+                               if( pdata.relation ) {
+                                       parentdesc += ' - variant type ' + pdata.relation.type;
+                                       if( pdata.relation.annotation ) {
+                                               parentdesc += ' [ ' + pdata.relation.annotation + ' ]';
+                                       }
+                               } else {
+                                       parentdesc += ' - no syntactic relation';
+                               }
+                               var parentitem = $('<li>').append( parentdesc );
+                               parentstats.find('.reading_parent_list').append( parentitem );
+                       });
+                       rdgstats.find('.reading_statistics').append( parentstats.contents() );
+               }
+               rshtml.append( rdgstats.contents() );
+       });
+       $('#row_statistics').empty();
+       $('#row_statistics').append( rshtml.contents() );
+};
+
+// Save the original unextended SVG for when we need it.
+$(document).ready(function () {
+       original_svg = $('#stemma_graph > svg').clone();
+});
index 246336d..68cebc7 100644 (file)
@@ -31,15 +31,13 @@ $(document).ready(function() {
          <span>Examine variants against this stemma</span>
         </div>
       </form>
-    </div>
-    <div id="variant_container">
-      <div style="float: left"><h2>Variant graph for <span class="texttitle">selected text</span></h2></div>
       <form id="run_relater" action="" method="GET" name="run_relater">
         <div class="button" id="relater_button" onClick="$('#run_relater').submit()">
           <span>Run relationship mapper</span>
         </div>
       </form>
-      <div id="variant_graph"></div>
+    </div>
+    <div id="variant_container">
     </div>
 
 [% PROCESS footer.tt %]
\ No newline at end of file
index 65bb19e..ee47b83 100644 (file)
@@ -4,15 +4,16 @@
        applicationstyle = c.uri_for('/css/relationship.css')
 %]
 <script type="text/javascript">
-$(function() {
-  $('#svgenlargement').svg({loadURL: '[% svg_string %]', onLoad: svgEnlargementLoaded});
+
+$(document).ready(function () {
+  loadSVG('[% svg_string %]');
 });
 </script>
 [% END %]
 
        <div id="topbanner">
                <div id="bannerinfo">
-                       <a href="help" title="Relationship mapper help" class="helptag">Help / About</a>
+                       <a href="help/[% text_lang %]" title="Relationship mapper help" class="helptag">Help / About</a>
                </div>
                <h1>Relationship mapper</h1>
                <h2>[% text_title %]</h2>
@@ -29,50 +30,83 @@ $(function() {
                </div>
        </div>
 
-       <div id="enlargement_container">        
+       <div id="enlargement_container" class="fillPage">       
                <div id="loading_overlay">
                        <div id="loading_message"><span>Loading, please wait...</span></div>
                </div>
                <div id="dialog_overlay"></div>
-               <div id="enlargement">
-               <div id="svgenlargement" style="height: 500px;"></div>
+               <div id="enlargement" style="">
+               <div id="svgenlargement"  class="fillPage"></div>
                </div>
        </div>
        
        <div id="update_workspace_button"></div>
        
-       <div id="dialog-form" title="Create relation between two nodes..">
-               <form id="collapse_node_form">
+       <div id="dialog-form" title="Create relation between two nodes...">
+               <form id="collapse_node_form" action="#">
                <fieldset>
                        <input type="hidden" name="source_id" id="source_node_id"/>
+                       <input type="hidden" name="source_text" id="source_node_text"/>
                        <input type="hidden" name="target_id" id="target_node_id"/>
+                       <input type="hidden" name="target_text" id="target_node_text"/>
                        <label for="rel_type">Relation type..&nbsp;</label>
                        <select name="rel_type" id="rel_type" class=".ui-widget select">
+                               <option></option>
                        </select>
                        <br/><br/>
                        <label for="scope">Scope of relation..&nbsp;</label>
                        <select name="scope" id="scope" class=".ui-widget select">
+                               <option></option>
                        </select>
                        <br/><br/>
                        <label for="note">Annotation or note..&nbsp;</label>
-                       <input type="text" width="60" name="note" id="note" class=".ui-widget input" />
+                       <textarea rows="3" style="width:100%;" name="note" id="note" class=".ui-widget input"></textarea>
                </fieldset>
        <div id="status"></div>         
                </form>
        </div>
        <div id="dialog_overlay"></div>
 
-       <div id="delete-form" title="Relation info..">
-               <form id="delete_relation_form">
+       <div id="delete-form" title="Relation info...">
+               <form id="delete_relation_form" action="#">
                        <input type="hidden" name="source_id" id="delete_source_node_id"/>
                        <input type="hidden" name="target_id" id="delete_target_node_id"/>
                </form>
                <div id="delete-form-text"></div>
        </div>
-       
+       
+       <div id="reading-form" title="Reading info...">
+               <form id="reading_data_form" action="#">
+                       <input type="hidden" name="reading_id" id="reading_id"/>
+                       <input type="checkbox" name="reading_is_nonsense" id="reading_is_nonsense"/>
+                       <label for="reading_is_nonsense">This is a nonsense word</label>
+                       <br/>
+                       <input type="checkbox" name="reading_grammar_invalid" id="reading_grammar_invalid"/>
+                       <label for="reading_grammar_invalid">This word's grammar cannot be right</label>
+                       <br/><br/>
+                       <!-- Morphological options go here -->
+                       <div id="normalization">
+                               <label for="reading_normal_form">Normalized form: </label>
+                               <input type="text" name="reading_normal_form" id="reading_normal_form"></input>
+                               <button id="#reading_relemmatize" onclick="relemmatize(); return false;">Re-lemmatize</button>
+                       </div>
+                       <div id="relemmatize_pending">
+                               <img src="[% c.uri_for('/images/ajax-loader.gif') %]"/>
+                       </div>
+                       <br/><br/>
+                       <div id="morph_outer">
+                               <label>Lemma / part of speech:</label><br/>
+                               <div id="morphology"></div>
+                       </div>
+                       </select>
+                       <div id="reading_status"></div>
+               </form>
+    </div>
+       
     <p/><p/>   
     <div id="keymap">
         <ul id="keymaplist">
+          <li></li>
         </ul>
     </div>
     
index 607f22d..a62e56e 100644 (file)
@@ -11,7 +11,7 @@
  
  <p>The premise of the tool is that, once a set of texts has been collated, there will be a need to chart the relationships between the variants—are they substantially the same word? Different words meaning the same thing? Is one an orthographic variant of the other that should be excluded from any eventual apparatus?</p>
  
- <h3>Instructions for use</h3>
+ <h3>Making relationships between words</h3>
  
  <p>The tool itself is an interface for allowing these relationships to be declared.  The collation is presented as a variant graph running from left to right.  In a variant graph, each node is a reading, and each witness takes a single path through the readings from beginning to end.  When readings appear vertically aligned with each other, it is an indication that they are variant readings, occurring at the same point in the text.
  
  
  <p>The relationships are displayed as colored paths between readings; while in 'edit' mode, clicking on a relationship path will display the information associated with it, and give the user an option to delete it.  Deletion of a 'global' relationship will remove that relationship throughout the graph.  When you are ready to move elsewhere in the graph, click the 'hand' icon to return to select mode.<p>
  
- <p>Please note that this tool is known to work with recent versions of Firefox (e.g. 8, 9, 10); it is known not to work with Safari and Chrome.</p>
+[% IF language != 'NONE' %]
+ <h3>Adding [% language %] morphological information to readings</h3>
  
+ <p>It is also possible to add morphological information to the readings in this text (that is, lemma and morphological tagging).  Double click on any reading to bring up the morphology info.  The options therein are:</p>
+ <ul>
+       <li>Indicate if the word is a nonsense word</li>
+       <li>Indicate if the word (whether sensical or not) cannot be grammatically correct here</li>
+       <li>Indicate the "normal" form of the word for lemmatization purposes.  Any readings linked to this one via 'spelling' or 'orthographic' links will appear when the entry text box is clicked.</li>
+       <li>Indicate the correct morphological form of this reading.  The shorthand syntax is:<br/>
+               &lt;LEMMA&gt; // cat@&lt;CATEGORY&gt; (type@&lt;TYPE&gt; etc.)<br/>
+               Each form must have at least a lemma and a category, and may have a number of additional features depending on the category (e.g. type, gender, number, case.) The possible options are as follows:
+               <ul>
+[% FOREACH stype IN tagset.structures -%]
+       [% IF stype.desc != 'use_features' -%]
+                       <li>Category [% stype.id -%] ([% stype.desc %])
+       [% END -%]
+[% IF stype.use_features.size -%]
+ has features [% stype.use_features.join(', ') %]
+[% END -%]
+                       </li>
+[% END -%]
+               </ul>
+               <ul>
+[% FOREACH ftype IN tagset.features.keys.sort -%]
+                       <li>Feature [% ftype %] can have values:
+                               <ul>
+[% FOREACH fval IN tagset.features.$ftype.values -%]
+                                       <li>[% fval.short %] =&gt; [% fval.long %]</li>
+[% END -%]
+                               </ul>
+                       </li>
+[% END -%]
+               </ul>
+       </li>
+  </ul>
+  
+  <p>If initial lemmatization has been performed on the text, a number of readings may appear in yellow rather than green; this means that there are multiple possible morphologies for the reading in question. Double click on the reading to select and save the correct morphology.</p>
+[% END -%]
 [% PROCESS footer.tt %]
\ No newline at end of file
index be3f0ad..25f1b32 100644 (file)
@@ -5,13 +5,7 @@
 %]
 <script type="text/javascript">
 var readingstats = [% reading_statistics %];
-function show_stats( row_index ) {
-       var rs = readingstats[row_index];
-       var rshtml = $('#stats_template').clone();
-       rshtml.find('#statrank').append( rs.id );
-       $('#row_statistics').empty();
-       $('#row_statistics').append( rshtml.contents() );
-};
+var graphdot = '[% graphdot %]';
 </script>
 [% END -%]
     <h1>Stexaminer</h1>
@@ -23,21 +17,41 @@ function show_stats( row_index ) {
 [% END -%]
      </table>
     </div>
-    <div id="stemma_graph">
-      [% svg %]
+    <div>
+               <div id="stemma_graph">
+                 [% svg %]
+               </div>
+               <div id="row_statistics">
+                 <h3>Aggregate text statistics</h3>
+                 <ul>
+                       <li>Total number of variant locations analyzed: [% total %]</li>
+                       <li>Number of fully genealogical locations: [% genealogical %]</li>
+                       <li>Number of readings that conflict with the stemma: [% conflict %]</li>
+                       <li>Genealogical reading transitions by relationship type: [% todo %]</li>
+                 </ul>
+                 <p>(Choose a row in the table to display statistics about individual readings.)</p>
+               </div>
     </div>
-    <div id="row_statistics">
-      <h3>Aggregate text statistics</h3>
-      <ul>
-       <li>Total number of variant locations analyzed: [% total %]</li>
-        <li>Number of fully genealogical locations: [% genealogical %]</li>
-        <li>Number of readings that conflict with the stemma: [% conflict %]</li>
-      </ul>
-      <p>(Choose a row in the table to display statistics about individual readings.)</p>
+    
+    <div id="stats_template" class="template">
+           <h3>Statistics for readings at <span id="statrank"></span>:</h3>
+           <!-- reading_template will be appended here for each reading -->
     </div>
-    <div id="stats_template">
-           <h3>Statistics for readings at <span id="statrank"></span></h3>
+    
+       <div id="reading_template" class="template">
+               <div class="reading_statistics">
+                       <span class="readinglabel"></span> - copied <span class="reading_copied"></span> time(s), changed <span class="reading_changed"></span> time(s)<br/>
+                       Reading root(s) at <span class="readingroots"></span><br/>
+                       <!-- reading_parent_template will be appended here if there are parents -->
+               </div>
     </div>
+    
+       <div id="reading_parent_template" class="template">
+               <div class="parent_statistics">
+                       Reading parent(s):
+                       <ul class="reading_parent_list"></ul>
+               </div>
+       </div>
 
 [% PROCESS footer.tt %]
     
@@ -46,7 +60,7 @@ function show_stats( row_index ) {
 [% SET rowclass = 'class="genealogical"' IF row.genealogical -%]
 [% SET rowclass = 'class="coincidental"' UNLESS row.genealogical -%]
         <tr [% rowclass %]>
-          <th><span class="rowid" onclick="color_row($(this));show_stats($(this).parent().parent().index())">[% row.id %]</span></th>
+          <th><span class="rowid" onclick="handle_row_click($(this))">[% row.id %]</span></th>
 [% FOREACH reading IN row.readings -%]
 [% SET cellclass = 'clickable conflict' IF reading.conflict -%]
 [% SET cellclass = 'clickable' IF !reading.conflict -%]