File Coverage

blib/lib/Game/HexDescribe.pm
Criterion Covered Total %
statement 45 74 60.8
branch 5 16 31.2
condition 2 5 40.0
subroutine 12 16 75.0
pod 4 5 80.0
total 68 116 58.6


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.04;
42              
43 6     6   1923948 use Modern::Perl;
  6         15  
  6         61  
44 6     6   8560 use Mojolicious::Lite;
  6         2742723  
  6         44  
45 6     6   221253 use Mojo::UserAgent;
  6         16  
  6         52  
46 6     6   240 use Mojo::Util qw(html_unescape);
  6         15  
  6         333  
47 6     6   38 use Mojo::ByteStream;
  6         14  
  6         287  
48 6         972 use Game::HexDescribe::Utils qw(init describe_text parse_table load_table
49 6     6   5210 describe_map parse_map load_map markdown);
  6         29  
50 6     6   54 use Game::HexDescribe::Log;
  6         10  
  6         211  
51 6     6   39 use Encode qw(decode_utf8);
  6         12  
  6         359  
52 6     6   4126 use File::ShareDir qw(dist_dir);
  6         221253  
  6         487  
53 6     6   60 use Cwd;
  6         14  
  6         47408  
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 => undef,
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             $Game::HexDescribe::Util::text_mapper_url = $text_mapper_url;
115             $log->debug("Text Mapper URL: $text_mapper_url");
116              
117             $Game::HexDescribe::Util::face_generator_url = app->config('face_generator_url') || 'https://campaignwiki.org/face';
118             $log->debug("Face Generator URL: $Game::HexDescribe::Util::face_generator_url");
119              
120             =head2 Entry Points
121              
122             As this is a web app, the URLs you can call are basically the API it exposes.
123             Each URL can accept either I or I requests, or I.
124              
125             The config file key C can be used to host the entire application on a
126             specific path. Example C file:
127              
128             {
129             loglevel => 'debug',
130             logfile => undef,
131             contrib => 'share',
132             text_mapper_url => 'http://localhost:3010',
133             face_generator_url => 'http://localhost:3020',
134             under => 'hex-describe',
135             };
136              
137             Command line to start it:
138              
139             morbo --mode development --listen http://*:3000 script/hex-describe
140              
141             The result is that Hex Describe is now available via
142             C.
143              
144             =cut
145              
146             my $under = app->config('under');
147             under $under if $under;
148              
149             =over 4
150              
151             =item get /
152              
153             The default entry point is where you I your map and table. B is the
154             map, B is the URL to an external table, B is the text of the table
155             if you want to paste it. See C below if you want to display the
156             result instead of allow the user to edit the form.
157              
158             =cut
159              
160             get '/' => sub {
161             my $c = shift;
162             my $map = $c->param('map') || load_map('default', app->config('contrib') || dist_dir('Game-HexDescribe'));
163             my $url = $c->param('url');
164             my $table = $c->param('table');
165             $c->render(template => 'edit', map => $map, url => $url, table => $table);
166             } => 'edit';
167              
168             =item get /load/random/smale
169              
170             This shows you the I page again, with a new random map generated by Text
171             Mapper using the Smale algorithm.
172              
173             =cut
174              
175             get '/load/random/smale' => sub {
176             my $c = shift;
177             my $url = "$text_mapper_url/smale/random/text";
178             my $map = get_data($url);
179             $c->render(template => 'edit', map => $map, url=>'', table => '');
180             };
181              
182             =item get /load/random/apocalypse
183              
184             This shows you the I page again, with a new random map generated by Text
185             Mapper using the Apocalypse algorithm.
186              
187             =cut
188              
189             get '/load/random/apocalypse' => sub {
190             my $c = shift;
191             my $url = "$text_mapper_url/apocalypse/random/text";
192             my $map = get_data($url);
193             $c->render(template => 'edit', map => $map, url=>'', table => '');
194             };
195              
196             =item get /load/random/traveller
197              
198             This shows you the I page again, with a new random map generated by Text
199             Mapper using the Traveller algorithm.
200              
201             =cut
202              
203             get '/load/random/traveller' => sub {
204             my $c = shift;
205             my $url = "$text_mapper_url/traveller/random/text";
206             my $map = get_data($url);
207             $c->render(template => 'edit', map => $map, url=>'', table => '');
208             };
209              
210             =item get /load/random/alpine
211              
212             This shows you the I page again, with a new random map generated by Text
213             Mapper using the Alpine algorithm.
214              
215             =cut
216              
217             get '/load/random/alpine' => sub {
218             my $c = shift;
219             my $url = "$text_mapper_url/alpine/random/text";
220             my $map = get_data($url);
221             $c->render(template => 'edit', map => $map, url=>'', table => '');
222             };
223              
224             =item get /stats/random/alpine
225              
226             This uses a random map and the Alpine algorithm, and describes the map, and then
227             it presents you with some stats.
228              
229             =cut
230              
231             get '/stats/random/alpine' => sub {
232             my $c = shift;
233             my $url = "$text_mapper_url/alpine/random/text";
234             my $map_data = parse_map(get_data($url));
235             my %reverse_map;
236             for my $coords (keys %$map_data) {
237             for my $type (@{$map_data->{$coords}}) {
238             $reverse_map{$type} ||= 0;
239             $reverse_map{$type}++;
240             }
241             }
242             my $table = load_table('schroeder', app->config('contrib') || dist_dir('Game-HexDescribe'));
243             my $descriptions = describe_map($map_data, parse_table($table), 0);
244             my %reverse_creatures;
245             for my $coords (keys %$descriptions) {
246             while ($descriptions->{$coords} =~ m!(.+?)!g) {
247             $reverse_creatures{$1} ||= 0;
248             $reverse_creatures{$1}++;
249             }
250             }
251             $c->render(template => 'stats',
252             map_stats => \%reverse_map,
253             creature_stats => \%reverse_creatures);
254             };
255              
256             =item any /describe
257              
258             This is where the actual map is described.
259              
260             B is the map, B is the URL to an external table. B is the text
261             of the table. B determines the table to load. Current valid values are
262             I, I, I, I, and I. B
263             returns Markdown and no map. B determines whether images are kept in the
264             HTML output. B determines whether the map is kept in the HTML output.
265              
266             If we want to call this from the command line, we will need to request a map
267             from Text Mapper, too.
268              
269             text-mapper get /alpine.txt > map.txt
270             hex-describe get /describe --form map=@map.txt --form load=schroeder
271              
272             Pipe through C to get text instead of HTML.
273              
274             =cut
275              
276             any '/describe' => sub {
277             my $c = shift;
278             my $map = $c->param('map');
279             my $labels = $c->param('labels');
280             my $markdown = $c->param('markdown');
281             my $faces = $c->param('faces');
282             my $show = $c->param('show');
283             my $table = get_table($c);
284             init();
285             my $descriptions = describe_map(parse_map($map), parse_table($table), $faces);
286             if ($markdown) {
287             my $texts = [];
288             my $top = delete $descriptions->{TOP};
289             my $end = delete $descriptions->{END};
290             push(@$texts, $top) if $top;
291             for my $hex (sort keys %$descriptions) {
292             push(@$texts, "**$hex**: $descriptions->{$hex}");
293             }
294             push(@$texts, $end) if $end;
295             $c->render(text => markdown($texts), format => 'txt');
296             } elsif ($show) {
297             $map = add_labels($map) if $labels;
298             my $svg = get_post_data($text_mapper_url . '/render', map => $map);
299             $c->render(template => 'description',
300             svg => add_links($svg),
301             descriptions => $descriptions);
302             } else {
303             $map = add_labels($map) if $labels;
304             $c->render(template => 'description',
305             svg => '',
306             descriptions => $descriptions);
307             }
308             };
309              
310             =item get /describe/random/smale
311              
312             This variant is for when you want to just keep reloading and getting different
313             maps with different descriptions. Note that you may pass a C parameter,
314             which determines the map retrieved by Text Mapper. This allows you to refer to
315             an existing, random map, if you use the seed parameter in that URL. If you don't
316             provide a URL, a random map using the Smale algorithm will get used. The
317             description will be generated using the Seckler tables.
318              
319             =cut
320              
321             get '/describe/random/smale' => sub {
322             my $c = shift;
323             my $labels = $c->param('labels');
324             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
325             my $map = get_data($url);
326             my $table = load_table('seckler', app->config('contrib') || dist_dir('Game-HexDescribe'));
327             init();
328             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
329             $map = add_labels($map) if $labels;
330             my $svg = get_post_data("$text_mapper_url/render", map => $map);
331             $c->render(template => 'description',
332             svg => add_links($svg),
333             url => $url,
334             descriptions => $descriptions);
335             };
336              
337             =item get /describe/random/alpine
338              
339             Same thing for a map using the Alpine algorithm and the Schroeder random tables.
340              
341             =cut
342              
343             get '/describe/random/alpine' => sub {
344             my $c = shift;
345             my $labels = $c->param('labels');
346             my $seed = $c->param('seed');
347             my $url = $c->param('url');
348             if (not $url) {
349             $url = "$text_mapper_url/alpine/random/text";
350             $url .= "?seed=$seed" if $seed;
351             }
352             srand($seed) if $seed;
353             my $map = get_data($url);
354             my $table = load_table('schroeder', app->config('contrib') || dist_dir('Game-HexDescribe'));
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/strom
366              
367             Same thing for a map using the Smale algorithm and the Strom random tables.
368              
369             =cut
370              
371             get '/describe/random/strom' => sub {
372             my $c = shift;
373             my $labels = $c->param('labels');
374             my $url = $c->param('url') || "$text_mapper_url/smale/random/text";
375             my $map = get_data($url);
376             my $table = load_table('strom', app->config('contrib') || dist_dir('Game-HexDescribe'));
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/johnston
388              
389             Same thing for a map using the Apocalypse algorithm and the Johnston random tables.
390              
391             =cut
392              
393             get '/describe/random/johnston' => sub {
394             my $c = shift;
395             my $labels = $c->param('labels');
396             my $url = $c->param('url') || "$text_mapper_url/apocalypse/random/text";
397             my $map = get_data($url);
398             my $table = load_table('johnston', app->config('contrib') || dist_dir('Game-HexDescribe'));
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 /describe/random/traveller
410              
411             Same thing for a map using the Traveller algorithm and the Traveller random tables.
412              
413             =cut
414              
415             get '/describe/random/traveller' => sub {
416             my $c = shift;
417             my $labels = $c->param('labels');
418             my $url = $c->param('url') || "$text_mapper_url/traveller/random/text";
419             my $map = get_data($url);
420             my $table = load_table('traveller', app->config('contrib') || dist_dir('Game-HexDescribe'));
421             init();
422             my $descriptions = describe_map(parse_map($map), parse_table($table), 1); # with faces
423             $map = add_labels($map) if $labels;
424             my $svg = get_post_data("$text_mapper_url/render", map => $map);
425             $c->render(template => 'description',
426             svg => add_links($svg),
427             url => $url,
428             descriptions => $descriptions);
429             };
430              
431             =item get /nomap
432              
433             This shows you the I page for use cases without a map. Now you're using
434             Hex Describe like many of the existing random table driven text generators. This
435             is where you can test your tables. If you've changed the code for the I
436             table, for example, generate a few villages to see some examples:
437              
438             [village]
439             [village]
440             [village]
441             [village]
442             [village]
443              
444             B is your source text. This is no longer a map. B is the URL to an
445             external table, B is the text of the table if you want to paste it. See
446             C below if you want to display the result instead of allow the
447             user to edit the form.
448              
449             =cut
450              
451             get '/nomap' => sub {
452             my $c = shift;
453             my $input = $c->param('input') || '';
454             my $url = $c->param('url');
455             my $table = $c->param('table');
456             my $seed = $c->param('seed') || time;
457             srand($c->param('seed')) if $c->param('seed');
458             $c->render(template => 'nomap', input => $input, url => $url, table => $table, seed => $seed);
459             };
460              
461             any '/nomap/markdown' => sub {
462             my $c = shift;
463             my $input = $c->param('input') || '';
464             my $table = get_table($c);
465             my $seed = $c->param('seed') || time;
466             srand($c->param('seed')) if $c->param('seed');
467             my $descriptions = describe_text($input, parse_table($table));
468             $c->render(text => markdown($descriptions), format => 'txt', seed => $seed);
469             } => 'nomap_markdown';
470              
471             =item /rules
472              
473             This lists all the rules we have and allows you to pick one.
474              
475             =cut
476              
477             get '/rules' => sub {
478             my $c = shift;
479             my $input = $c->param('input') || '';
480             my $url = $c->param('url');
481             my $table = $c->param('table');
482             $c->render(template => 'rules', input => $input, url => $url, table => $table);
483             };
484              
485             any '/rules/list' => sub {
486             my $c = shift;
487             my $input = $c->param('input') || '';
488             my ($url, $table) = get_table($c);
489             # we cannot test for 'load' because a radiobutton is always selected
490             if ($c->param('url') or $c->param('table')) {
491             $c->render(template => 'ruleslist_post', input => $input,
492             url => $url, table => $table,
493             log => $c->param('log'),
494             rules => [keys %{parse_table($table)}]);
495             } else {
496             $c->render(template => 'ruleslist_get',
497             load => $c->param('load'),
498             log => $c->param('log'),
499             rules => [keys %{parse_table($table)}]);
500             }
501             } => 'ruleslist';
502              
503             sub to_id {
504 1     1 0 238 $_ = shift;
505 1 50       4 return "" unless $_;
506 1         4 s/ /_/g;
507 1         3 s/[^0-9a-z_]//gi;
508 1         3 s/^(\d)/x$1/;
509 1         4 $_;
510             }
511              
512             any '/rule' => sub {
513             my $c = shift;
514             my $rule = $c->param('rule');
515             my $n = $c->param('n') || 10;
516             my $input = "[$rule]\n" x $n;
517             my $table = get_table($c);
518             my $seed = $c->param('seed') || time;
519             srand($seed) if $seed;
520             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
521             $c->render(template => 'text', input => $input, load => $c->param('load'), seed => $seed,
522             n => $n, url => $c->param('url'), table => $c->param('table'),
523             rule => $rule, id => to_id($rule),
524             log => $c->param('log') ? $log->history : undef,
525             descriptions => $descriptions);
526             } => 'rule';
527              
528             any '/rule/markdown' => sub {
529             my $c = shift;
530             my $rule = $c->param('rule');
531             my $n = $c->param('n') || 10;
532             my $input = $c->param('input') || "[$rule]\n" x $n;
533             my $table = get_table($c);
534             srand($c->param('seed')) if $c->param('seed');
535             my $descriptions = describe_text($input, parse_table($table), 1); # with redirects
536             $c->render(text => markdown($descriptions), format => 'txt');
537             } => 'rule_markdown';
538              
539             any '/rule/show' => sub {
540             my $c = shift;
541             my $rule = $c->param('rule');
542             my $load = $c->param('load');
543             my $table = get_table($c);
544             $table =~ s!\r!!g;
545             $table =~ s!&!&!gm;
546             $table =~ s!
547             $table =~ s!>!>!gm;
548             $table =~ s!\[([^][\n]+)\]!"[$1]"!gme;
549             my $jump = 0;
550             if ($c->param('url') or $c->param('table')) {
551             $jump = 1;
552             $table =~ s!^;(.+)!";$1"!gme;
553             } else {
554             $table =~ s!^;(.+)!";
555             . "\" href=\"" . $c->url_for('rule')->query(load => $load, rule => $1)
556             . "\">$1"!gme;
557             }
558             $c->render(template => 'show',
559             id => to_id($rule),
560             rule => $rule,
561             jump => $jump,
562             load => $load,
563             table => $table);
564             } => 'rule_show';
565              
566             =item any /describe/text
567              
568             This is where the text input is rendered. B is the text, B is the
569             URL to an external table. If not provided, B is the text of the table. If
570             neither is provided, the default table is used.
571              
572             To call this from the command line:
573              
574             hex-describe get /describe/text --form input=[village] --form load=schroeder
575              
576             Pipe through C to get text instead of HTML.
577              
578             =cut
579              
580             any '/describe/text' => sub {
581             my $c = shift;
582             my $rule = $c->param('rule');
583             my $load = $c->param('load');
584             my $n = $c->param('n');
585             my $input = $c->param('input');
586             my $url = $c->param('url');
587             my $table = $c->param('table');
588             my $seed = $c->param('seed');
589             srand($seed) if $seed;
590             my $data = get_table($c); # must be scalar context
591             $c->render(template => 'text', input => $input, load => $load, seed => $seed,
592             n => $n, url => $url, table => $table,
593             rule => $rule, id => to_id($rule),
594             log => $c->param('log') ? $log->history : undef,
595             descriptions => describe_text($input, parse_table($data)));
596             };
597              
598             =item get /default/map
599              
600             This shows you the default map.
601              
602             =cut
603              
604             get '/default/map' => sub {
605             my $c = shift;
606             $c->render(text => load_map('default', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
607             };
608              
609             =item get /schroeder/table
610              
611             This shows you the table by Alex Schroeder.
612              
613             =cut
614              
615             get '/schroeder/table' => sub {
616             my $c = shift;
617             $c->render(text => load_table('schroeder', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
618             };
619              
620             =item get /seckler/table
621              
622             This shows you the table by Peter Seckler.
623              
624             =cut
625              
626             get '/seckler/table' => sub {
627             my $c = shift;
628             $c->render(text => load_table('seckler', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
629             };
630              
631             =item get /strom/table
632              
633             This shows you the table by Matt Strom.
634              
635             =cut
636              
637             get '/strom/table' => sub {
638             my $c = shift;
639             $c->render(text => load_table('strom', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
640             };
641              
642             =item get /johnston/table
643              
644             This shows you the table by Josh Johnston.
645              
646             =cut
647              
648             get '/johnston/table' => sub {
649             my $c = shift;
650             $c->render(text => load_table('johnston', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
651             };
652              
653             =item get /traveller/table
654              
655             This shows you the Traveller table by Vicky Radcliffe and Alex Schroeder.
656              
657             =cut
658              
659             get '/traveller/table' => sub {
660             my $c = shift;
661             $c->render(text => load_table('traveller', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
662             };
663              
664             =item get /rorschachhamster/table
665              
666             Für die deutschen Tabellen von Rorschachhamster Alex Schroeder.
667              
668             =cut
669              
670             get '/rorschachhamster/table' => sub {
671             my $c = shift;
672             $c->render(text => load_table('rorschachhamster', app->config('contrib') || dist_dir('Game-HexDescribe')), format => 'txt');
673             };
674              
675             =item get /authors
676              
677             This lists the contributors to Hex Describe.
678              
679             =cut
680              
681             get '/authors' => sub {
682             my $c = shift;
683             $c->render(template => 'authors');
684             };
685              
686             =item get /help
687              
688             This shows you a little tutorial. Unlike this documentation, which is for
689             programmers, the tutorial is for the users of the app.
690              
691             =cut
692              
693             get '/help' => sub {
694             my $c = shift;
695             $c->render(template => 'help');
696             };
697              
698             =back
699              
700             =head2 Code
701              
702             This chapter is used to document the code.
703              
704             =over 4
705              
706             =item get_data
707              
708             This is is the basic work horse to get data from a URL. It is used to download
709             the table from a URL, if provided. This uses a simple GET request.
710              
711             =cut
712              
713             sub get_data {
714 0     0 1 0 my $url = shift;
715 0         0 $log->debug("get_data: $url");
716 0         0 my $ua = Mojo::UserAgent->new;
717 0         0 my $res = $ua->get($url)->result;
718 0 0       0 return decode_utf8($res->body) if $res->is_success;
719 0         0 $log->error("get_data: " . $res->code . " " . $res->message);
720             }
721              
722             =item get_post_data
723              
724             This is is used to get data from a URL when we need a POST request instead of a
725             GET request. We need this for Text Mapper when rendering the map since we send
726             the entire map to Text Mapper in order to render it. A simple GET request will
727             not do.
728              
729             =cut
730              
731             sub get_post_data {
732 0     0 1 0 my $url = shift;
733 0         0 my %data = @_;
734 0         0 $log->debug("get_post_data: $url");
735 0         0 my $ua = Mojo::UserAgent->new;
736 0         0 my $tx = $ua->post($url => form => \%data);
737 0         0 my $error;
738 0 0       0 if (my $err = $tx->error) {
739 0 0       0 if ($err->{code}) {
740 0         0 $error = $err->{code} . " " . $err->{message};
741             } else {
742 0         0 $error = $err->{message};
743             }
744             } else {
745 0         0 my $res = $ua->post($url => form => \%data)->result;
746 0 0       0 return decode_utf8($res->body) if $res->is_success;
747 0         0 $error = $res->code . " " . $res->message;
748             }
749 0         0 $log->error("get_post_data: $error");
750 0         0 return "

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

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

Try it.

));
820             };
821              
822             =back
823              
824             =head2 Finally
825              
826             Start the app at the very end. The rest is templates for the various web pages.
827              
828             =cut
829              
830             app->start || 1;
831              
832             __DATA__