File Coverage

blib/lib/Game/TextMapper/Mapper.pm
Criterion Covered Total %
statement 262 337 77.7
branch 70 120 58.3
condition 37 71 52.1
subroutine 27 30 90.0
pod 2 18 11.1
total 398 576 69.1


line stmt bran cond sub pod time code
1             # Copyright (C) 2009-2022 Alex Schroeder
2             #
3             # This program is free software: you can redistribute it and/or modify it under
4             # the terms of the GNU Affero General Public License as published by the Free
5             # Software Foundation, either version 3 of the License, or (at your option) any
6             # later version.
7             #
8             # This program is distributed in the hope that it will be useful, but WITHOUT
9             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10             # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11             # details.
12             #
13             # You should have received a copy of the GNU Affero General Public License along
14             # with this program. If not, see .
15              
16             =encoding utf8
17              
18             =head1 NAME
19              
20             Game::TextMapper::Mapper - a text map parser and builder
21              
22             =head1 SYNOPSIS
23              
24             use Modern::Perl;
25             use Game::TextMapper::Mapper::Hex;
26             my $map = <
27             0101 forest
28             include default.txt
29             EOT
30             my $svg = Game::TextMapper::Mapper::Hex->new(dist_dir => 'share')
31             ->initialize($map)
32             ->svg();
33             print $svg;
34              
35             =head1 DESCRIPTION
36              
37             This class knows how to parse a text containing a map description into SVG
38             definitions, and regions. Once the map is built, this class knows how to
39             generate the SVG for the entire map.
40              
41             The details depend on whether the map is a hex map or a square map. You should
42             use the appropriate class instead of this one: L
43             or L.
44              
45             =cut
46              
47             package Game::TextMapper::Mapper;
48 11     11   7267 use Game::TextMapper::Log;
  11         23  
  11         417  
49 11     11   53 use Modern::Perl '2018';
  11         17  
  11         110  
50 11     11   8648 use Mojo::UserAgent;
  11         2351223  
  11         158  
51 11     11   547 use Mojo::Base -base;
  11         19  
  11         59  
52 11     11   7874 use File::Slurper qw(read_text);
  11         34324  
  11         811  
53 11     11   74 use Encode qw(encode_utf8 decode_utf8);
  11         21  
  11         600  
54 11     11   71 use Mojo::Util qw(url_escape);
  11         22  
  11         429  
55 11     11   5248 use File::ShareDir 'dist_dir';
  11         268478  
  11         819  
56 11     11   104 use Scalar::Util 'weaken';
  11         20  
  11         78567  
