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   7050 use Game::TextMapper::Log;
  11         21  
  11         382  
49 11     11   44 use Modern::Perl '2018';
  11         42  
  11         70  
50 11     11   8398 use Mojo::UserAgent;
  11         3021273  
  11         172  
51 11     11   654 use Mojo::Base -base;
  11         49  
  11         75  
52 11     11   8518 use File::Slurper qw(read_text);
  11         41252  
  11         896  
53 11     11   83 use Encode qw(encode_utf8 decode_utf8);
  11         23  
  11         629  
54 11     11   65 use Mojo::Util qw(url_escape);
  11         20  
  11         600  
55 11     11   6445 use File::ShareDir 'dist_dir';
  11         355119  
  11         857  
56 11     11   90 use Scalar::Util 'weaken';
  11         26  
  11         107040  
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 3 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 115 my ($self, $map) = @_;
127 11         45 $map =~ s/-/-/g; # -- are invalid in source comments...
128 11         65 $self->map($map);
129 11         2634 $self->process(split(/\r?\n/, $map));
130             }
131              
132             sub process {
133 19     19 0 3098 my $self = shift;
134 19         41 my $line_id = 0;
135 19         50 foreach (@_) {
136 3993 100 66     1131853 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 2641   50     10107 my $region = $self->make_region(x => $1, y => $2, z => $3||'00', map => $self);
138 2641         20955 weaken($region->{map});
139 2641         4420 my $rest = $4;
140 2641         6947 while (my ($tag, $label, $size) = $rest =~ /\b([a-z]+)=["“]([^"”]+)["”]\s*(\d+)?/) {
141 78 100       115 if ($tag eq 'name') {
142 39         73 $region->label($label);
143 39 50       170 $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 39     39   38 push(@{$self->other()}, sub () { $self->other_info($region, $label, $size, "translate(0,45)", 'opacity="0.2"') });
  39         59  
  39         71  
147             }
148 78         1333 $rest =~ s/\b([a-z]+)=["“]([^"”]+)["”]\s*(\d+)?//;
149             }
150 2641         6183 while (my ($label, $size, $transform) = $rest =~ /["“]([^"”]+)["”]\s*(\d+)?((?:\s*[a-z]+\([^\)]+\))*)/) {
151 6 50 33     23 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         37 $region->label($label);
156 6         40 $region->size($size);
157             }
158 6         54 $rest =~ s/["“]([^"”]+)["”]\s*(\d+)?((?:\s*[a-z]+\([^\)]+\))*)//;
159             }
160 2641         6025 my @types = split(/\s+/, $rest);
161 2641         6454 $region->type(\@types);
162 2641         12325 push(@{$self->regions}, $region);
  2641         4870  
163 2641         9257 push(@{$self->things}, $region);
  2641         4819  
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 321         851 my $line = $self->make_line(map => $self);
167 321         1944 weaken($line->{map});
168 321         642 my $str = $1;
169 321         721 $line->type($2);
170 321         1917 $line->label($3);
171 321         1850 $line->side($4);
172 321         1893 $line->start($5);
173 321         1998 $line->id('line' . $line_id++);
174 321         1483 my @points;
175 321         1445 while ($str =~ /\G(?:(-?\d\d)(-?\d\d)(\d\d)?|(-?\d\d+)\.(-?\d\d+)\.(\d\d+)?)-?/cg) {
176 2038   33     21908 push(@points, Game::TextMapper::Point->new(x => $1||$4, y => $2||$5, z => $3||$6||'00'));
      33        
      50        
177             }
178 321         2734 $line->points(\@points);
179 321         1492 push(@{$self->lines}, $line);
  321         642  
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         371 $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         88 $self->def(qq{$2});
217             } elsif (/^(<.*>)/) {
218 448         2279 $self->def($1);
219             } elsif (/^(\S+)\s+path\s+attributes\s+(.*)/) {
220 37         267 $self->path_attributes->{$1} = $2;
221             } elsif (/^(\S+)\s+path\s+(.*)/) {
222 15         215 $self->path->{$1} = $2;
223             } elsif (/^text\s+(.*)/) {
224 8         48 $self->text_attributes($1);
225             } elsif (/^glow\s+(.*)/) {
226 7         51 $self->glow_attributes($1);
227             } elsif (/^label\s+(.*)/) {
228 8         44 $self->label_attributes($1);
229             } elsif (/^license\s+(.*)/) {
230 5         87 $self->license($1);
231             } elsif (/^other\s+(.*)/) {
232 8         13 push(@{$self->other()}, $1);
  8         23  
233             } elsif (/^url\s+(\S+)/) {
234 1         8 $self->url($1);
235             } elsif (/^include\s+(\S*)/) {
236 8 50       17 if (scalar keys %{$self->seen} > 5) {
  8 50       47  
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         65 my $location = $1;
241 8         23 $self->seen->{$location} = 1;
242 8         44 my $path;
243 8 50 33     80 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         2506 $log->debug("Reading $location");
246 8         146 $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->code . " " . $response->message);
  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->code . " " . $response->message);
  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     1490 $log->debug("Did not parse $_") if $_ and not /^\s*#/;
