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.04;
42              
43 6     6   1436958 use Modern::Perl;
  6         15  
  6         57  
44 6     6   7636 use Mojolicious::Lite;
  6         2063583  
  6         31  
45 6     6   165079 use Mojo::UserAgent;
  6         15  
  6         41  
46 6     6   229 use Mojo::Util qw(html_unescape);
  6         29  
  6         268  
47 6     6   27 use Mojo::ByteStream;
  6         10  
  6         248  
48 6         684 use Game::HexDescribe::Utils qw(init describe_text parse_table load_table
49 6     6   4033 describe_map parse_map load_map markdown);
  6         20  
50 6     6   35 use Game::HexDescribe::Log;
  6         9  
  6         129  
51 6     6   23 use Encode qw(decode_utf8);
  6         8  
  6         213  
52 6     6   3122 use File::ShareDir qw(dist_dir);
  6         140724  
  6         431  
53 6     6   50 use Cwd;
  6         26  
  6         32437  
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             $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'));
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'));
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'));
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'));
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'));
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'));
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'));
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 222 $_ = shift;
505 1 50       3 return "" unless $_;
506 1         4 s/ /_/g;
507 1         3 s/[^0-9a-z_]//gi;
508 1         4 s/^(\d)/x$1/;
509 1         3 $_;
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')), 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')), 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')), 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')), 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')), 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')), 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')), format => 'txt');
673             };
674              
675             =item get /source
676              
677             This gets you the source code of Hex Describe in case the source repository is
678             no longer available.
679              
680             =cut
681              
682             get '/source' => sub {
683             my $c = shift;
684             seek(DATA,0,0);
685             local $/ = undef;
686             $c->render(text => , format => 'txt');
687             };
688              
689             =item get /authors
690              
691             This lists the contributors to Hex Describe.
692              
693             =cut
694              
695             get '/authors' => sub {
696             my $c = shift;
697             $c->render(template => 'authors');
698             };
699              
700             =item get /help
701              
702             This shows you a little tutorial. Unlike this documentation, which is for
703             programmers, the tutorial is for the users of the app.
704              
705             =cut
706              
707             get '/help' => sub {
708             my $c = shift;
709             $c->render(template => 'help');
710             };
711              
712             =back
713              
714             =head2 Code
715              
716             This chapter is used to document the code.
717              
718             =over 4
719              
720             =item get_data
721              
722             This is is the basic work horse to get data from a URL. It is used to download
723             the table from a URL, if provided. This uses a simple GET request.
724              
725             =cut
726              
727             sub get_data {
728 0     0 1 0 my $url = shift;
729 0         0 $log->debug("get_data: $url");
730 0         0 my $ua = Mojo::UserAgent->new;
731 0         0 my $res = $ua->get($url)->result;
732 0 0       0 return decode_utf8($res->body) if $res->is_success;
733 0         0 $log->error("get_data: " . $res->code . " " . $res->message);
734             }
735              
736             =item get_post_data
737              
738             This is is used to get data from a URL when we need a POST request instead of a
739             GET request. We need this for Text Mapper when rendering the map since we send
740             the entire map to Text Mapper in order to render it. A simple GET request will
741             not do.
742              
743             =cut
744              
745             sub get_post_data {
746 0     0 1 0 my $url = shift;
747 0         0 my %data = @_;
748 0         0 $log->debug("get_post_data: $url");
749 0         0 my $ua = Mojo::UserAgent->new;
750 0         0 my $tx = $ua->post($url => form => \%data);
751 0         0 my $error;
752 0 0       0 if (my $err = $tx->error) {
753 0 0       0 if ($err->{code}) {
754 0         0 $error = $err->{code} . " " . $err->{message};
755             } else {
756 0         0 $error = $err->{message};
757             }
758             } else {
759 0         0 my $res = $ua->post($url => form => \%data)->result;
760 0 0       0 return decode_utf8($res->body) if $res->is_success;
761 0         0 $error = $res->code . " " . $res->message;
762             }
763 0         0 $log->error("get_post_data: $error");
764 0         0 return "

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

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

Try it.

));
834             };
835              
836             =back
837              
838             =head2 Finally
839              
840             Start the app at the very end. The rest is templates for the various web pages.
841              
842             =cut
843              
844             app->start || 1;
845              
846             __DATA__