File Coverage

blib/lib/Game/HexDescribe.pm
Criterion Covered Total %
statement 45 74 60.8
branch 5 16 31.2
condition 1 2 50.0
subroutine 12 16 75.0
pod 4 5 80.0
total 67 113 59.2


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             # Copyright (C) 2018–2022 Alex Schroeder
3             #
4             # This program is free software: you can redistribute it and/or modify it under
5             # the terms of the GNU Affero General Public License as published by the Free
6             # Software Foundation, either version 3 of the License, or (at your option) any
7             # later version.
8             #
9             # This program is distributed in the hope that it will be useful, but WITHOUT
10             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU Affero General Public License along
15             # with this program. If not, see .
16              
17             =encoding utf8
18              
19             =head1 NAME
20              
21             Game::HexDescribe - a web app to add random table driven data to map data
22              
23             =head1 DESCRIPTION
24              
25             Hex Describe is a web application which uses recursive random tables to create
26             the description of a map. A map in this context is a hex map. This is different
27             from other such tools like Tracery because a collection of locations on a maps
28             differ from a list of unrelated items. Neighbouring locations can share features
29             and thus a river can flow through many locations, a forest can cover many
30             locations, and so on.
31              
32             On a technical level, Hex Describe is a web app based on the Mojolicious
33             framework. This class in particular uses L.
34              
35             See L for more information.
36              
37             =cut
38              
39             package Game::HexDescribe;
40              
41             our $VERSION = 1.02;
42              
43 1     1   3636 use Modern::Perl;
  1         2  
  1         9  
44 1     1   690 use Mojolicious::Lite;
  1         65446  
  1         7  
45 1     1   21977 use Mojo::UserAgent;
  1         2  
  1         7  
46 1     1   27 use Mojo::Util qw(html_unescape);
  1         1  
  1         40  
47 1     1   6 use Mojo::ByteStream;
  1         2  
  1         34  
48 1         121 use Game::HexDescribe::Utils qw(init describe_text parse_table load_table
49 1     1   616 describe_map parse_map load_map);
  1         3  
50 1     1   7 use Game::HexDescribe::Log;
  1         2  
  1         21  
51 1     1   4 use Encode qw(decode_utf8);
  1         2  
  1         39  
52 1     1   558 use File::ShareDir qw(dist_dir);
  1         21927  
  1         65  
53 1     1   9 use Cwd;
  1         2  
  1         4528  