280             }
281             }
282 19         621 return $self;
283             }
284              
285             sub svg_other {
286 10     10 0 24 my ($self) = @_;
287 10         38 my $data = "\n";
288 10         21 for my $other (@{$self->other()}) {
  10         54  
289 47 100       90 if (ref $other eq 'CODE') {
290 39         59 $data .= $other->();
291             } else {
292 8         19 $data .= $other;
293             }
294 47         62 $data .= "\n";
295             }
296 10         40 $self->other(undef);
297 10         132 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 39     39 0 54 my ($self, $region, $label, $size, $transform, $attributes) = @_;
322 39         49 $transform = sprintf("translate(%.1f,%.1f)", $region->pixels($self->offset)) . $transform;
323 39         56 $attributes .= " transform=\"$transform\"";
324 39 50       52 $attributes .= " " . $self->label_attributes if $self->label_attributes;
325 39 50 33     190 if ($size and not $attributes =~ s/\bfont-size="\d+pt"/font-size="$size"/) {
326 0         0 $attributes .= " font-size=\"$size\"";
327             }
328 39   50     52 my $data = sprintf(qq{ %s}, $attributes, $self->glow_attributes||'', $label);
329 39         161 $data .= sprintf(qq{%s\n}, $attributes, $label);
330 39         72 return $data;
331             }
332              
333             sub def {
334 474     474 0 2230 my ($self, $svg) = @_;
335 474         2268 $svg =~ s/>\s+
336 474         798 push(@{$self->defs}, $svg);
  474         1445  
337             }
338              
339             sub merge_attributes {
340 454     454 0 2011 my %attr = ();
341 454         559 for my $attr (@_) {
342 681 100       1188 if ($attr) {
343 65         531 while ($attr =~ /(\S+)=((["']).*?\3)/g) {
344 101         478 $attr{$1} = $2;
345             }
346             }
347             }
348 454         883 return join(' ', map { $_ . '=' . $attr{$_} } sort keys %attr);
  101         352  
349             }
350              
351             sub svg_header {
352 10     10 0 30 my ($self) = @_;
353              
354 10         65 my $header = qq{
355            
356             xmlns:xlink="http://www.w3.org/1999/xlink"
357             };
358 10 50       16 return $header . "\n" unless @{$self->regions};
  10         43  
359 10         77 my $maxz = 0;
360 10         107 foreach my $region (@{$self->regions}) {
  10         34  
361 2641 50       10480 $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         85 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         34 for my $z (0 .. $maxz) {
374 10         17 my ($minx, $miny, $maxx, $maxy);
375 10   50     90 $self->offset->[$z] = $max_y_overall // 0;
376 10         26 foreach my $region (@{$self->regions}) {
  10         33  
377 2641 50       10575 next unless $region->z == $z;
378 2641 100 66     10229 $minx = $region->x unless defined $minx and $minx <= $region->x;
379 2641 100 100     11125 $maxx = $region->x unless defined $maxx and $maxx >= $region->x;
380 2641 100 66     11268 $miny = $region->y unless defined $miny and $miny <= $region->y;
381 2641 100 100     11042 $maxy = $region->y unless defined $maxy and $maxy >= $region->y;
382             }
383 10 50 33     66 $min_x_overall = $minx unless defined $min_x_overall and $minx >= $min_x_overall;
384 10 50 33     40 $max_x_overall = $maxx unless defined $min_y_overall and $maxx <= $max_x_overall;;
385 10 50       71 $min_y_overall = $miny unless defined $min_y_overall; # first row of the first level
386 10 50       34 $max_y_overall = $miny unless defined $max_y_overall; # also (!) first row of the first level
387 10 50       115 $max_y_overall += 1 if $z > 0; # plus a separator row for every extra level
388 10         38 $max_y_overall += 1 + $maxy - $miny; # plus the number of rows for every level
389             }
390 10         84 my ($vx1, $vy1, $vx2, $vy2) = $self->viewbox($min_x_overall, $min_y_overall, $max_x_overall, $max_y_overall);
391 10         33 my ($width, $height) = ($vx2 - $vx1, $vy2 - $vy1);
392 10         55 $header .= qq{ viewBox="$vx1 $vy1 $width $height">\n};
393 10         39 $header .= qq{ \n};
394 10         48 return $header;
395             }
396              
397             sub svg_defs {
398 10     10 0 21 my ($self) = @_;
399             # All the definitions are included by default.
400 10         22 my $doc = " \n";
401 10 100       20 $doc .= " " . join("\n ", @{$self->defs}, "") if @{$self->defs};
  7         56  
  10         41  
402             # collect region types from attributess and paths in case the sets don't overlap
403 10         262 my %types = ();
404 10         14 foreach my $region (@{$self->regions}) {
  10         37  
405 2641         3097 foreach my $type (@{$region->type}) {
  2641         3613  
406 3995         9864 $types{$type} = 1;
407             }
408             }
409 10         25 foreach my $line (@{$self->lines}) {
  10         76  
410 321         1508 $types{$line->type} = 1;
411             }
412             # now go through them all
413 10         166 foreach my $type (sort keys %types) {
414 227         402 my $path = $self->path->{$type};
415 227         773 my $attributes = merge_attributes($self->attributes->{$type});
416             my $path_attributes = merge_attributes($self->path_attributes->{'default'},
417 227         571 $self->path_attributes->{$type});
418 227         478 my $glow_attributes = $self->glow_attributes;
419 227 100 100     1033 if ($path || $attributes) {
420 49         82 $doc .= qq{ \n};
421             # just shapes get a glow such, eg. a house (must come first)
422 49 100 100     113 if ($path && !$attributes) {
423 3         8 $doc .= qq{ \n}
424             }
425             # region with attributes get a shape (square or hex), eg. plains and grass
426 49 100       89 if ($attributes) {
427 46         144 $doc .= " " . $self->shape($attributes) . "\n";
428             }
429             # and now the attributes themselves the shape itself
430 49 100       99 if ($path) {
431 8         20 $doc .= qq{ \n}
432             }
433             # close
434 49         97 $doc .= qq{ \n};
435             } else {
436             # nothing
437             }
438             }
439 10         341 $doc .= qq{ \n};
440             }
441              
442             sub svg_backgrounds {
443 10     10 0 36 my $self = shift;
444 10         27 my $doc = qq{ \n};
445 10         22 foreach my $thing (@{$self->things}) {
  10         45  
446             # make a copy
447 2641         12234 my @types = @{$thing->type};
  2641         4105  
448             # keep attributes
449 2641         9730 $thing->type([grep { $self->attributes->{$_} } @{$thing->type}]);
  3995         14398  
  2641         4178  
450 2641         19150 $doc .= $thing->svg($self->offset);
451             # reset copy
452 2641         5393 $thing->type(\@types);
453             }
454 10         61 $doc .= qq{ \n};
455 10         388 return $doc;
456             }
457              
458             sub svg_things {
459 10     10 0 25 my $self = shift;
460 10         22 my $doc = qq{ \n};
461 10         24 foreach my $thing (@{$self->things}) {
  10         60  
462             # drop attributes
463 2641         3267 $thing->type([grep { not $self->attributes->{$_} } @{$thing->type}]);
  3995         14752  
  2641         4087  
464 2641         19189 $doc .= $thing->svg($self->offset);
465             }
466 10         21 $doc .= qq{ \n};
467 10         605 return $doc;
468             }
469              
470             sub svg_coordinates {
471 10     10 0 23 my $self = shift;
472 10         21 my $doc = qq{ \n};
473 10         20 foreach my $region (@{$self->regions}) {
  10         39  
474 2641         5165 $doc .= $region->svg_coordinates($self->offset);
475             }
476 10         23 $doc .= qq{ \n};
477 10         1139 return $doc;
478             }
479              
480             sub svg_lines {
481 10     10 0 21 my $self = shift;
482 10         23 my $doc = qq{ \n};
483 10         14 foreach my $line (@{$self->lines}) {
  10         40  
484 321         1149 $doc .= $line->svg($self->offset);
485             }
486 10         45 $doc .= qq{ \n};
487 10         304 return $doc;
488             }
489              
490             sub svg_regions {
491 10     10 0 38 my $self = shift;
492 10         44 my $doc = qq{ \n};
493 10   100     70 my $attributes = $self->attributes->{default} || qq{fill="none"};
494 10         103 foreach my $region (@{$self->regions}) {
  10         66  
495 2641         5853 $doc .= $region->svg_region($attributes, $self->offset);
496             }
497 10         816 $doc .= qq{ \n};
498             }
499              
500             sub svg_line_labels {
501 10     10 0 27 my $self = shift;
502 10         35 my $doc = qq{ \n};
503 10         26 foreach my $line (@{$self->lines}) {
  10         54  
504 321         1484 $doc .= $line->svg_label($self->offset);
505             }
506 10         78 $doc .= qq{ \n};
507 10         56 return $doc;
508             }
509              
510             sub svg_labels {
511 10     10 0 18 my $self = shift;
512 10         25 my $doc = qq{ \n};
513 10         23 foreach my $region (@{$self->regions}) {
  10         35  
514 2641         11453 $doc .= $region->svg_label($self->url, $self->offset);
515             }
516 10         71 $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 30 my ($self) = @_;
528              
529 10         60 my $doc = $self->svg_header();
530 10         55 $doc .= $self->svg_defs();
531 10         136 $doc .= $self->svg_backgrounds(); # opaque backgrounds
532 10         74 $doc .= $self->svg_lines();
533 10         73 $doc .= $self->svg_things(); # icons, lines
534 10         68 $doc .= $self->svg_coordinates();
535 10         105 $doc .= $self->svg_regions();
536 10         66 $doc .= $self->svg_line_labels();
537 10         56 $doc .= $self->svg_labels();
538 10   100     117 $doc .= $self->license() ||'';
539 10         136 $doc .= $self->svg_other();
540              
541             # error messages
542 10         23 my $y = 10;
543 10         18 foreach my $msg (@{$self->messages}) {
  10         71  
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         51 my $source = $self->map();
551 10         130 $source =~ s/--/--/g;
552 10         347 $doc .= "\n";
553 10         26 $doc .= qq{\n};
554              
555 10         1423 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;