morphology piece now works for main submit, not for relemmatization yet
[scpubgit/stemmatology.git] / stemmaweb / lib / stemmaweb / Controller / Relation.pm
1 package stemmaweb::Controller::Relation;
2 use Moose;
3 use namespace::autoclean;
4 use TryCatch;
5
6 BEGIN { extends 'Catalyst::Controller' }
7
8
9 =head1 NAME
10
11 stemmaweb::Controller::Relation - Controller for the relationship mapper
12
13 =head1 DESCRIPTION
14
15 The reading relationship mapper with draggable nodes.
16
17 =head1 METHODS
18
19 =head2 index
20
21  GET relation/$textid
22  
23 Renders the application for the text identified by $textid.
24
25 =cut
26
27 sub index :Path :Args(0) {
28         my( $self, $c ) = @_;
29         $c->stash->{'template'} = 'relate.tt';
30 }
31
32 =head2 help
33
34  GET relation/help
35
36 Returns the help window HTML.
37
38 =cut
39
40 sub help :Local :Args(0) {
41         my( $self, $c ) = @_;
42         $c->stash->{'template'} = 'relatehelp.tt';
43 }
44
45 =head2 definitions
46
47  GET relation/definitions
48  
49 Returns a data structure giving the valid types and scopes for a relationship.
50
51 =cut
52
53 sub definitions :Local :Args(0) {
54         my( $self, $c ) = @_;
55         my $valid_relationships = [ qw/ spelling orthographic grammatical lexical transposition / ];
56         my $valid_scopes = [ qw/ local global / ];
57         $c->stash->{'result'} = { 'types' => $valid_relationships, 'scopes' => $valid_scopes };
58         $c->forward('View::JSON');
59 }
60
61 =head2 text
62
63  GET relation/$textid/
64  
65  Runs the relationship mapper for the specified text ID.
66  
67 =cut
68
69 sub text :Chained('/') :PathPart('relation') :CaptureArgs(1) {
70         my( $self, $c, $textid ) = @_;
71         # If the tradition has more than 500 ranks or so, split it up.
72         my $tradition = $c->model('Directory')->tradition( $textid );
73     # Account for a bad interaction between FastCGI and KiokuDB
74     unless( $tradition->collation->tradition ) {
75         $c->log->warn( "Fixing broken tradition link" );
76         $tradition->collation->_set_tradition( $tradition );
77         $c->model('Directory')->save( $tradition );
78     }
79         # See how big the tradition is. Edges are more important than nodes
80         # when it comes to rendering difficulty.
81         my $numnodes = scalar $tradition->collation->readings;
82         my $numedges = scalar $tradition->collation->paths;
83         my $length = $tradition->collation->end->rank;
84         # We should display no more than roughly 500 nodes, or roughly 700
85         # edges, at a time.
86         my $segments = $numnodes / 500;
87         if( $numedges / 700 > $segments ) {
88                 $segments = $numedges / 700;
89         }
90         my $segsize = sprintf( "%.0f", $length / $segments );
91         my $margin = sprintf( "%.0f", $segsize / 10 );
92         if( $segments > 1 ) {
93                 # Segment the tradition in order not to overload the browser.
94                 my @divs;
95                 my $r = 0;
96                 while( $r + $margin < $length ) {
97                         push( @divs, $r );
98                         $r += $segsize;
99                 }
100                 $c->stash->{'textsegments'} = [];
101                 $c->stash->{'segsize'} = $segsize;
102                 $c->stash->{'margin'} = $margin;
103                 foreach my $i ( 0..$#divs ) {
104                         my $seg = { 'start' => $divs[$i] };
105                         $seg->{'display'} = "Segment " . ($i+1);
106                         push( @{$c->stash->{'textsegments'}}, $seg );
107                 }
108         }
109         $c->stash->{'textid'} = $textid;
110         $c->stash->{'tradition'} = $tradition;
111 }
112
113 sub main :Chained('text') :PathPart('') :Args(0) {
114         my( $self, $c ) = @_;
115         my $startseg = $c->req->param('start');
116         my $tradition = delete $c->stash->{'tradition'};
117         my $collation = $tradition->collation;
118         my $svgopts;
119         if( $startseg ) {
120                 # Only render the subgraph from startseg to endseg or to END,
121                 # whichever is less.
122                 my $endseg = $startseg + $c->stash->{'segsize'} + $c->stash->{'margin'};
123                 $svgopts = { 'from' => $startseg };
124                 $svgopts->{'to'} = $endseg if $endseg < $collation->end->rank;
125         } elsif( exists $c->stash->{'textsegments'} ) {
126                 # This is the unqualified load of a long tradition. We implicitly start 
127                 # at zero, but go only as far as 550.
128                 my $endseg = $c->stash->{'segsize'} + $c->stash->{'margin'};
129                 $startseg = 0;
130                 $svgopts = { 'to' => $endseg };
131         }
132         my $svg_str = $collation->as_svg( $svgopts );
133         $svg_str =~ s/\n//gs;
134         $c->stash->{'startseg'} = $startseg if defined $startseg;
135         $c->stash->{'svg_string'} = $svg_str;
136         $c->stash->{'text_title'} = $tradition->name;
137         $c->stash->{'template'} = 'relate.tt';
138 }
139
140 =head2 relationships
141
142  GET relation/$textid/relationships
143
144 Returns the list of relationships defined for this text.
145
146  POST relation/$textid/relationships { request }
147  
148 Attempts to define the requested relationship within the text. Returns 200 on
149 success or 403 on error.
150
151  DELETE relation/$textid/relationships { request }
152  
153
154 =cut
155
156 sub relationships :Chained('text') :PathPart :Args(0) {
157         my( $self, $c ) = @_;
158         my $tradition = delete $c->stash->{'tradition'};
159         my $collation = $tradition->collation;
160         my $m = $c->model('Directory');
161         if( $c->request->method eq 'GET' ) {
162                 my @pairs = $collation->relationships; # returns the edges
163                 my @all_relations;
164                 foreach my $p ( @pairs ) {
165                         my $relobj = $collation->relations->get_relationship( @$p );
166                         next if $relobj->type eq 'collated'; # Don't show these
167                         next if $p->[0] eq $p->[1]; # HACK until bugfix
168                         my $relhash = { source => $p->[0], target => $p->[1], 
169                                   type => $relobj->type, scope => $relobj->scope };
170                         $relhash->{'note'} = $relobj->annotation if $relobj->has_annotation;
171                         push( @all_relations, $relhash );
172                 }
173                 $c->stash->{'result'} = \@all_relations;
174         } elsif( $c->request->method eq 'POST' ) {
175                 my $node = $c->request->param('source_id');
176                 my $target = $c->request->param('target_id');
177                 my $relation = $c->request->param('rel_type');
178                 my $note = $c->request->param('note');
179                 my $scope = $c->request->param('scope');
180         
181                 my $opts = { 'type' => $relation,
182                                          'scope' => $scope };
183                 $opts->{'annotation'} = $note if $note;
184                 
185                 try {
186                         my @vectors = $collation->add_relationship( $node, $target, $opts );
187                         $c->stash->{'result'} = \@vectors;
188                         $m->save( $tradition );
189                 } catch( Text::Tradition::Error $e ) {
190                         $c->response->status( '403' );
191                         $c->stash->{'result'} = { 'error' => $e->message };
192                 }
193         } elsif( $c->request->method eq 'DELETE' ) {
194                 my $node = $c->request->param('source_id');
195                 my $target = $c->request->param('target_id');
196         
197                 try {
198                         my @vectors = $collation->del_relationship( $node, $target );
199                         $m->save( $tradition );
200                         $c->stash->{'result'} = \@vectors;
201                 } catch( Text::Tradition::Error $e ) {
202                         $c->response->status( '403' );
203                         $c->stash->{'result'} = { 'error' => $e->message };
204                 }       
205         }
206         $c->forward('View::JSON');
207 }
208
209 =head2 readings
210
211  GET relation/$textid/readings
212
213 Returns the list of readings defined for this text along with their metadata.
214
215 =cut
216
217 my %read_write_keys = (
218         'id' => 0,
219         'text' => 0,
220         'is_meta' => 0,
221         'grammar_invalid' => 1,
222         'is_nonsense' => 1,
223         'normal_form' => 1,
224 );
225
226 sub _reading_struct {
227         my( $reading ) = @_;
228         # Return a JSONable struct of the useful keys.  Keys meant to be writable
229         # have a true value; read-only keys have a false value.
230         my $struct = {};
231         map { $struct->{$_} = $reading->$_ } keys( %read_write_keys );
232         # Special case
233         $struct->{'lexemes'} = [ $reading->lexemes ];
234         # Look up any words related via spelling or orthography
235         my $sameword = sub { 
236                 my $t = $_[0]->type;
237                 return $t eq 'spelling' || $t eq 'orthographic';
238         };
239         my @variants;
240         foreach my $sr ( $reading->related_readings( $sameword ) ) {
241                 push( @variants, $sr->text );
242         }
243         $struct->{'variants'} = \@variants;
244         return $struct;
245 }
246
247 sub readings :Chained('text') :PathPart :Args(0) {
248         my( $self, $c ) = @_;
249         my $tradition = delete $c->stash->{'tradition'};
250         my $collation = $tradition->collation;
251         my $m = $c->model('Directory');
252         if( $c->request->method eq 'GET' ) {
253                 my $rdginfo = {};
254                 foreach my $rdg ( $collation->readings ) {
255                         $rdginfo->{$rdg->id} = _reading_struct( $rdg );
256                 }
257                 $c->stash->{'result'} = $rdginfo;
258         }
259         $c->forward('View::JSON');
260 }
261
262 =head2 reading
263
264  GET relation/$textid/reading/$id
265
266 Returns the list of readings defined for this text along with their metadata.
267
268  POST relation/$textid/reading/$id { request }
269  
270 Alters the reading according to the values in request. Returns 403 Forbidden if
271 the alteration isn't allowed.
272
273 =cut
274
275 sub reading :Chained('text') :PathPart :Args(1) {
276         my( $self, $c, $reading_id ) = @_;
277         my $tradition = delete $c->stash->{'tradition'};
278         my $collation = $tradition->collation;
279         my $rdg = $collation->reading( $reading_id );
280         my $m = $c->model('Directory');
281         if( $c->request->method eq 'GET' ) {
282                 $c->stash->{'result'} = $rdg ? _reading_struct( $rdg )
283                         : { 'error' => "No reading with ID $reading_id" };
284         } elsif ( $c->request->method eq 'POST' ) {
285                 # Are we re-lemmatizing?
286                 if( $c->request->param('relemmatize') ) {
287                         my $nf = $c->request->param('normal_form');
288                         # TODO throw error unless $nf
289                         $rdg->normal_form( $nf );
290                         $rdg->lemmatize();
291                 } else {
292                         # Set all the values that we have for the reading.
293                         # TODO error handling
294                         foreach my $p ( keys %{$c->request->params} ) {
295                                 if( $p =~ /^morphology_(\d+)$/ ) {
296                                         # Set the form on the correct lexeme
297                                         my $midx = $1;
298                                         $c->log->debug( "Fetching lexeme $midx" );
299                                         my $lx = $rdg->lexeme( $midx );
300                                         my $strrep = $rdg->language . ' // ' 
301                                                 . $c->request->param( $p );
302                                         my $idx = $lx->has_form( $strrep );
303                                         unless( defined $idx ) {
304                                                 # Make the word form and add it to the lexeme.
305                                                 $c->log->debug("Adding new form for $strrep");
306                                                 $idx = $lx->add_matching_form( $strrep ) - 1;
307                                         }
308                                         $lx->disambiguate( $idx );
309                                 } elsif( $read_write_keys{$p} ) {
310                                         $rdg->$p( $c->request->param( $p ) );
311                                 }
312                         }               
313                 }
314                 $m->save( $tradition );
315                 $c->stash->{'result'} = _reading_struct( $rdg );
316
317         }
318         $c->forward('View::JSON');
319
320 }
321
322 =head2 end
323
324 Attempt to render a view, if needed.
325
326 =cut
327
328 sub end : ActionClass('RenderView') {}
329
330 =head1 AUTHOR
331
332 Tara L Andrews
333
334 =head1 LICENSE
335
336 This library is free software. You can redistribute it and/or modify
337 it under the same terms as Perl itself.
338
339 =cut
340
341 __PACKAGE__->meta->make_immutable;
342
343 1;