54              
55             # Commands for the command line!
56             push @{app->commands->namespaces}, 'Game::HexDescribe::Command';
57              
58             =head2 Configuration
59              
60             As a Mojolicious application, it will read a config file called
61             F in the same directory, if it exists. As the default log
62             level is 'warn', one use of the config file is to change the log level using
63             the C key, and if you're not running the server in a terminal, using
64             the C key to set a file.
65              
66             The default map and table are stored in the F directory. You can change
67             this directory using the C key. By default, the directory included with
68             the distribution is used. Thus, if you're a developer, you probably want to use
69             something like the following to use the files from the source directory.
70              
71             {
72             loglevel => 'debug',
73             logfile => undef,
74             contrib => 'share',
75             };
76              
77             The default map was created using Text Mapper's Alpine algorithm at one point in
78             time and the code has changed in the mean time, so it cannot be recreated
79             anymore.
80              
81             =cut
82              
83             plugin Config => {
84             default => {
85             loglevel => 'warn',
86             logfile => undef,
87             contrib => dist_dir('Game-HexDescribe'),
88             },
89             file => getcwd() . '/hex-describe.conf',
90             };
91              
92             my $log = Game::HexDescribe::Log->get;
93             $log->level(app->config('loglevel'));
94             $log->path(app->config('logfile'));
95             $log->info($log->path ? "Logfile is " . $log->path : "Logging to stderr");
96              
97             =head2 URLs
98              
99             The code needs to know where Text Mapper and the Face Generator can be found.
100             You can add these to the same config file we mentioned above. This is what you
101             probably want as a developer:
102              
103             {
104             text_mapper_url => 'http://localhost:3010',
105             face_generator_url => 'http://localhost:3020',
106             };
107              
108             This assumes you are running the two locally. See L for Text
109             Mapper.
110              
111             =cut
112              
113             my $text_mapper_url = app->config('text_mapper_url') || 'https://campaignwiki.org/text-mapper';
114             $log->debug("Text Mapper URL: $text_mapper_url");
115              
116             $Game::HexDescribe::Util::face_generator_url = app->config('face_generator_url') || 'https://campaignwiki.org/face';
117             $log->debug("Face Generator URL: $Game::HexDescribe::Util::face_generator_url");
118              
119             $Game::HexDescribe::Util::text_mapper_url = app->config('text_mapper_url') || 'https://campaignwiki.org/text-mapper';
120             $log->debug("Text Mapper URL: $Game::HexDescribe::Util::text_mapper_url");
121              
122             =head2 Entry Points
123              
124             As this is a web app, the URLs you can call are basically the API it exposes.
125             Each URL can accept either I or I requests, or I.
126              
127             =over 4
128              
129             =item get /
130              
131             The default entry point is where you I your map and table. B is the
132             map, B is the URL to an external table, B is the text of the table
133             if you want to paste it. See C below if you want to display the
134             result instead of allow the user to edit the form.
135              
136             =cut
137              
138             get '/' => sub {
139             my $c = shift;
140             my $map = $c->param('map') || load_map('default', app->config('contrib'));
141             my $url = $c->param('url');
142             my $table = $c->param('table');
143             $c->render(template => 'edit', map => $map, url => $url, table => $table);
144             } => 'edit';
145              
146             =item get /load/random/smale
147              
148             This shows you the I page again, with a new random map generated by Text
149             Mapper using the Smale algorithm.
150              
151             =cut
152              
153             get '/load/random/smale' => sub {
154             my $c = shift;
155             my $url = "$text_mapper_url/smale/random/text";
156             my $map = get_data($url);
157             $c->render(template => 'edit', map => $map, url=>'', table => '');
158             };
159              
160             =item get /load/random/apocalypse
161              
162             This shows you the I page again, with a new random map generated by Text
163             Mapper using the Apocalypse algorithm.
164              
165             =cut
166              
167             get '/load/random/apocalypse' => sub {
168             my $c = shift;
169             my $url = "$text_mapper_url/apocalypse/random/text";
170             my $map = get_data($url);
171             $c->render(template => 'edit', map => $map, url=>'', table => '');
172             };
173              
174             =item get /load/random/traveller
175              
176             This shows you the I page again, with a new random map generated by Text
177             Mapper using the Traveller algorithm.
178              
179             =cut
180              
181             get '/load/random/traveller' => sub {
182             my $c = shift;
183             my $url = "$text_mapper_url/traveller/random/text";
184             my $map = get_data($url);
185             $c->render(template => 'edit', map => $map, url=>'', table => '');
186             };
187              
188             =item get /load/random/alpine
189              
190             This shows you the I page again, with a new random map generated by Text
191             Mapper using the Alpine algorithm.
192              
193             =cut
194              
195             get '/load/random/alpine' => sub {
196             my $c = shift;
197             my $url = "$text_mapper_url/alpine/random/text";
198             my $map = get_data($url);
199             $c->render(template => 'edit', map => $map, url=>'', table => '');
200             };
201              
202             =item get /stats/random/alpine
203              
204             This uses a random map and the Alpine algorithm, and describes the map, and then
205             it presents you with some stats.
206              
207             =cut
208              
209             get '/stats/random/alpine' => sub {
210             my $c = shift;
211             my $url = "$text_mapper_url/alpine/random/text";
212             my $map_data = parse_map(get_data($url));
213             my %reverse_map;
214             for my $coords (keys %$map_data) {
215             for my $type (@{$map_data->{$coords}}) {
216             $reverse_map{$type} ||= 0;
217             $reverse_map{$type}++;
218             }
219             }
220             my $table = load_table('schroeder', app->config('contrib'));
221             my $descriptions = describe_map($map_data, parse_table($table), 0);
222             my %reverse_creatures;
223             for my $coords (keys %$descriptions) {
224             while ($descriptions->{$coords} =~ m!(.+?)!g) {
225             $reverse_creatures{$1} ||= 0;
226             $reverse_creatures{$1}++;
227             }
228             }
229             $c->render(template => 'stats',
230             map_stats => \%reverse_map,
231             creature_stats => \%reverse_creatures);
232             };
233              
234             =item any /describe
235              
236             This is where the actual map is described.
237              
238             B is the map, B is the URL to an external table, B is the text
239             of the table, and a table will be loaded based on the B parameter. Current
240             valid values are I, I, I, I, and
241             I.
242              
243             If we want to call this from the command line, we will need to request a map
244             from Text Mapper, too.
245              
246             text-mapper get /alpine.txt > map.txt
247             hex-describe get /describe --form map=@map.txt --form load=schroeder
248              
249             Pipe through C to get text instead of HTML.
250              
251             =cut
252              
253             any '/describe' => sub {
254             my $c = shift;
255             my $map = $c->param('map');
256             my $labels = $c->param('labels');
257             my $markdown = $c->param('markdown');
258             my $faces = $c->param('faces');
259             my $table = get_table($c);
260             init();
261             my $descriptions = describe_map(parse_map($map), parse_table($table), $faces);
262             if ($markdown) {
263             my $texts = [];
264             my $top = delete $descriptions->{TOP};
265             my $end = delete $descriptions->{END};
266             push(@$texts, $top) if $top;
267             for my $hex (sort keys %$descriptions) {
268             push(@$texts, "**$hex**: $descriptions->{$hex}");
269             }
270             push(@$texts, $end) if $end;
271             $c->render(text => markdown($texts), format => 'txt');
272             } else {
273             $map = add_labels($map) if $labels;
274             my $svg = get_post_data($text_mapper_url . '/render', map => $map);
275             $c->render(template => 'description',
276             svg => add_links($svg),
277             descriptions => $descriptions);
278             }
279             };
280              
281             =item get /describe/random/smale
282              
283             This variant is for when you want to just keep reloading and getting different
284             maps with different descriptions. Note that you may pass a C parameter,
285             which determines the map retrieved by Text Mapper. This allows you to refer to
286             an existing, random map, if you use the seed parameter in that URL. If you don't
287             provide a URL, a random map using the Smale algorithm will get used. The
288             description will be generated using the Seckler tables.
289              
290             =cut
291              
292             get '/describe/random/smale' => sub {
293             my $c = shift;
294             my $labels = $c->param('labels');
295             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
296             my $map = get_data($url);
297             my $table = load_table('seckler', app->config('contrib'));
298             init();
299             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
300             $map = add_labels($map) if $labels;
301             my $svg = get_post_data("$text_mapper_url/render", map => $map);
302             $c->render(template => 'description',
303             svg => add_links($svg),
304             url => $url,
305             descriptions => $descriptions);
306             };
307              
308             =item get /describe/random/alpine
309              
310             Same thing for a map using the Alpine algorithm and the Schroeder random tables.
311              
312             =cut
313              
314             get '/describe/random/alpine' => sub {
315             my $c = shift;
316             my $labels = $c->param('labels');
317             my $seed = $c->param('seed');
318             my $url = $c->param('url');
319             if (not $url) {
320             $url = "$text_mapper_url/alpine/random/text";
321             $url .= "?seed=$seed" if $seed;
322             }
323             srand($seed) if $seed;
324             my $map = get_data($url);
325             my $table = load_table('schroeder', app->config('contrib'));
326             init();
327             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
328             $map = add_labels($map) if $labels;
329             my $svg = get_post_data("$text_mapper_url/render", map => $map);
330             $c->render(template => 'description',
331             svg => add_links($svg),
332             url => $url,
333             descriptions => $descriptions);
334             };
335              
336             =item get /describe/random/strom
337              
338             Same thing for a map using the Smale algorithm and the Strom random tables.
339              
340             =cut
341              
342             get '/describe/random/strom' => sub {
343             my $c = shift;
344             my $labels = $c->param('labels');
345             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
346             my $map = get_data($url);
347             my $table = load_table('strom', app->config('contrib'));
348             init();
349             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
350             $map = add_labels($map) if $labels;
351             my $svg = get_post_data("$text_mapper_url/render", map => $map);
352             $c->render(template => 'description',
353             svg => add_links($svg),
354             url => $url,
355             descriptions => $descriptions);
356             };
357              
358             =item get /describe/random/johnston
359              
360             Same thing for a map using the Apocalypse algorithm and the Johnston random tables.
361              
362             =cut
363              
364             get '/describe/random/johnston' => sub {
365             my $c = shift;
366             my $labels = $c->param('labels');
367             my $url = $c->param('url') || "$text_mapper_url/apocalypse/random/text";
368             my $map = get_data($url);
369             my $table = load_table('johnston', app->config('contrib'));
370             init();
371             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
372             $map = add_labels($map) if $labels;
373             my $svg = get_post_data("$text_mapper_url/render", map => $map);
374             $c->render(template => 'description',
375             svg => add_links($svg),
376             url => $url,
377             descriptions => $descriptions);
378             };
379              
380             =item get /describe/random/traveller
381              
382             Same thing for a map using the Traveller algorithm and the Traveller random tables.
383              
384             =cut
385              
386             get '/describe/random/traveller' => sub {
387             my $c = shift;
388             my $labels = $c->param('labels');
389             my $url = $c->param('url') || "$text_mapper_url/traveller/random/text";
390             my $map = get_data($url);
391             my $table = load_table('traveller', app->config('contrib'));
392             init();
393             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
394             $map = add_labels($map) if $labels;
395             my $svg = get_post_data("$text_mapper_url/render", map => $map);
396             $c->render(template => 'description',
397             svg => add_links($svg),
398             url => $url,
399             descriptions => $descriptions);
400             };
401              
402             =item get /nomap
403              
404             This shows you the I page for use cases without a map. Now you're using
405             Hex Describe like many of the existing random table driven text generators. This
406             is where you can test your tables. If you've changed the code for the I
407             table, for example, generate a few villages to see some examples:
408              
409             [village]
410             [village]
411             [village]
412             [village]
413             [village]
414              
415             B is your source text. This is no longer a map. B is the URL to an
416             external table, B is the text of the table if you want to paste it. See
417             C below if you want to display the result instead of allow the
418             user to edit the form.
419              
420             =cut
421              
422             get '/nomap' => sub {
423             my $c = shift;
424             my $input = $c->param('input') || '';
425             my $url = $c->param('url');
426             my $table = $c->param('table');
427             my $seed = $c->param('seed') || time;
428             srand($c->param('seed')) if $c->param('seed');
429             $c->render(template => 'nomap', input => $input, url => $url, table => $table, seed => $seed);
430             };
431              
432             any '/nomap/markdown' => sub {
433             my $c = shift;
434             my $input = $c->param('input') || '';
435             my $table = get_table($c);
436             my $seed = $c->param('seed') || time;
437             srand($c->param('seed')) if $c->param('seed');
438             my $descriptions = describe_text($input, parse_table($table));
439             $c->render(text => markdown($descriptions), format => 'txt', seed => $seed);
440             } => 'nomap_markdown';
441              
442             =item /rules
443              
444             This lists all the rules we have and allows you to pick one.
445              
446             =cut
447              
448             get '/rules' => sub {
449             my $c = shift;
450             my $input = $c->param('input') || '';
451             my $url = $c->param('url');
452             my $table = $c->param('table');
453             $c->render(template => 'rules', input => $input, url => $url, table => $table);
454             };
455              
456             any '/rules/list' => sub {
457             my $c = shift;
458             my $input = $c->param('input') || '';
459             my ($url, $table) = get_table($c);
460             # we cannot test for 'load' because a radiobutton is always selected
461             if ($c->param('url') or $c->param('table')) {
462             $c->render(template => 'ruleslist_post', input => $input,
463             url => $url, table => $table,
464             log => $c->param('log'),
465             rules => [keys %{parse_table($table)}]);
466             } else {
467             $c->render(template => 'ruleslist_get',
468             load => $c->param('load'),
469             log => $c->param('log'),
470             rules => [keys %{parse_table($table)}]);
471             }
472             } => 'ruleslist';
473              
474             sub to_id {
475 1     1 0 221 $_ = shift;
476 1 50       4 return "" unless $_;
477 1         9 s/ /_/g;
478 1         4 s/[^0-9a-z_]//gi;
479 1         4 s/^(\d)/x$1/;
480 1         4 $_;
481             }
482              
483             any '/rule' => sub {
484             my $c = shift;
485             my $rule = $c->param('rule');
486             my $n = $c->param('n') || 10;
487             my $input = "[$rule]\n" x $n;
488             my $table = get_table($c);
489             my $seed = $c->param('seed') || time;
490             srand($seed) if $seed;
491             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
492             $c->render(template => 'text', input => $input, load => $c->param('load'), seed => $seed,
493             n => $n, url => $c->param('url'), table => $c->param('table'),
494             rule => $rule, id => to_id($rule),
495             log => $c->param('log') ? $log->history : undef,
496             descriptions => $descriptions);
497             } => 'rule';
498              
499             any '/rule/markdown' => sub {
500             my $c = shift;
501             my $rule = $c->param('rule');
502             my $n = $c->param('n') || 10;
503             my $input = $c->param('input') || "[$rule]\n" x $n;
504             my $table = get_table($c);
505             srand($c->param('seed')) if $c->param('seed');
506             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
507             $c->render(text => markdown($descriptions), format => 'txt');
508             } => 'rule_markdown';
509              
510             any '/rule/show' => sub {
511             my $c = shift;
512             my $rule = $c->param('rule');
513             my $load = $c->param('load');
514             my $table = get_table($c);
515             $table =~ s!\r!!g;
516             $table =~ s!&!&!gm;
517             $table =~ s!
518             $table =~ s!>!>!gm;
519             $table =~ s!\[([^][\n]+)\]!"[$1]"!gme;
520             my $jump = 0;
521             if ($c->param('url') or $c->param('table')) {
522             $jump = 1;
523             $table =~ s!^;(.+)!";$1"!gme;
524             } else {
525             $table =~ s!^;(.+)!";
526             . "\" href=\"" . $c->url_for('rule')->query(load => $load, rule => $1)
527             . "\">$1"!gme;
528             }
529             $c->render(template => 'show',
530             id => to_id($rule),
531             rule => $rule,
532             jump => $jump,
533             load => $load,
534             table => $table);
535             } => 'rule_show';
536              
537             =item any /describe/text
538              
539             This is where the text input is rendered. B is the text, B is the
540             URL to an external table. If not provided, B is the text of the table. If
541             neither is provided, the default table is used.
542              
543             To call this from the command line:
544              
545             hex-describe get /describe/text --form input=[village] --form load=schroeder
546              
547             Pipe through C to get text instead of HTML.
548              
549             =cut
550              
551             any '/describe/text' => sub {
552             my $c = shift;
553             my $rule = $c->param('rule');
554             my $load = $c->param('load');
555             my $n = $c->param('n');
556             my $input = $c->param('input');
557             my $url = $c->param('url');
558             my $table = $c->param('table');
559             my $seed = $c->param('seed');
560             srand($seed) if $seed;
561             my $data = get_table($c); # must be scalar context
562             $c->render(template => 'text', input => $input, load => $load, seed => $seed,
563             n => $n, url => $url, table => $table,
564             rule => $rule, id => to_id($rule),
565             log => $c->param('log') ? $log->history : undef,
566             descriptions => describe_text($input, parse_table($data)));
567             };
568              
569             =item get /default/map
570              
571             This shows you the default map.
572              
573             =cut
574              
575             get '/default/map' => sub {
576             my $c = shift;
577             $c->render(text => load_map('default', app->config('contrib')), format => 'txt');
578             };
579              
580             =item get /schroeder/table
581              
582             This shows you the table by Alex Schroeder.
583              
584             =cut
585              
586             get '/schroeder/table' => sub {
587             my $c = shift;
588             $c->render(text => load_table('schroeder', app->config('contrib')), format => 'txt');
589             };
590              
591             =item get /seckler/table
592              
593             This shows you the table by Peter Seckler.
594              
595             =cut
596              
597             get '/seckler/table' => sub {
598             my $c = shift;
599             $c->render(text => load_table('seckler', app->config('contrib')), format => 'txt');
600             };
601              
602             =item get /strom/table
603              
604             This shows you the table by Matt Strom.
605              
606             =cut
607              
608             get '/strom/table' => sub {
609             my $c = shift;
610             $c->render(text => load_table('strom', app->config('contrib')), format => 'txt');
611             };
612              
613             =item get /johnston/table
614              
615             This shows you the table by Josh Johnston.
616              
617             =cut
618              
619             get '/johnston/table' => sub {
620             my $c = shift;
621             $c->render(text => load_table('johnston', app->config('contrib')), format => 'txt');
622             };
623              
624             =item get /traveller/table
625              
626             This shows you the Traveller table by Vicky Radcliffe and Alex Schroeder.
627              
628             =cut
629              
630             get '/traveller/table' => sub {
631             my $c = shift;
632             $c->render(text => load_table('traveller', app->config('contrib')), format => 'txt');
633             };
634              
635             =item get /rorschachhamster/table
636              
637             Für die deutschen Tabellen von Rorschachhamster Alex Schroeder.
638              
639             =cut
640              
641             get '/rorschachhamster/table' => sub {
642             my $c = shift;
643             $c->render(text => load_table('rorschachhamster', app->config('contrib')), format => 'txt');
644             };
645              
646             =item get /source
647              
648             This gets you the source code of Hex Describe in case the source repository is
649             no longer available.
650              
651             =cut
652              
653             get '/source' => sub {
654             my $c = shift;
655             seek(DATA,0,0);
656             local $/ = undef;
657             $c->render(text => , format => 'txt');
658             };
659              
660             =item get /authors
661              
662             This lists the contributors to Hex Describe.
663              
664             =cut
665              
666             get '/authors' => sub {
667             my $c = shift;
668             $c->render(template => 'authors');
669             };
670              
671             =item get /help
672              
673             This shows you a little tutorial. Unlike this documentation, which is for
674             programmers, the tutorial is for the users of the app.
675              
676             =cut
677              
678             get '/help' => sub {
679             my $c = shift;
680             $c->render(template => 'help');
681             };
682              
683             =back
684              
685             =head2 Code
686              
687             This chapter is used to document the code.
688              
689             =over 4
690              
691             =item get_data
692              
693             This is is the basic work horse to get data from a URL. It is used to download
694             the table from a URL, if provided. This uses a simple GET request.
695              
696             =cut
697              
698             sub get_data {
699 0     0 1 0 my $url = shift;
700 0         0 $log->debug("get_data: $url");
701 0         0 my $ua = Mojo::UserAgent->new;
702 0         0 my $res = $ua->get($url)->result;
703 0 0       0 return decode_utf8($res->body) if $res->is_success;
704 0         0 $log->error("get_data: " . $res->code . " " . $res->message);
705             }
706              
707             =item get_post_data
708              
709             This is is used to get data from a URL when we need a POST request instead of a
710             GET request. We need this for Text Mapper when rendering the map since we send
711             the entire map to Text Mapper in order to render it. A simple GET request will
712             not do.
713              
714             =cut
715              
716             sub get_post_data {
717 0     0 1 0 my $url = shift;
718 0         0 my %data = @_;
719 0         0 $log->debug("get_post_data: $url");
720 0         0 my $ua = Mojo::UserAgent->new;
721 0         0 my $tx = $ua->post($url => form => \%data);
722 0         0 my $error;
723 0 0       0 if (my $err = $tx->error) {
724 0 0       0 if ($err->{code}) {
725 0         0 $error = $err->{code} . " " . $err->{message};
726             } else {
727 0         0 $error = $err->{message};
728             }
729             } else {
730 0         0 my $res = $ua->post($url => form => \%data)->result;
731 0 0       0 return decode_utf8($res->body) if $res->is_success;
732 0         0 $error = $res->code . " " . $res->message;
733             }
734 0         0 $log->error("get_post_data: $error");
735 0         0 return "

There was an error when attempting to load the map ($error).

";
736             }
737              
738             =item get_table
739              
740             This function gets a Mojolicious Controller object and looks for C,
741             C, C and C parameters in order to determine the table data to
742             use.
743              
744             =cut
745              
746             sub get_table {
747 2     2 1 5 my $c = shift;
748 2         7 my $load = $c->param('load');
749 2         100 my $url = $c->param('url');
750 2         105 my $table = '';
751 2 50       6 $table .= get_data($url) if $url;
752 2 50       15 $table .= load_table($load, app->config('contrib')) if $load;
753             # the table in the text area comes at the end and overrides the defaults
754 2   50     9530 $table .= $c->param('table') || '';
755 2 100       206 return $url, $table if wantarray;
756 1         3 return $table;
757             }
758              
759             =item add_links
760              
761             After we get the SVG map from Text Mapper, we need to add links to the hex
762             descriptions. Text Mapper already allows us to define an URL such that I
763             get linked to that URL. This feature is of no use to us because we're not using
764             labels. Basically, we want to add links to the I. This function
765             does that: it goes through the SVG and adds appropriate anchor elements.
766              
767             =cut
768              
769             sub add_links {
770 0     0 1   my $svg = shift;
771 0           $svg =~ s/<\?xml[^>]*>\s*//g; # remove processing instruction
772 0           my $dom = Mojo::DOM->new($svg);
773             $dom->find('g#coordinates text')
774             ->each(sub {
775 0     0     my $text = $_->text;
776 0           $text =~ s/\.//; # strip dot
777 0           $_->wrap(qq{})});
  0            
778 0           return "$dom";
779             }
780              
781             =item helper example
782              
783             This Mojolicious helper is used on the help page to make all the examples
784             clickable.
785              
786             =cut
787              
788             helper example => sub {
789             my ($c, $block) = @_;
790             my $result = $block->();
791             my $url;
792             if ($result =~ /^\d\d\d\d/m) {
793             my $map = join("\n", grep(/^\d\d\d\d|^include/, split(/\n/, $result)));
794             my $table = join("\n", grep(!/^\d\d\d\d|^include/, split(/\n/, $result)));
795             $url = $c->url_for('edit')->query(map => $map,
796             load => 'none',
797             table=> html_unescape($table));
798             } else {
799             my ($key) = $result =~ /^;(.*)/m;
800             $url = $c->url_for('nomap')->query(input => "[$key]\n" x 10,
801             load => 'none',
802             table=> html_unescape($result));
803             }
804             return Mojo::ByteStream->new(qq(
$result

Try it.

));
805             };
806              
807             =back
808              
809             =head2 Finally
810              
811             Start the app at the very end. The rest is templates for the various web pages.
812              
813             =cut
814              
815             app->start;
816              
817             __DATA__