57              
58             =head1 ATTRIBUTES
59              
60             =head2 dist_dir
61              
62             You need to pass this during instantiation so that the mapper knows where to
63             find files it needs to include.
64              
65             =cut
66              
67             has 'local_files';
68             has 'dist_dir';
69             has 'map';
70             has 'regions' => sub { [] };
71             has 'attributes' => sub { {} };
72             has 'defs' => sub { [] };
73             has 'path' => sub { {} };
74             has 'lines' => sub { [] };
75             has 'things' => sub { [] };
76             has 'path_attributes' => sub { {} };
77             has 'text_attributes' => '';
78             has 'glow_attributes' => '';
79             has 'label_attributes' => '';
80             has 'messages' => sub { [] };
81             has 'seen' => sub { {} };
82             has 'license' => '';
83             has 'other' => sub { [] };
84             has 'url' => '';
85             has 'offset' => sub { [] };
86              
87             my $log = Game::TextMapper::Log->get;
88              
89             sub example {
90 1     1 0 4 return <<"EOT";
91             0101 mountain "mountain"
92             0102 swamp "swamp"
93             0103 hill "hill"
94             0104 forest "forest"
95             0201 empty pyramid "pyramid"
96             0202 tundra "tundra"
97             0203 coast "coast"
98             0204 empty house "house"
99             0301 woodland "woodland"
100             0302 wetland "wetland"
101             0303 plain "plain"
102             0304 sea "sea"
103             0401 hill tower "tower"
104             0402 sand house "house"
105             0403 jungle "jungle"
106             0501 mountain cave "cave"
107             0502 sand "sand"
108             0503 hill castle "castle"
109             0205-0103-0202-0303-0402 road
110             0101-0203 river
111             0401-0303-0403 border
112             include default.txt
113             license Public Domain
114             EOT
115             }
116              
117             =head1 METHODS
118              
119             =head2 initialize($map)
120              
121             Call this to load a map into the mapper.
122              
123             =cut
124              
125             sub initialize {
126 11     11 1 145 my ($self, $map) = @_;
127 11         56 $map =~ s/-/-/g; # -- are invalid in source comments...
128 11         88 $self->map($map);
129 11         2847 $self->process(split(/\r?\n/, $map));
130             }
131              
132             sub process {
133 19     19 0 2607 my $self = shift;
134 19         35 my $line_id = 0;
135 19         2802 foreach (@_) {
136 4003 100 66     854613 if (/^(-?\d\d)(-?\d\d)(\d\d)?\s+(.*)/ or /^(-?\d\d+)\.(-?\d\d+)(?:\.(\d\d+))?\s+(.*)/) {
    100 66        
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
137 2631   50     7044 my $region = $self->make_region(x => $1, y => $2, z => $3||'00', map => $self);
138 2631         14342 weaken($region->{map});
139 2631         3170 my $rest = $4;
140 2631         5300 while (my ($tag, $label, $size) = $rest =~ /\b([a-z]+)=["“]([^"”]+)["”]\s*(\d+)?/) {
141 76 100       154 if ($tag eq 'name') {
142 38         110 $region->label($label);
143 38 50       233 $region->size($size) if $size;
144             } else {
145             # delay the calling of $self->other_info because the URL or the $self->glow_attributes might not be set
146 38     38   49 push(@{$self->other()}, sub () { $self->other_info($region, $label, $size, "translate(0,45)", 'opacity="0.2"') });
  38         116  
  38         95  
147             }
148 76         2052 $rest =~ s/\b([a-z]+)=["“]([^"”]+)["”]\s*(\d+)?//;
149             }
150 2631         4800 while (my ($label, $size, $transform) = $rest =~ /["“]([^"”]+)["”]\s*(\d+)?((?:\s*[a-z]+\([^\)]+\))*)/) {
151 6 50 33     28 if ($transform or $region->label) {
152             # delay the calling of $self->other_text because the URL or the $self->glow_attributes might not be set
153 0     0   0 push(@{$self->other()}, sub () { $self->other_text($region, $label, $size, $transform) });
  0         0  
  0         0  
154             } else {
155 6         27 $region->label($label);
156 6         41 $region->size($size);
157             }
158 6         49 $rest =~ s/["“]([^"”]+)["”]\s*(\d+)?((?:\s*[a-z]+\([^\)]+\))*)//;
159             }
160 2631         4344 my @types = split(/\s+/, $rest);
161 2631         4820 $region->type(\@types);
162 2631         9401 push(@{$self->regions}, $region);
  2631         3584  
163 2631         7207 push(@{$self->things}, $region);
  2631         3482  
164             } elsif (/^(-?\d\d-?\d\d(?:\d\d)?(?:--?\d\d-?\d\d(?:\d\d)?)+)\s+(\S+)\s*(?:["“](.+)["”])?\s*(left|right)?\s*(\d+%)?/
165             or /^(-?\d\d+\.-?\d\d+(?:\.\d\d+)?(?:--?\d\d+\.-?\d\d+(?:\.\d\d+)?)+)\s+(\S+)\s*(?:["“](.+)["”])?\s*(left|right)?\s*(\d+%)?/) {
166 341         759 my $line = $self->make_line(map => $self);
167 341         1725 weaken($line->{map});
168 341         599 my $str = $1;
169 341         731 $line->type($2);
170 341         1807 $line->label($3);
171 341         1685 $line->side($4);
172 341         1667 $line->start($5);
173 341         2013 $line->id('line' . $line_id++);
174 341         1581 my @points;
175 341         1153 while ($str =~ /\G(?:(-?\d\d)(-?\d\d)(\d\d)?|(-?\d\d+)\.(-?\d\d+)\.(\d\d+)?)-?/cg) {
176 1520   33     14701 push(@points, Game::TextMapper::Point->new(x => $1||$4, y => $2||$5, z => $3||$6||'00'));
      33        
      50        
177             }
178 341         2356 $line->points(\@points);
179 341         1371 push(@{$self->lines}, $line);
  341         619  
180             } elsif (my ($name, $x, $y, $uwp, $starport, $size, $atmosphere, $hydrographic, $population, $government, $law, $tech, $bases, $rest) =
181             /(?:([^>\r\n\t]*?)\s+)?(\d\d)(\d\d)\s+(([A-EX])([\dA])([\dA-F])([\dA])([\dA-C])([\dA-F])([\dA-L])-(\d{1,2}|[\dA-HJ-NP-Z]))(?:\s+([PCTRNSG ]+)\b)?(.*)/) {
182 0         0 my $region = $self->make_region(x => $x, y => $y, z => '00', map => $self);
183 0         0 weaken($region->{map});
184 0         0 my @types;
185 0         0 $region->label($name);
186             # delay the calling of $self->other_info because the URL or the $self->glow_attributes might not be set
187 0     0   0 push(@{$self->other()}, sub () { $self->other_info($region, $uwp, undef, "translate(0,45)", 'opacity="0.2"') });
  0         0  
  0         0  
188 0         0 push(@types, "starport-$starport");
189 0         0 push(@types, "size-$size");
190 0         0 push(@types, "atmosphere-$atmosphere");
191 0         0 push(@types, "hydrosphere-$hydrographic");
192 0         0 push(@types, "population-$population");
193 0         0 push(@types, "government-$government");
194 0         0 push(@types, "law-$law");
195 0         0 push(@types, "tech-$tech");
196 0 0       0 push(@types, "consulate") if $bases =~ /C/;
197 0 0       0 push(@types, "tas") if $bases =~ /T/;
198 0 0       0 push(@types, "pirate") if $bases =~ /P/;
199 0 0       0 push(@types, "research") if $bases =~ /R/;
200 0 0       0 push(@types, "naval") if $bases =~ /N/;
201 0 0       0 push(@types, "gas") if $bases =~ /G/;
202 0 0       0 push(@types, "scout") if $bases =~ /S/;
203 0         0 my @tokens = split(' ', $rest);
204 0         0 my %map = (A => "amber", R => "red");
205 0         0 my ($travelzone) = grep /^([AR])$/, @tokens; # amber or red travel zone
206 0 0       0 push(@types, $map{$travelzone}) if $travelzone;
207 0         0 push(@types, grep(/^[A-Z][A-Za-z]$/, @tokens));
208 0         0 $region->type(\@types);
209 0         0 push(@{$self->regions}, $region);
  0         0  
210 0         0 push(@{$self->things}, $region);
  0         0  
211             } elsif (/^(\S+)\s+attributes\s+(.*)/) {
212 141         272 $self->attributes->{$1} = $2;
213             } elsif (/^(\S+)\s+lib\s+(.*)/) {
214 0         0 $self->def(qq{$2});
215             } elsif (/^(\S+)\s+xml\s+(.*)/) {
216 26         87 $self->def(qq{$2});
217             } elsif (/^(<.*>)/) {
218 448         1093 $self->def($1);
219             } elsif (/^(\S+)\s+path\s+attributes\s+(.*)/) {
220 37         132 $self->path_attributes->{$1} = $2;
221             } elsif (/^(\S+)\s+path\s+(.*)/) {
222 15         154 $self->path->{$1} = $2;
223             } elsif (/^text\s+(.*)/) {
224 8         55 $self->text_attributes($1);
225             } elsif (/^glow\s+(.*)/) {
226 7         32 $self->glow_attributes($1);
227             } elsif (/^label\s+(.*)/) {
228 8         44 $self->label_attributes($1);
229             } elsif (/^license\s+(.*)/) {
230 5         47 $self->license($1);
231             } elsif (/^other\s+(.*)/) {
232 8         13 push(@{$self->other()}, $1);
  8         17  
233             } elsif (/^url\s+(\S+)/) {
234 1         11 $self->url($1);
235             } elsif (/^include\s+(\S*)/) {
236 8 50       17 if (scalar keys %{$self->seen} > 5) {
  8 50       51  
237 0         0 push(@{$self->messages},
  0         0  
238             "Includes are limited to five to prevent loops");
239             } elsif (not $self->seen->{$1}) {
240 8         92 my $location = $1;
241 8         22 $self->seen->{$location} = 1;
242 8         42 my $path;
243 8 50 33     57 if (index($location, '/') == -1 and -f ($path = Mojo::File->new($self->dist_dir, $location))) {
    0 0        
    0          
    0          
244             # without a slash, it could be a file from dist_dir
245 8         791 $log->debug("Reading $location");
246 8         160 $self->process(split(/\n/, decode_utf8($path->slurp())));
247             } elsif ($self->local_files and -f ($path = Mojo::File->new($location))) {
248             # it could also be a local file in the same directory, but only if
249             # called from the render command (which sets local_files)
250 0         0 $log->debug("Reading $location");
251 0         0 $self->process(split(/\n/, decode_utf8($path->slurp())));
252             } elsif ($location =~ /^https?:/) {
253 0         0 $log->debug("Getting $location");
254 0         0 my $ua = Mojo::UserAgent->new;
255 0         0 my $response = $ua->get($location)->result;
256 0 0       0 if ($response->is_success) {
257 0         0 $self->process(split(/\n/, $response->text));
258             } else {
259 0         0 push(@{$self->messages}, "Getting $location: " . $response->status_line);
  0         0  
260             }
261             } elsif ($self->dist_dir =~ /^https?:/) {
262 0         0 my $url = $self->dist_dir;
263 0 0       0 $url .= '/' unless $url =~ /\/$/;
264 0         0 $url .= $location;
265 0         0 $log->debug("Getting $url");
266 0         0 my $ua = Mojo::UserAgent->new;
267 0         0 my $response = $ua->get($url)->result;
268 0 0       0 if ($response->is_success) {
269 0         0 $self->process(split(/\n/, $response->text));
270             } else {
271 0         0 push(@{$self->messages}, "Getting $url: " . $response->status_line);
  0         0  
272             }
273             } else {
274 0         0 $log->warn("No library '$location' in " . $self->dist_dir);
275 0         0 push(@{$self->messages}, "Library '$location' is must be an existing file on the server or a HTTP/HTTPS URL");
  0         0  
276             }
277             }
278             } else {
279 319 50 66     972 $log->debug("Did not parse $_") if $_ and not /^\s*#/;
280             }
281             }
282 19         517 return $self;
283             }
284              
285             sub svg_other {
286 10     10 0 22 my ($self) = @_;
287 10         23 my $data = "\n";
288 10         18 for my $other (@{$self->other()}) {
  10         44  
289 46 100       75 if (ref $other eq 'CODE') {
290 38         82 $data .= $other->();
291             } else {
292 8         11 $data .= $other;
293             }
294 46         67 $data .= "\n";
295             }
296 10         32 $self->other(undef);
297 10         117 return $data;
298             }
299              
300             # Very similar to svg_label, but given that we have a transformation, we
301             # translate the object to it's final position.
302             sub other_text {
303 0     0 0 0 my ($self, $region, $label, $size, $transform) = @_;
304 0         0 $transform = sprintf("translate(%.1f,%.1f)", $region->pixels($self->offset)) . $transform;
305 0         0 my $attributes = "transform=\"$transform\" " . $self->label_attributes;
306 0 0 0     0 if ($size and not $attributes =~ s/\bfont-size="\d+pt"/font-size="$size"/) {
307 0         0 $attributes .= " font-size=\"$size\"";
308             }
309 0   0     0 my $data = sprintf(qq{ %s},
310             $attributes, $self->glow_attributes||'', $label);
311 0         0 my $url = $self->url;
312 0 0 0     0 $url =~ s/\%s/url_escape(encode_utf8($label))/e or $url .= url_escape(encode_utf8($label)) if $url;
  0         0  
313 0         0 $data .= sprintf(qq{%s},
314             $url, $attributes, $label);
315 0         0 $data .= qq{\n};
316 0         0 return $data;
317             }
318              
319             # Very similar to other_text, but without a link and we have extra attributes
320             sub other_info {
321 38     38 0 53 my ($self, $region, $label, $size, $transform, $attributes) = @_;
322 38         52 $transform = sprintf("translate(%.1f,%.1f)", $region->pixels($self->offset)) . $transform;
323 38         42 $attributes .= " transform=\"$transform\"";
324 38 50       55 $attributes .= " " . $self->label_attributes if $self->label_attributes;
325 38 50 33     196 if ($size and not $attributes =~ s/\bfont-size="\d+pt"/font-size="$size"/) {
326 0         0 $attributes .= " font-size=\"$size\"";
327             }
328 38   50     53 my $data = sprintf(qq{ %s}, $attributes, $self->glow_attributes||'', $label);
329 38         162 $data .= sprintf(qq{%s\n}, $attributes, $label);
330 38         78 return $data;
331             }
332              
333             sub def {
334 474     474 0 1455 my ($self, $svg) = @_;
335 474         1555 $svg =~ s/>\s+
336 474         538 push(@{$self->defs}, $svg);
  474         866  
337             }
338              
339             sub merge_attributes {
340 464     464 0 1822 my %attr = ();
341 464         500 for my $attr (@_) {
342 696 100       1189 if ($attr) {
343 63         337 while ($attr =~ /(\S+)=((["']).*?\3)/g) {
344 101         394 $attr{$1} = $2;
345             }
346             }
347             }
348 464         744 return join(' ', map { $_ . '=' . $attr{$_} } sort keys %attr);
  101         278  
349             }
350              
351             sub svg_header {
352 10     10 0 47 my ($self) = @_;
353              
354 10         27 my $header = qq{
355            
356             xmlns:xlink="http://www.w3.org/1999/xlink"
357             };
358 10 50       13 return $header . "\n" unless @{$self->regions};
  10         37  
359 10         68 my $maxz = 0;
360 10         107 foreach my $region (@{$self->regions}) {
  10         42  
361 2631 50       8658 $maxz = $region->z if $region->z > $maxz;
362             }
363             # These are required to calculate the viewBox for the SVG. Min and max X are
364             # what you would expect. Min and max Y are different, however, since we want
365             # to count all the rows on all the levels, plus an extra separator between
366             # them. Thus, min y is the min y of the first level, and max y is the min y of
367             # the first level + 1 for every level beyond the first, + all the rows for
368             # each level.
369 10         81 my $min_x_overall;
370             my $max_x_overall;
371 10         0 my $min_y_overall;
372 10         0 my $max_y_overall;
373 10         30 for my $z (0 .. $maxz) {
374 10         15 my ($minx, $miny, $maxx, $maxy);
375 10   50     78 $self->offset->[$z] = $max_y_overall // 0;
376 10         21 foreach my $region (@{$self->regions}) {
  10         26  
377 2631 50       9416 next unless $region->z == $z;
378 2631 100 66     9030 $minx = $region->x unless defined $minx and $minx <= $region->x;
379 2631 100 100     9765 $maxx = $region->x unless defined $maxx and $maxx >= $region->x;
380 2631 100 66     9836 $miny = $region->y unless defined $miny and $miny <= $region->y;
381 2631 100 100     9705 $maxy = $region->y unless defined $maxy and $maxy >= $region->y;
382             }
383 10 50 33     84 $min_x_overall = $minx unless defined $min_x_overall and $minx >= $min_x_overall;
384 10 50 33     113 $max_x_overall = $maxx unless defined $min_y_overall and $maxx <= $max_x_overall;;
385 10 50       39 $min_y_overall = $miny unless defined $min_y_overall; # first row of the first level
386 10 50       26 $max_y_overall = $miny unless defined $max_y_overall; # also (!) first row of the first level
387 10 50       25 $max_y_overall += 1 if $z > 0; # plus a separator row for every extra level
388 10         41 $max_y_overall += 1 + $maxy - $miny; # plus the number of rows for every level
389             }
390 10         76 my ($vx1, $vy1, $vx2, $vy2) = $self->viewbox($min_x_overall, $min_y_overall, $max_x_overall, $max_y_overall);
391 10         25 my ($width, $height) = ($vx2 - $vx1, $vy2 - $vy1);
392 10         57 $header .= qq{ viewBox="$vx1 $vy1 $width $height">\n};
393 10         38 $header .= qq{ \n};
394 10         33 return $header;
395             }
396              
397             sub svg_defs {
398 10     10 0 22 my ($self) = @_;
399             # All the definitions are included by default.
400 10         21 my $doc = " \n";
401 10 100       16 $doc .= " " . join("\n ", @{$self->defs}, "") if @{$self->defs};
  7         51  
  10         43  
402             # collect region types from attributess and paths in case the sets don't overlap
403 10         302 my %types = ();
404 10         15 foreach my $region (@{$self->regions}) {
  10         26  
405 2631         2891 foreach my $type (@{$region->type}) {
  2631         3235  
406 4053         8524 $types{$type} = 1;
407             }
408             }
409 10         19 foreach my $line (@{$self->lines}) {
  10         34  
410 341         1256 $types{$line->type} = 1;
411             }
412             # now go through them all
413 10         157 foreach my $type (sort keys %types) {
414 232         378 my $path = $self->path->{$type};
415 232         672 my $attributes = merge_attributes($self->attributes->{$type});
416             my $path_attributes = merge_attributes($self->path_attributes->{'default'},
417 232         381 $self->path_attributes->{$type});
418 232         481 my $glow_attributes = $self->glow_attributes;
419 232 100 100     966 if ($path || $attributes) {
420 46         60 $doc .= qq{ \n};
421             # just shapes get a glow such, eg. a house (must come first)
422 46 100 100     98 if ($path && !$attributes) {
423 3         10 $doc .= qq{ \n}
424             }
425             # region with attributes get a shape (square or hex), eg. plains and grass
426 46 100       66 if ($attributes) {
427 43         104 $doc .= " " . $self->shape($attributes) . "\n";
428             }
429             # and now the attributes themselves the shape itself
430 46 100       79 if ($path) {
431 8         20 $doc .= qq{ \n}
432             }
433             # close
434 46         77 $doc .= qq{ \n};
435             } else {
436             # nothing
437             }
438             }
439 10         287 $doc .= qq{ \n};
440             }
441              
442             sub svg_backgrounds {
443 10     10 0 15 my $self = shift;
444 10         22 my $doc = qq{ \n};
445 10         14 foreach my $thing (@{$self->things}) {
  10         52  
446             # make a copy
447 2631         10704 my @types = @{$thing->type};
  2631         4154  
448             # keep attributes
449 2631         8432 $thing->type([grep { $self->attributes->{$_} } @{$thing->type}]);
  4053         12604  
  2631         3585  
450 2631         17322 $doc .= $thing->svg($self->offset);
451             # reset copy
452 2631         4601 $thing->type(\@types);
453             }
454 10         88 $doc .= qq{ \n};
455 10         316 return $doc;
456             }
457              
458             sub svg_things {
459 10     10 0 19 my $self = shift;
460 10         19 my $doc = qq{ \n};
461 10         15 foreach my $thing (@{$self->things}) {
  10         38  
462             # drop attributes
463 2631         2868 $thing->type([grep { not $self->attributes->{$_} } @{$thing->type}]);
  4053         13180  
  2631         3682  
464 2631         16213 $doc .= $thing->svg($self->offset);
465             }
466 10         19 $doc .= qq{ \n};
467 10         297 return $doc;
468             }
469              
470             sub svg_coordinates {
471 10     10 0 21 my $self = shift;
472 10         21 my $doc = qq{ \n};
473 10         15 foreach my $region (@{$self->regions}) {
  10         41  
474 2631         3983 $doc .= $region->svg_coordinates($self->offset);
475             }
476 10         36 $doc .= qq{ \n};
477 10         569 return $doc;
478             }
479              
480             sub svg_lines {
481 10     10 0 17 my $self = shift;
482 10         19 my $doc = qq{ \n};
483 10         17 foreach my $line (@{$self->lines}) {
  10         42  
484 341         970 $doc .= $line->svg($self->offset);
485             }
486 10         34 $doc .= qq{ \n};
487 10         135 return $doc;
488             }
489              
490             sub svg_regions {
491 10     10 0 27 my $self = shift;
492 10         20 my $doc = qq{ \n};
493 10   100     40 my $attributes = $self->attributes->{default} || qq{fill="none"};
494 10         89 foreach my $region (@{$self->regions}) {
  10         57  
495 2631         5569 $doc .= $region->svg_region($attributes, $self->offset);
496             }
497 10         856 $doc .= qq{ \n};
498             }
499              
500             sub svg_line_labels {
501 10     10 0 19 my $self = shift;
502 10         23 my $doc = qq{ \n};
503 10         28 foreach my $line (@{$self->lines}) {
  10         46  
504 341         1429 $doc .= $line->svg_label($self->offset);
505             }
506 10         82 $doc .= qq{ \n};
507 10         49 return $doc;
508             }
509              
510             sub svg_labels {
511 10     10 0 16 my $self = shift;
512 10         19 my $doc = qq{ \n};
513 10         19 foreach my $region (@{$self->regions}) {
  10         37  
514 2631         10205 $doc .= $region->svg_label($self->url, $self->offset);
515             }
516 10         59 $doc .= qq{ \n};
517 10         36 return $doc;
518             }
519              
520             =head2 svg()
521              
522             This method generates the SVG once the map is initialized.
523              
524             =cut
525              
526             sub svg {
527 10     10 1 29 my ($self) = @_;
528              
529 10         49 my $doc = $self->svg_header();
530 10         44 $doc .= $self->svg_defs();
531 10         63 $doc .= $self->svg_backgrounds(); # opaque backgrounds
532 10         69 $doc .= $self->svg_lines();
533 10         74 $doc .= $self->svg_things(); # icons, lines
534 10         55 $doc .= $self->svg_coordinates();
535 10         62 $doc .= $self->svg_regions();
536 10         64 $doc .= $self->svg_line_labels();
537 10         62 $doc .= $self->svg_labels();
538 10   100     50 $doc .= $self->license() ||'';
539 10         115 $doc .= $self->svg_other();
540              
541             # error messages
542 10         20 my $y = 10;
543 10         20 foreach my $msg (@{$self->messages}) {
  10         40  
544 0         0 $doc .= " $msg\n";
545 0         0 $y += 10;
546             }
547              
548             # source code (comments may not include -- for SGML compatibility!)
549             # https://stackoverflow.com/questions/10842131/xml-comments-and
550 10         40 my $source = $self->map();
551 10         108 $source =~ s/--/--/g;
552 10         88 $doc .= "\n";
553 10         18 $doc .= qq{\n};
554              
555 10         1325 return $doc;
556             }
557              
558             =head1 SEE ALSO
559              
560             L is for hex maps.
561              
562             L is for square maps.
563              
564             =cut
565              
566             1;