request reading data on page load
[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 sub _reading_struct {
218         my( $reading ) = @_;
219         # Return a JSONable struct of the useful keys.  Keys meant to be writable
220         # have a true value; read-only keys have a false value.
221         my %read_write_keys = (
222                 'id' => 0,
223                 'text' => 0,
224                 'is_meta' => 0,
225                 'grammar_invalid' => 1,
226                 'is_nonsense' => 1,
227                 'normal_form' => 1,
228                 'lexemes' => 1,  # special case?
229         );
230         my $struct = {};
231         map { $struct->{$_} = $reading->$_ } keys( %read_write_keys );
232         # Special case
233         $struct->{'lexemes'} = [ $reading->lexemes ];
234         return $struct;
235 }
236
237 sub readings :Chained('text') :PathPart :Args(0) {
238         my( $self, $c ) = @_;
239         my $tradition = delete $c->stash->{'tradition'};
240         my $collation = $tradition->collation;
241         my $m = $c->model('Directory');
242         if( $c->request->method eq 'GET' ) {
243                 my $rdginfo = {};
244                 foreach my $rdg ( $collation->readings ) {
245                         $rdginfo->{$rdg->id} = _reading_struct( $rdg );
246                 }
247                 $c->stash->{'result'} = $rdginfo;
248         }
249         $c->forward('View::JSON');
250 }
251
252 =head2 reading
253
254  GET relation/$textid/reading/$id
255
256 Returns the list of readings defined for this text along with their metadata.
257
258  POST relation/$textid/reading/$id { request }
259  
260 Alters the reading according to the values in request. Returns 403 Forbidden if
261 the alteration isn't allowed.
262
263 =cut
264
265 sub reading :Chained('text') :PathPart :Args(1) {
266         my( $self, $c, $reading_id ) = @_;
267         my $tradition = delete $c->stash->{'tradition'};
268         my $collation = $tradition->collation;
269         my $m = $c->model('Directory');
270         if( $c->request->method eq 'GET' ) {
271                 my $rdg = $collation->reading( $reading_id );
272                 $c->stash->{'result'} = $rdg ? _reading_struct( $rdg )
273                         : { 'error' => "No reading with ID $reading_id" };
274         } elsif ( $c->request->method eq 'POST' ) {
275                 # TODO Update the reading if we can.
276         }
277         $c->forward('View::JSON');
278
279 }
280
281 =head2 end
282
283 Attempt to render a view, if needed.
284
285 =cut
286
287 sub end : ActionClass('RenderView') {}
288
289 =head1 AUTHOR
290
291 Tara L Andrews
292
293 =head1 LICENSE
294
295 This library is free software. You can redistribute it and/or modify
296 it under the same terms as Perl itself.
297
298 =cut
299
300 __PACKAGE__->meta->make_immutable;
301
302 1;