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.03;
42              
43 2     2   8659 use Modern::Perl;
  2         5  
  2         22  
44 2     2   1421 use Mojolicious::Lite;
  2         142652  
  2         18  
45 2     2   58797 use Mojo::UserAgent;
  2         4  
  2         15  
46 2     2   62 use Mojo::Util qw(html_unescape);
  2         6  
  2         96  
47 2     2   11 use Mojo::ByteStream;
  2         4  
  2         85  
48 2         218 use Game::HexDescribe::Utils qw(init describe_text parse_table load_table
49 2     2   1394 describe_map parse_map load_map markdown);
  2         6  
50 2     2   16 use Game::HexDescribe::Log;
  2         4  
  2         47  
51 2     2   14 use Encode qw(decode_utf8);
  2         4  
  2         84  
52 2     2   1062 use File::ShareDir qw(dist_dir);
  2         50339  
  2         121  
53 2     2   17 use Cwd;
  2         5  
  2         10856  
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. B determines the table to load. Current valid values are
240             I, I, I, I, and I. B
241             returns Markdown and no map. B determines whether images are kept in the
242             HTML output. B determines whether the map is kept in the HTML output.
243              
244             If we want to call this from the command line, we will need to request a map
245             from Text Mapper, too.
246              
247             text-mapper get /alpine.txt > map.txt
248             hex-describe get /describe --form map=@map.txt --form load=schroeder
249              
250             Pipe through C to get text instead of HTML.
251              
252             =cut
253              
254             any '/describe' => sub {
255             my $c = shift;
256             my $map = $c->param('map');
257             my $labels = $c->param('labels');
258             my $markdown = $c->param('markdown');
259             my $faces = $c->param('faces');
260             my $show = $c->param('show');
261             my $table = get_table($c);
262             init();
263             my $descriptions = describe_map(parse_map($map), parse_table($table), $faces);
264             if ($markdown) {
265             my $texts = [];
266             my $top = delete $descriptions->{TOP};
267             my $end = delete $descriptions->{END};
268             push(@$texts, $top) if $top;
269             for my $hex (sort keys %$descriptions) {
270             push(@$texts, "**$hex**: $descriptions->{$hex}");
271             }
272             push(@$texts, $end) if $end;
273             $c->render(text => markdown($texts), format => 'txt');
274             } elsif ($show) {
275             $map = add_labels($map) if $labels;
276             my $svg = get_post_data($text_mapper_url . '/render', map => $map);
277             $c->render(template => 'description',
278             svg => add_links($svg),
279             descriptions => $descriptions);
280             } else {
281             $map = add_labels($map) if $labels;
282             $c->render(template => 'description',
283             svg => '',
284             descriptions => $descriptions);
285             }
286             };
287              
288             =item get /describe/random/smale
289              
290             This variant is for when you want to just keep reloading and getting different
291             maps with different descriptions. Note that you may pass a C parameter,
292             which determines the map retrieved by Text Mapper. This allows you to refer to
293             an existing, random map, if you use the seed parameter in that URL. If you don't
294             provide a URL, a random map using the Smale algorithm will get used. The
295             description will be generated using the Seckler tables.
296              
297             =cut
298              
299             get '/describe/random/smale' => sub {
300             my $c = shift;
301             my $labels = $c->param('labels');
302             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
303             my $map = get_data($url);
304             my $table = load_table('seckler', app->config('contrib'));
305             init();
306             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
307             $map = add_labels($map) if $labels;
308             my $svg = get_post_data("$text_mapper_url/render", map => $map);
309             $c->render(template => 'description',
310             svg => add_links($svg),
311             url => $url,
312             descriptions => $descriptions);
313             };
314              
315             =item get /describe/random/alpine
316              
317             Same thing for a map using the Alpine algorithm and the Schroeder random tables.
318              
319             =cut
320              
321             get '/describe/random/alpine' => sub {
322             my $c = shift;
323             my $labels = $c->param('labels');
324             my $seed = $c->param('seed');
325             my $url = $c->param('url');
326             if (not $url) {
327             $url = "$text_mapper_url/alpine/random/text";
328             $url .= "?seed=$seed" if $seed;
329             }
330             srand($seed) if $seed;
331             my $map = get_data($url);
332             my $table = load_table('schroeder', app->config('contrib'));
333             init();
334             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
335             $map = add_labels($map) if $labels;
336             my $svg = get_post_data("$text_mapper_url/render", map => $map);
337             $c->render(template => 'description',
338             svg => add_links($svg),
339             url => $url,
340             descriptions => $descriptions);
341             };
342              
343             =item get /describe/random/strom
344              
345             Same thing for a map using the Smale algorithm and the Strom random tables.
346              
347             =cut
348              
349             get '/describe/random/strom' => sub {
350             my $c = shift;
351             my $labels = $c->param('labels');
352             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
353             my $map = get_data($url);
354             my $table = load_table('strom', app->config('contrib'));
355             init();
356             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
357             $map = add_labels($map) if $labels;
358             my $svg = get_post_data("$text_mapper_url/render", map => $map);
359             $c->render(template => 'description',
360             svg => add_links($svg),
361             url => $url,
362             descriptions => $descriptions);
363             };
364              
365             =item get /describe/random/johnston
366              
367             Same thing for a map using the Apocalypse algorithm and the Johnston random tables.
368              
369             =cut
370              
371             get '/describe/random/johnston' => sub {
372             my $c = shift;
373             my $labels = $c->param('labels');
374             my $url = $c->param('url') || "$text_mapper_url/apocalypse/random/text";
375             my $map = get_data($url);
376             my $table = load_table('johnston', app->config('contrib'));
377             init();
378             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
379             $map = add_labels($map) if $labels;
380             my $svg = get_post_data("$text_mapper_url/render", map => $map);
381             $c->render(template => 'description',
382             svg => add_links($svg),
383             url => $url,
384             descriptions => $descriptions);
385             };
386              
387             =item get /describe/random/traveller
388              
389             Same thing for a map using the Traveller algorithm and the Traveller random tables.
390              
391             =cut
392              
393             get '/describe/random/traveller' => sub {
394             my $c = shift;
395             my $labels = $c->param('labels');
396             my $url = $c->param('url') || "$text_mapper_url/traveller/random/text";
397             my $map = get_data($url);
398             my $table = load_table('traveller', app->config('contrib'));
399             init();
400             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
401             $map = add_labels($map) if $labels;
402             my $svg = get_post_data("$text_mapper_url/render", map => $map);
403             $c->render(template => 'description',
404             svg => add_links($svg),
405             url => $url,
406             descriptions => $descriptions);
407             };
408              
409             =item get /nomap
410              
411             This shows you the I page for use cases without a map. Now you're using
412             Hex Describe like many of the existing random table driven text generators. This
413             is where you can test your tables. If you've changed the code for the I
414             table, for example, generate a few villages to see some examples:
415              
416             [village]
417             [village]
418             [village]
419             [village]
420             [village]
421              
422             B is your source text. This is no longer a map. B is the URL to an
423             external table, B is the text of the table if you want to paste it. See
424             C below if you want to display the result instead of allow the
425             user to edit the form.
426              
427             =cut
428              
429             get '/nomap' => sub {
430             my $c = shift;
431             my $input = $c->param('input') || '';
432             my $url = $c->param('url');
433             my $table = $c->param('table');
434             my $seed = $c->param('seed') || time;
435             srand($c->param('seed')) if $c->param('seed');
436             $c->render(template => 'nomap', input => $input, url => $url, table => $table, seed => $seed);
437             };
438              
439             any '/nomap/markdown' => sub {
440             my $c = shift;
441             my $input = $c->param('input') || '';
442             my $table = get_table($c);
443             my $seed = $c->param('seed') || time;
444             srand($c->param('seed')) if $c->param('seed');
445             my $descriptions = describe_text($input, parse_table($table));
446             $c->render(text => markdown($descriptions), format => 'txt', seed => $seed);
447             } => 'nomap_markdown';
448              
449             =item /rules
450              
451             This lists all the rules we have and allows you to pick one.
452              
453             =cut
454              
455             get '/rules' => sub {
456             my $c = shift;
457             my $input = $c->param('input') || '';
458             my $url = $c->param('url');
459             my $table = $c->param('table');
460             $c->render(template => 'rules', input => $input, url => $url, table => $table);
461             };
462              
463             any '/rules/list' => sub {
464             my $c = shift;
465             my $input = $c->param('input') || '';
466             my ($url, $table) = get_table($c);
467             # we cannot test for 'load' because a radiobutton is always selected
468             if ($c->param('url') or $c->param('table')) {
469             $c->render(template => 'ruleslist_post', input => $input,
470             url => $url, table => $table,
471             log => $c->param('log'),
472             rules => [keys %{parse_table($table)}]);
473             } else {
474             $c->render(template => 'ruleslist_get',
475             load => $c->param('load'),
476             log => $c->param('log'),
477             rules => [keys %{parse_table($table)}]);
478             }
479             } => 'ruleslist';
480              
481             sub to_id {
482 1     1 0 281 $_ = shift;
483 1 50       5 return "" unless $_;
484 1         7 s/ /_/g;
485 1         4 s/[^0-9a-z_]//gi;
486 1         4 s/^(\d)/x$1/;
487 1         5 $_;
488             }
489              
490             any '/rule' => sub {
491             my $c = shift;
492             my $rule = $c->param('rule');
493             my $n = $c->param('n') || 10;
494             my $input = "[$rule]\n" x $n;
495             my $table = get_table($c);
496             my $seed = $c->param('seed') || time;
497             srand($seed) if $seed;
498             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
499             $c->render(template => 'text', input => $input, load => $c->param('load'), seed => $seed,
500             n => $n, url => $c->param('url'), table => $c->param('table'),
501             rule => $rule, id => to_id($rule),
502             log => $c->param('log') ? $log->history : undef,
503             descriptions => $descriptions);
504             } => 'rule';
505              
506             any '/rule/markdown' => sub {
507             my $c = shift;
508             my $rule = $c->param('rule');
509             my $n = $c->param('n') || 10;
510             my $input = $c->param('input') || "[$rule]\n" x $n;
511             my $table = get_table($c);
512             srand($c->param('seed')) if $c->param('seed');
513             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
514             $c->render(text => markdown($descriptions), format => 'txt');
515             } => 'rule_markdown';
516              
517             any '/rule/show' => sub {
518             my $c = shift;
519             my $rule = $c->param('rule');
520             my $load = $c->param('load');
521             my $table = get_table($c);
522             $table =~ s!\r!!g;
523             $table =~ s!&!&!gm;
524             $table =~ s!
525             $table =~ s!>!>!gm;
526             $table =~ s!\[([^][\n]+)\]!"[$1]"!gme;
527             my $jump = 0;
528             if ($c->param('url') or $c->param('table')) {
529             $jump = 1;
530             $table =~ s!^;(.+)!";$1"!gme;
531             } else {
532             $table =~ s!^;(.+)!";
533             . "\" href=\"" . $c->url_for('rule')->query(load => $load, rule => $1)
534             . "\">$1"!gme;
535             }
536             $c->render(template => 'show',
537             id => to_id($rule),
538             rule => $rule,
539             jump => $jump,
540             load => $load,
541             table => $table);
542             } => 'rule_show';
543              
544             =item any /describe/text
545              
546             This is where the text input is rendered. B is the text, B is the
547             URL to an external table. If not provided, B is the text of the table. If
548             neither is provided, the default table is used.
549              
550             To call this from the command line:
551              
552             hex-describe get /describe/text --form input=[village] --form load=schroeder
553              
554             Pipe through C to get text instead of HTML.
555              
556             =cut
557              
558             any '/describe/text' => sub {
559             my $c = shift;
560             my $rule = $c->param('rule');
561             my $load = $c->param('load');
562             my $n = $c->param('n');
563             my $input = $c->param('input');
564             my $url = $c->param('url');
565             my $table = $c->param('table');
566             my $seed = $c->param('seed');
567             srand($seed) if $seed;
568             my $data = get_table($c); # must be scalar context
569             $c->render(template => 'text', input => $input, load => $load, seed => $seed,
570             n => $n, url => $url, table => $table,
571             rule => $rule, id => to_id($rule),
572             log => $c->param('log') ? $log->history : undef,
573             descriptions => describe_text($input, parse_table($data)));
574             };
575              
576             =item get /default/map
577              
578             This shows you the default map.
579              
580             =cut
581              
582             get '/default/map' => sub {
583             my $c = shift;
584             $c->render(text => load_map('default', app->config('contrib')), format => 'txt');
585             };
586              
587             =item get /schroeder/table
588              
589             This shows you the table by Alex Schroeder.
590              
591             =cut
592              
593             get '/schroeder/table' => sub {
594             my $c = shift;
595             $c->render(text => load_table('schroeder', app->config('contrib')), format => 'txt');
596             };
597              
598             =item get /seckler/table
599              
600             This shows you the table by Peter Seckler.
601              
602             =cut
603              
604             get '/seckler/table' => sub {
605             my $c = shift;
606             $c->render(text => load_table('seckler', app->config('contrib')), format => 'txt');
607             };
608              
609             =item get /strom/table
610              
611             This shows you the table by Matt Strom.
612              
613             =cut
614              
615             get '/strom/table' => sub {
616             my $c = shift;
617             $c->render(text => load_table('strom', app->config('contrib')), format => 'txt');
618             };
619              
620             =item get /johnston/table
621              
622             This shows you the table by Josh Johnston.
623              
624             =cut
625              
626             get '/johnston/table' => sub {
627             my $c = shift;
628             $c->render(text => load_table('johnston', app->config('contrib')), format => 'txt');
629             };
630              
631             =item get /traveller/table
632              
633             This shows you the Traveller table by Vicky Radcliffe and Alex Schroeder.
634              
635             =cut
636              
637             get '/traveller/table' => sub {
638             my $c = shift;
639             $c->render(text => load_table('traveller', app->config('contrib')), format => 'txt');
640             };
641              
642             =item get /rorschachhamster/table
643              
644             Für die deutschen Tabellen von Rorschachhamster Alex Schroeder.
645              
646             =cut
647              
648             get '/rorschachhamster/table' => sub {
649             my $c = shift;
650             $c->render(text => load_table('rorschachhamster', app->config('contrib')), format => 'txt');
651             };
652              
653             =item get /source
654              
655             This gets you the source code of Hex Describe in case the source repository is
656             no longer available.
657              
658             =cut
659              
660             get '/source' => sub {
661             my $c = shift;
662             seek(DATA,0,0);
663             local $/ = undef;
664             $c->render(text => , format => 'txt');
665             };
666              
667             =item get /authors
668              
669             This lists the contributors to Hex Describe.
670              
671             =cut
672              
673             get '/authors' => sub {
674             my $c = shift;
675             $c->render(template => 'authors');
676             };
677              
678             =item get /help
679              
680             This shows you a little tutorial. Unlike this documentation, which is for
681             programmers, the tutorial is for the users of the app.
682              
683             =cut
684              
685             get '/help' => sub {
686             my $c = shift;
687             $c->render(template => 'help');
688             };
689              
690             =back
691              
692             =head2 Code
693              
694             This chapter is used to document the code.
695              
696             =over 4
697              
698             =item get_data
699              
700             This is is the basic work horse to get data from a URL. It is used to download
701             the table from a URL, if provided. This uses a simple GET request.
702              
703             =cut
704              
705             sub get_data {
706 0     0 1 0 my $url = shift;
707 0         0 $log->debug("get_data: $url");
708 0         0 my $ua = Mojo::UserAgent->new;
709 0         0 my $res = $ua->get($url)->result;
710 0 0       0 return decode_utf8($res->body) if $res->is_success;
711 0         0 $log->error("get_data: " . $res->code . " " . $res->message);
712             }
713              
714             =item get_post_data
715              
716             This is is used to get data from a URL when we need a POST request instead of a
717             GET request. We need this for Text Mapper when rendering the map since we send
718             the entire map to Text Mapper in order to render it. A simple GET request will
719             not do.
720              
721             =cut
722              
723             sub get_post_data {
724 0     0 1 0 my $url = shift;
725 0         0 my %data = @_;
726 0         0 $log->debug("get_post_data: $url");
727 0         0 my $ua = Mojo::UserAgent->new;
728 0         0 my $tx = $ua->post($url => form => \%data);
729 0         0 my $error;
730 0 0       0 if (my $err = $tx->error) {
731 0 0       0 if ($err->{code}) {
732 0         0 $error = $err->{code} . " " . $err->{message};
733             } else {
734 0         0 $error = $err->{message};
735             }
736             } else {
737 0         0 my $res = $ua->post($url => form => \%data)->result;
738 0 0       0 return decode_utf8($res->body) if $res->is_success;
739 0         0 $error = $res->code . " " . $res->message;
740             }
741 0         0 $log->error("get_post_data: $error");
742 0         0 return "

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

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

Try it.

));
812             };
813              
814             =back
815              
816             =head2 Finally
817              
818             Start the app at the very end. The rest is templates for the various web pages.
819              
820             =cut
821              
822             app->start || 1;
823              
824             __DATA__