| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # Copyright (C) 2009-2021  Alex Schroeder | 
| 2 |  |  |  |  |  |  | # Copyright (C) 2020       Christian Carey | 
| 3 |  |  |  |  |  |  | # | 
| 4 |  |  |  |  |  |  | # This program is free software: you can redistribute it and/or modify it under | 
| 5 |  |  |  |  |  |  | # the terms of the GNU General Public License as published by the Free Software | 
| 6 |  |  |  |  |  |  | # Foundation, either version 3 of the License, or (at your option) any later | 
| 7 |  |  |  |  |  |  | # 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 General Public License for more details. | 
| 12 |  |  |  |  |  |  | # | 
| 13 |  |  |  |  |  |  | # You should have received a copy of the GNU General Public License along with | 
| 14 |  |  |  |  |  |  | # this program. If not, see . | 
| 15 |  |  |  |  |  |  | # | 
| 16 |  |  |  |  |  |  | # Algorithms based on Traveller ©2008 Mongoose Publishing. | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  | package Traveller::Mapper; | 
| 19 | 2 |  |  | 2 |  | 1208 | use List::Util qw(shuffle reduce); | 
|  | 2 |  |  |  |  | 5 |  | 
|  | 2 |  |  |  |  | 182 |  | 
| 20 | 2 |  |  | 2 |  | 14 | use Mojo::Base -base; | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 14 |  | 
| 21 | 2 |  |  | 2 |  | 340 | use Traveller::Util qw(nearby distance in); | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 112 |  | 
| 22 | 2 |  |  | 2 |  | 940 | use Traveller::Hex; | 
|  | 2 |  |  |  |  | 5 |  | 
|  | 2 |  |  |  |  | 18 |  | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | has 'hexes' => sub { [] }; | 
| 25 |  |  |  |  |  |  | has 'routes' => sub { [] }; | 
| 26 |  |  |  |  |  |  | has 'comm_set'; | 
| 27 |  |  |  |  |  |  | has 'trade_set'; | 
| 28 |  |  |  |  |  |  | has 'source'; | 
| 29 |  |  |  |  |  |  | has 'width'; | 
| 30 |  |  |  |  |  |  | has 'height'; | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | my $example = q!Inedgeus     0101 D7A5579-8        G  Fl Ni          A | 
| 33 |  |  |  |  |  |  | Geaan        0102 E66A999-7        G  Hi Wa          A | 
| 34 |  |  |  |  |  |  | Orgemaso     0103 C555875-5       SG  Ga Lt | 
| 35 |  |  |  |  |  |  | Veesso       0105 C5A0369-8        G  De Lo          A | 
| 36 |  |  |  |  |  |  | Ticezale     0106 B769799-7    T  SG  Ri             A | 
| 37 |  |  |  |  |  |  | Maatonte     0107 C6B3544-8   C    G  Fl Ni          A | 
| 38 |  |  |  |  |  |  | Diesra       0109 D510522-8       SG  Ni | 
| 39 |  |  |  |  |  |  | Esarra       0204 E869100-8        G  Lo             A | 
| 40 |  |  |  |  |  |  | Rience       0205 C687267-8        G  Ga Lo | 
| 41 |  |  |  |  |  |  | Rearreso     0208 C655432-5   C    G  Ga Lt Ni | 
| 42 |  |  |  |  |  |  | Laisbe       0210 E354663-3           Ag Lt Ni | 
| 43 |  |  |  |  |  |  | Biveer       0302 C646576-9   C    G  Ag Ga Ni | 
| 44 |  |  |  |  |  |  | Labeveri     0303 A796100-9   CT N G  Ga Lo          A | 
| 45 |  |  |  |  |  |  | Sotexe       0408 E544778-3        G  Ag Ga Lt       A | 
| 46 |  |  |  |  |  |  | Zamala       0409 A544658-13   T N G  Ag Ga Ht Ni | 
| 47 |  |  |  |  |  |  | Sogeeran     0502 A200443-14  CT N G  Ht Ni Va | 
| 48 |  |  |  |  |  |  | Aanbi        0503 E697102-7        G  Ga Lo          A | 
| 49 |  |  |  |  |  |  | Bemaat       0504 C643384-9   C R  G  Lo Po | 
| 50 |  |  |  |  |  |  | Diare        0505 A254430-11   TRN G  Ni             A | 
| 51 |  |  |  |  |  |  | Esgeed       0507 A8B1579-11    RN G  Fl Ni          A | 
| 52 |  |  |  |  |  |  | Leonbi       0510 B365789-9    T  SG  Ag Ri          A | 
| 53 |  |  |  |  |  |  | Reisbeon     0604 C561526-8     R  G  Ni | 
| 54 |  |  |  |  |  |  | Atcevein     0605 A231313-11  CT   G  Lo Po | 
| 55 |  |  |  |  |  |  | Usmabe       0607 A540A84-15   T   G  De Hi Ht In Po | 
| 56 |  |  |  |  |  |  | Onbebior     0608 B220530-10       G  De Ni Po       A | 
| 57 |  |  |  |  |  |  | Raraxema     0609 B421768-8    T NSG  Na Po | 
| 58 |  |  |  |  |  |  | Xeerri       0610 C210862-9        G  Na | 
| 59 |  |  |  |  |  |  | Onreon       0702 D8838A9-2       S   Lt Ri          A | 
| 60 |  |  |  |  |  |  | Ismave       0703 E272654-4           Lt Ni | 
| 61 |  |  |  |  |  |  | Lara         0704 C0008D9-5       SG  As Lt Na Va    A | 
| 62 |  |  |  |  |  |  | Lalala       0705 C140473-9     R  G  De Ni Po | 
| 63 |  |  |  |  |  |  | Maxereis     0707 A55A747-12  CT NSG  Ht Wa | 
| 64 |  |  |  |  |  |  | Requbire     0802 C9B4200-10       G  Fl Lo          A | 
| 65 |  |  |  |  |  |  | Azaxe        0804 B6746B9-8   C    G  Ag Ga Ni       A | 
| 66 |  |  |  |  |  |  | Rieddige     0805 B355578-7        G  Ag Ni          A | 
| 67 |  |  |  |  |  |  | Usorce       0806 E736110-3        G  Lo Lt          A | 
| 68 |  |  |  |  |  |  | Solacexe     0810 D342635-4  P    S   Lt Ni Po       R | 
| 69 |  |  |  |  |  |  | !; | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | sub example { | 
| 72 | 1 |  |  | 1 | 0 | 17 | return $example; | 
| 73 |  |  |  |  |  |  | } | 
| 74 |  |  |  |  |  |  |  | 
| 75 |  |  |  |  |  |  | # The empty hex is centered around 0,0 and has a side length of 1, | 
| 76 |  |  |  |  |  |  | # a maximum diameter of 2, and a minimum diameter of √3. | 
| 77 |  |  |  |  |  |  | my @hex = (  -1,          0, | 
| 78 |  |  |  |  |  |  | -0.5,  sqrt(3)/2, | 
| 79 |  |  |  |  |  |  | 0.5,  sqrt(3)/2, | 
| 80 |  |  |  |  |  |  | 1,          0, | 
| 81 |  |  |  |  |  |  | 0.5, -sqrt(3)/2, | 
| 82 |  |  |  |  |  |  | -0.5, -sqrt(3)/2); | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | sub header { | 
| 85 | 0 |  |  | 0 | 0 |  | my ($self, $width, $height) = @_; | 
| 86 |  |  |  |  |  |  | # TO DO: support an option for North American “A” paper dimensions (width 215.9 mm, length 279.4 mm) | 
| 87 | 0 |  | 0 |  |  |  | $width //= 210; | 
| 88 | 0 |  | 0 |  |  |  | $height //= 297; | 
| 89 | 0 |  |  |  |  |  | my $template = < | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | xmlns:xlink="http://www.w3.org/1999/xlink" | 
| 93 |  |  |  |  |  |  | width="${width}mm" | 
| 94 |  |  |  |  |  |  | height="${height}mm" | 
| 95 |  |  |  |  |  |  | viewBox="%s %s %s %s"> | 
| 96 |  |  |  |  |  |  | Traveller Subsector | 
| 97 |  |  |  |  |  |  |  | 
| 98 |  |  |  |  |  |  |  | 
| 190 |  |  |  |  |  |  |  | 
| 191 |  |  |  |  |  |  |  | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  |  | 
| 194 |  |  |  |  |  |  | x="%s" y="%s" width="%s" height="%s" /> | 
| 195 |  |  |  |  |  |  |  | 
| 196 |  |  |  |  |  |  | EOT | 
| 197 | 0 |  |  |  |  |  | my $scale = 100; | 
| 198 |  |  |  |  |  |  | return sprintf($template, | 
| 199 | 0 |  |  |  |  |  | map { sprintf("%.3f", $_ * $scale) } | 
|  | 0 |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | # viewport | 
| 201 |  |  |  |  |  |  | -0.5, -0.5, 3 + ($self->width - 1) * 1.5, ($self->height + 1.5) * sqrt(3), | 
| 202 |  |  |  |  |  |  | # empty hex, once for the backgrounds and once for the stroke | 
| 203 |  |  |  |  |  |  | @hex, | 
| 204 |  |  |  |  |  |  | @hex, | 
| 205 |  |  |  |  |  |  | # framing rectangle | 
| 206 |  |  |  |  |  |  | -0.5, -0.5, 3 + ($self->width - 1) * 1.5, ($self->height + 1.5) * sqrt(3)); | 
| 207 |  |  |  |  |  |  | } | 
| 208 |  |  |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | sub background { | 
| 210 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 211 | 0 |  |  |  |  |  | my $scale = 100; | 
| 212 | 0 |  |  |  |  |  | my $doc; | 
| 213 |  |  |  |  |  |  | # We want to colour cultures such that the same colours result from the same | 
| 214 |  |  |  |  |  |  | # names. The number of colours is given by the CSS. We must therefore hash all | 
| 215 |  |  |  |  |  |  | # the names to one of these colours; but index 0 is a white background, so | 
| 216 |  |  |  |  |  |  | # don't use that. | 
| 217 | 0 |  |  |  |  |  | my $colours = 28; | 
| 218 | 0 |  |  |  |  |  | my %id; | 
| 219 |  |  |  |  |  |  | my %seen; | 
| 220 | 0 |  |  |  |  |  | my %used; | 
| 221 | 0 |  |  |  |  |  | for my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 222 | 0 | 0 |  |  |  |  | if ($hex->culture) { | 
| 223 | 0 |  |  |  |  |  | my $coord = $hex->x . $hex->y; | 
| 224 | 0 | 0 |  |  |  |  | if ($seen{$hex->culture}) { | 
| 225 | 0 |  |  |  |  |  | $id{$coord} = $seen{$hex->culture}; | 
| 226 |  |  |  |  |  |  | } else { | 
| 227 | 0 |  |  |  |  |  | my $colour = 1 + unpack("%32W*", lc $hex->culture) % $colours; # checksum | 
| 228 |  |  |  |  |  |  | # reduce collisions | 
| 229 | 0 |  |  |  |  |  | for (1 .. 3) { | 
| 230 | 0 | 0 |  |  |  |  | last unless $used{$colour}; | 
| 231 | 0 |  |  |  |  |  | $colour = 1+ ($colour + 1) % $colours; | 
| 232 |  |  |  |  |  |  | } | 
| 233 | 0 |  |  |  |  |  | $seen{$hex->culture} = $id{$coord} = $colour; | 
| 234 | 0 |  |  |  |  |  | $used{$colour} = $hex->culture; | 
| 235 |  |  |  |  |  |  | } | 
| 236 |  |  |  |  |  |  | } | 
| 237 |  |  |  |  |  |  | } | 
| 238 |  |  |  |  |  |  | # warn scalar(keys %used) . " colours used\n"; | 
| 239 |  |  |  |  |  |  | $doc .= join("\n", | 
| 240 |  |  |  |  |  |  | map { | 
| 241 | 0 |  |  |  |  |  | my $n = shift; | 
|  | 0 |  |  |  |  |  |  | 
| 242 | 0 |  |  |  |  |  | my $x = int($_/$self->height+1); | 
| 243 | 0 |  |  |  |  |  | my $y = $_ % $self->height + 1; | 
| 244 | 0 |  |  |  |  |  | my $coord = sprintf('%02d%02d', $x, $y); | 
| 245 | 0 |  | 0 |  |  |  | my $class = $id{$coord} // 0; | 
| 246 | 0 |  |  |  |  |  | my $svg = sprintf(qq{    }, | 
| 247 |  |  |  |  |  |  | (1 + ($x-1) * 1.5) * $scale, | 
| 248 |  |  |  |  |  |  | ($y - $x%2/2) * sqrt(3) * $scale); | 
| 249 |  |  |  |  |  |  | } | 
| 250 |  |  |  |  |  |  | (0 .. $self->width * $self->height - 1)); | 
| 251 | 0 |  |  |  |  |  | return $doc; | 
| 252 |  |  |  |  |  |  | } | 
| 253 |  |  |  |  |  |  |  | 
| 254 |  |  |  |  |  |  | sub grid { | 
| 255 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 256 | 0 |  |  |  |  |  | my $scale = 100; | 
| 257 | 0 |  |  |  |  |  | my $doc; | 
| 258 |  |  |  |  |  |  | $doc .= join("\n", | 
| 259 |  |  |  |  |  |  | map { | 
| 260 | 0 |  |  |  |  |  | my $n = shift; | 
|  | 0 |  |  |  |  |  |  | 
| 261 | 0 |  |  |  |  |  | my $x = int($_/$self->height+1); | 
| 262 | 0 |  |  |  |  |  | my $y = $_ % $self->height + 1; | 
| 263 | 0 |  |  |  |  |  | my $svg = sprintf(qq{    \n}, | 
| 264 |  |  |  |  |  |  | (1 + ($x-1) * 1.5) * $scale, | 
| 265 |  |  |  |  |  |  | ($y - $x%2/2) * sqrt(3) * $scale); | 
| 266 | 0 |  |  |  |  |  | $svg   .= sprintf(qq{    } | 
| 267 |  |  |  |  |  |  | . qq{%02d%02d\n}, | 
| 268 |  |  |  |  |  |  | (1 + ($x-1) * 1.5) * $scale, | 
| 269 |  |  |  |  |  |  | ($y - $x%2/2) * sqrt(3) * $scale - 0.6 * $scale, | 
| 270 |  |  |  |  |  |  | $x, $y); | 
| 271 |  |  |  |  |  |  | } (0 .. $self->width * $self->height - 1)); | 
| 272 | 0 |  |  |  |  |  | return $doc; | 
| 273 |  |  |  |  |  |  | } | 
| 274 |  |  |  |  |  |  |  | 
| 275 |  |  |  |  |  |  | sub legend { | 
| 276 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 277 | 0 |  |  |  |  |  | my $scale = 100; | 
| 278 | 0 |  |  |  |  |  | my $doc; | 
| 279 | 0 |  |  |  |  |  | my $uwp = ''; | 
| 280 | 0 | 0 |  |  |  |  | if ($self->source) { | 
| 281 | 0 |  |  |  |  |  | $uwp = ' – UWP'; | 
| 282 |  |  |  |  |  |  | } | 
| 283 | 0 |  |  |  |  |  | $doc .= sprintf(qq{    ◉ gas giant} | 
| 284 |  |  |  |  |  |  | . qq{ – ■ Imperial consulate – ☼ TAS facility – ▲ scout base} | 
| 285 |  |  |  |  |  |  | . qq{ – ★ naval base – π research station – ☠ pirate base} | 
| 286 |  |  |  |  |  |  | . qq{ – ▮ communication} | 
| 287 |  |  |  |  |  |  | . qq{ – ▮ trade$uwp\n}, | 
| 288 |  |  |  |  |  |  | -10, ($self->height + 1) * sqrt(3) * $scale); | 
| 289 | 0 |  |  |  |  |  | $doc .= sprintf(qq{    coreward\n}, | 
| 290 |  |  |  |  |  |  | $self->width/2 * 1.5 * $scale, -0.13 * $scale); | 
| 291 | 0 |  |  |  |  |  | $doc .= sprintf(qq{ | 
| 292 |  |  |  |  |  |  | . qq{ class="direction">trailing\n}, | 
| 293 |  |  |  |  |  |  | ($self->width + 0.4) * 1.5 * $scale, $self->height/2 * sqrt(3) * $scale); | 
| 294 | 0 |  |  |  |  |  | $doc .= sprintf(qq{    rimward\n}, | 
| 295 |  |  |  |  |  |  | $self->width/2 * 1.5 * $scale, ($self->height + 0.7) * sqrt(3) * $scale); | 
| 296 | 0 |  |  |  |  |  | $doc .= sprintf(qq{ | 
| 297 |  |  |  |  |  |  | . qq{ class="direction">spinward\n}, | 
| 298 |  |  |  |  |  |  | -0.1 * $scale, $self->height/2 * sqrt(3) * $scale); | 
| 299 | 0 |  |  |  |  |  | return $doc; | 
| 300 |  |  |  |  |  |  | } | 
| 301 |  |  |  |  |  |  |  | 
| 302 |  |  |  |  |  |  | sub footer { | 
| 303 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 304 | 0 |  |  |  |  |  | my $doc; | 
| 305 | 0 |  |  |  |  |  | my $y = 10; | 
| 306 | 0 |  |  |  |  |  | my $debug = ''; # for developers | 
| 307 | 0 |  |  |  |  |  | for my $line (split(/\n/, $debug)) { | 
| 308 | 0 |  |  |  |  |  | $doc .= qq{} | 
| 309 |  |  |  |  |  |  | . $line . qq{\n}; | 
| 310 | 0 |  |  |  |  |  | $y += 20; | 
| 311 |  |  |  |  |  |  | } | 
| 312 | 0 |  |  |  |  |  | $doc .= qq{\n}; | 
| 313 | 0 |  |  |  |  |  | return $doc; | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  |  | 
| 316 |  |  |  |  |  |  | sub initialize { | 
| 317 | 0 |  |  | 0 | 0 |  | my ($self, $map, $wiki, $source) = @_; | 
| 318 | 0 |  |  |  |  |  | $self->source($source); | 
| 319 | 0 |  |  |  |  |  | $self->width(0); | 
| 320 | 0 |  |  |  |  |  | $self->height(0); | 
| 321 | 0 |  |  |  |  |  | my @lines = split(/\n/, $map); | 
| 322 | 0 |  |  |  |  |  | $self->initialize_map($wiki, \@lines); | 
| 323 | 0 |  |  |  |  |  | $self->initialize_routes(\@lines); | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  |  | 
| 326 |  |  |  |  |  |  | sub initialize_map { | 
| 327 | 0 |  |  | 0 | 0 |  | my ($self, $wiki, $lines) = @_; | 
| 328 | 0 |  |  |  |  |  | foreach (@$lines) { | 
| 329 |  |  |  |  |  |  | # parse Traveller UWP | 
| 330 | 0 |  |  |  |  |  | my ($name, $x, $y, | 
| 331 |  |  |  |  |  |  | $starport, $size, $atmosphere, $hydrographic, $population, | 
| 332 |  |  |  |  |  |  | $government, $law, $tech, $bases, $rest) = | 
| 333 |  |  |  |  |  |  | /([^>\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)?(.*)/; | 
| 334 |  |  |  |  |  |  | # alternative super simple name, coordinates, optional size (0-9), optional bases (PCTRNSG), optional travel zones (AR) | 
| 335 | 0 | 0 |  |  |  |  | ($name, $x, $y, $size, $bases, $rest) = | 
| 336 |  |  |  |  |  |  | /([^>\r\n\t]*?)\s+(\d\d)(\d\d)(?:\s+(\d)\b)?(?:\s+([PCTRNSG ]+)\b)?(.*)/ | 
| 337 |  |  |  |  |  |  | unless $name; | 
| 338 | 0 | 0 |  |  |  |  | next unless $name; | 
| 339 | 0 | 0 |  |  |  |  | $self->width($x) if $x > $self->width; | 
| 340 | 0 | 0 |  |  |  |  | $self->height($y) if $y > $self->height; | 
| 341 | 0 |  |  |  |  |  | my @tokens = split(' ', $rest); | 
| 342 | 0 |  |  |  |  |  | my %trade = map { $_ => 1 } grep(/^[A-Z][A-Za-z]$/, @tokens); | 
|  | 0 |  |  |  |  |  |  | 
| 343 | 0 |  |  |  |  |  | my ($culture) = grep /^\[.*\]$/, @tokens; # culture in square brackets | 
| 344 | 0 |  |  |  |  |  | my ($travelzone) = grep /^([AR])$/, @tokens;    # amber or red travel zone | 
| 345 |  |  |  |  |  |  | # avoid uninitialized values warnings in the rest of the code | 
| 346 | 0 |  | 0 |  |  |  | map { $$_ //= '' } (\$size, | 
|  | 0 |  |  |  |  |  |  | 
| 347 |  |  |  |  |  |  | \$atmosphere, | 
| 348 |  |  |  |  |  |  | \$hydrographic, | 
| 349 |  |  |  |  |  |  | \$population, | 
| 350 |  |  |  |  |  |  | \$government, | 
| 351 |  |  |  |  |  |  | \$law, | 
| 352 |  |  |  |  |  |  | \$starport, | 
| 353 |  |  |  |  |  |  | \$travelzone); | 
| 354 |  |  |  |  |  |  | # get "hex" values, but accept letters beyond F! (excepting I and O) | 
| 355 | 0 | 0 | 0 |  |  |  | map { $$_ = $$_ ge 'P' and $$_ le 'Z' ? 23 + ord($$_) - 80 | 
|  | 0 | 0 | 0 |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 356 |  |  |  |  |  |  | : $$_ ge 'J' and $$_ le 'N' ? 18 + ord($$_) - 74 | 
| 357 |  |  |  |  |  |  | : $$_ ge 'A' and $$_ le 'H' ? 10 + ord($$_) - 65 | 
| 358 |  |  |  |  |  |  | : $$_ eq '' ? 0 | 
| 359 |  |  |  |  |  |  | : $$_ } (\$size, | 
| 360 |  |  |  |  |  |  | \$atmosphere, | 
| 361 |  |  |  |  |  |  | \$hydrographic, | 
| 362 |  |  |  |  |  |  | \$population, | 
| 363 |  |  |  |  |  |  | \$government, | 
| 364 |  |  |  |  |  |  | \$law); | 
| 365 | 0 |  | 0 |  |  |  | my $hex = Traveller::Hex->new( | 
| 366 |  |  |  |  |  |  | name => $name, | 
| 367 |  |  |  |  |  |  | x => $x, | 
| 368 |  |  |  |  |  |  | y => $y, | 
| 369 |  |  |  |  |  |  | starport => $starport, | 
| 370 |  |  |  |  |  |  | population => $population, | 
| 371 |  |  |  |  |  |  | size => $size, | 
| 372 |  |  |  |  |  |  | travelzone => $travelzone, | 
| 373 |  |  |  |  |  |  | trade => \%trade, | 
| 374 |  |  |  |  |  |  | culture => $culture // ''); | 
| 375 | 0 | 0 |  |  |  |  | $hex->url("$wiki$name") if $wiki; | 
| 376 | 0 | 0 |  |  |  |  | if ($bases) { | 
| 377 | 0 |  |  |  |  |  | for my $base (split(//, $bases)) { | 
| 378 | 0 |  |  |  |  |  | $hex->base($base); | 
| 379 |  |  |  |  |  |  | } | 
| 380 |  |  |  |  |  |  | } | 
| 381 | 0 |  |  |  |  |  | $self->add($hex); | 
| 382 |  |  |  |  |  |  | } | 
| 383 |  |  |  |  |  |  | } | 
| 384 |  |  |  |  |  |  |  | 
| 385 |  |  |  |  |  |  | sub add { | 
| 386 | 0 |  |  | 0 | 0 |  | my ($self, $hex) = @_; | 
| 387 | 0 |  |  |  |  |  | push(@{$self->hexes}, $hex); | 
|  | 0 |  |  |  |  |  |  | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  |  | 
| 390 |  |  |  |  |  |  | sub initialize_routes { | 
| 391 | 0 |  |  | 0 | 0 |  | my ($self, $lines) = @_; | 
| 392 | 0 |  |  |  |  |  | foreach (@$lines) { | 
| 393 |  |  |  |  |  |  | # parse non-standard routes | 
| 394 | 0 |  |  |  |  |  | my ($from, $to, $type) = /^(\d\d\d\d)-(\d\d\d\d)\s+(C|T)\b/i; | 
| 395 | 0 | 0 |  |  |  |  | next unless $type; | 
| 396 | 0 | 0 |  |  |  |  | if (lc($type) eq 'c') { | 
| 397 | 0 |  |  |  |  |  | $self->comm_set(1); # at least one hex here has comm | 
| 398 | 0 |  |  |  |  |  | push(@{$self->at($from)->comm}, $self->at($to)); # a property of the hex | 
|  | 0 |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | } else { | 
| 400 | 0 |  |  |  |  |  | $self->trade_set(1); # at least one hex here has trade | 
| 401 | 0 |  |  |  |  |  | my $from_hex = $self->at($from); | 
| 402 | 0 |  |  |  |  |  | my $to_hex = $self->at($to); | 
| 403 |  |  |  |  |  |  |  | 
| 404 | 0 |  |  |  |  |  | push(@{$self->routes}, [$from_hex, $to_hex]); # a property of the mapper | 
|  | 0 |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | } | 
| 406 |  |  |  |  |  |  | } | 
| 407 |  |  |  |  |  |  | } | 
| 408 |  |  |  |  |  |  |  | 
| 409 |  |  |  |  |  |  | sub at { | 
| 410 | 0 |  |  | 0 | 0 |  | my ($self, $coord) = @_; | 
| 411 | 0 |  |  |  |  |  | my ($x, $y) = $coord =~ /(\d\d)(\d\d)/; | 
| 412 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 413 | 0 | 0 | 0 |  |  |  | return $hex if $hex->x == $x and $hex->y == $y; | 
| 414 |  |  |  |  |  |  | } | 
| 415 |  |  |  |  |  |  | } | 
| 416 |  |  |  |  |  |  |  | 
| 417 |  |  |  |  |  |  | sub communications { | 
| 418 |  |  |  |  |  |  | # connect all the class A starports, naval bases, and Imperial | 
| 419 |  |  |  |  |  |  | # consulates | 
| 420 | 0 |  |  | 0 | 0 |  | my ($self) = @_; | 
| 421 | 0 | 0 |  |  |  |  | return if $self->comm_set; | 
| 422 | 0 |  |  |  |  |  | my @candidates = (); | 
| 423 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 424 | 0 | 0 | 0 |  |  |  | push(@candidates, $hex) | 
|  |  |  | 0 |  |  |  |  | 
| 425 |  |  |  |  |  |  | if $hex->starport eq 'A' | 
| 426 |  |  |  |  |  |  | or $hex->naval | 
| 427 |  |  |  |  |  |  | or $hex->consulate; | 
| 428 |  |  |  |  |  |  | } | 
| 429 |  |  |  |  |  |  | # every system has a link to its neighbours | 
| 430 | 0 |  |  |  |  |  | foreach my $hex (@candidates) { | 
| 431 | 0 |  |  |  |  |  | my @ar = nearby($hex, 2, \@candidates); | 
| 432 | 0 |  |  |  |  |  | $hex->comm(\@ar); | 
| 433 |  |  |  |  |  |  | } | 
| 434 |  |  |  |  |  |  | # eliminate all but the best connections if the system has | 
| 435 |  |  |  |  |  |  | # amber or red travel zone | 
| 436 | 0 |  |  |  |  |  | foreach my $hex (@candidates) { | 
| 437 | 0 | 0 |  |  |  |  | next unless $hex->travelzone; | 
| 438 | 0 |  |  |  |  |  | my $best; | 
| 439 | 0 |  |  |  |  |  | foreach my $other (@{$hex->comm}) { | 
|  | 0 |  |  |  |  |  |  | 
| 440 | 0 | 0 | 0 |  |  |  | if (not $best | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 441 |  |  |  |  |  |  | or $other->starport lt $best->starport | 
| 442 |  |  |  |  |  |  | or $other->starport eq $best->starport | 
| 443 |  |  |  |  |  |  | and distance($hex, $other) < distance($hex, $best)) { | 
| 444 | 0 |  |  |  |  |  | $best = $other; | 
| 445 |  |  |  |  |  |  | } | 
| 446 |  |  |  |  |  |  | } | 
| 447 | 0 |  |  |  |  |  | $hex->eliminate(grep { $_ != $best } @{$hex->comm}); | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | } | 
| 449 |  |  |  |  |  |  | } | 
| 450 |  |  |  |  |  |  |  | 
| 451 |  |  |  |  |  |  | sub trade { | 
| 452 |  |  |  |  |  |  | # connect In or Ht with As, De, Ic, Ni | 
| 453 |  |  |  |  |  |  | # connect Hi or Ri with Ag, Ga, Wa | 
| 454 | 0 |  |  | 0 | 0 |  | my ($self) = @_; | 
| 455 | 0 | 0 |  |  |  |  | return if $self->trade_set; | 
| 456 |  |  |  |  |  |  | # candidates need to be on a travel route, i.e. must have fuel | 
| 457 |  |  |  |  |  |  | # available; skip worlds with a red travel zone | 
| 458 | 0 |  |  |  |  |  | my @candidates = (); | 
| 459 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | push(@candidates, $hex) | 
| 461 |  |  |  |  |  |  | if ($hex->starport =~ /^[A-D]$/ | 
| 462 |  |  |  |  |  |  | or $hex->gasgiant | 
| 463 |  |  |  |  |  |  | or $hex->trade->{Wa}) | 
| 464 | 0 | 0 | 0 |  |  |  | and $hex->travelzone ne 'R'; | 
|  |  |  | 0 |  |  |  |  | 
| 465 |  |  |  |  |  |  | } | 
| 466 |  |  |  |  |  |  | # every system has a link to its partners | 
| 467 | 0 |  |  |  |  |  | foreach my $hex (@candidates) { | 
| 468 | 0 |  |  |  |  |  | my @routes; | 
| 469 | 0 | 0 | 0 |  |  |  | if ($hex->trade->{In} or $hex->trade->{Ht}) { | 
|  |  | 0 | 0 |  |  |  |  | 
| 470 | 0 |  |  |  |  |  | foreach my $other (nearby($hex, 4, \@candidates)) { | 
| 471 | 0 | 0 | 0 |  |  |  | if ($other->trade->{As} | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 472 |  |  |  |  |  |  | or $other->trade->{De} | 
| 473 |  |  |  |  |  |  | or $other->trade->{Ic} | 
| 474 |  |  |  |  |  |  | or $other->trade->{Ni}) { | 
| 475 | 0 |  |  |  |  |  | my @route = $self->route($hex, $other, 4, \@candidates); | 
| 476 | 0 | 0 |  |  |  |  | push(@routes, \@route) if @route; | 
| 477 |  |  |  |  |  |  | } | 
| 478 |  |  |  |  |  |  | } | 
| 479 |  |  |  |  |  |  | } elsif ($hex->trade->{Hi} or $hex->trade->{Ri}) { | 
| 480 | 0 |  |  |  |  |  | foreach my $other (nearby($hex, 4, \@candidates)) { | 
| 481 | 0 | 0 | 0 |  |  |  | if ($other->trade->{Ag} | 
|  |  |  | 0 |  |  |  |  | 
| 482 |  |  |  |  |  |  | or $other->trade->{Ga} | 
| 483 |  |  |  |  |  |  | or $other->trade->{Wa}) { | 
| 484 | 0 |  |  |  |  |  | my @route = $self->route($hex, $other, 4, \@candidates); | 
| 485 | 0 | 0 |  |  |  |  | push(@routes, \@route) if @route; | 
| 486 |  |  |  |  |  |  | } | 
| 487 |  |  |  |  |  |  | } | 
| 488 |  |  |  |  |  |  | } | 
| 489 | 0 |  |  |  |  |  | $hex->routes(\@routes); | 
| 490 |  |  |  |  |  |  | } | 
| 491 | 0 |  |  |  |  |  | $self->routes($self->minimal_spanning_tree($self->edges(@candidates))); | 
| 492 |  |  |  |  |  |  | } | 
| 493 |  |  |  |  |  |  |  | 
| 494 |  |  |  |  |  |  | sub edges { | 
| 495 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 496 | 0 |  |  |  |  |  | my @edges; | 
| 497 |  |  |  |  |  |  | my %seen; | 
| 498 | 0 |  |  |  |  |  | foreach my $hex (@_) { | 
| 499 | 0 |  |  |  |  |  | foreach my $route (@{$hex->routes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 500 | 0 |  |  |  |  |  | my ($start, @route) = @{$route}; | 
|  | 0 |  |  |  |  |  |  | 
| 501 | 0 |  |  |  |  |  | foreach my $end (@route) { | 
| 502 |  |  |  |  |  |  | # keep everything unidirectional | 
| 503 | 0 | 0 | 0 |  |  |  | next if exists $seen{$start}{$end} or exists $seen{$end}{$start}; | 
| 504 | 0 |  |  |  |  |  | push(@edges, [$start, $end, distance($start,$end)]); | 
| 505 | 0 |  |  |  |  |  | $seen{$start}{$end} = 1; | 
| 506 | 0 |  |  |  |  |  | $start = $end; | 
| 507 |  |  |  |  |  |  | } | 
| 508 |  |  |  |  |  |  | } | 
| 509 |  |  |  |  |  |  | } | 
| 510 | 0 |  |  |  |  |  | return @edges; | 
| 511 |  |  |  |  |  |  | } | 
| 512 |  |  |  |  |  |  |  | 
| 513 |  |  |  |  |  |  | sub minimal_spanning_tree { | 
| 514 |  |  |  |  |  |  | # http://en.wikipedia.org/wiki/Kruskal%27s_algorithm | 
| 515 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 516 |  |  |  |  |  |  | # Initialize a priority queue Q to contain all edges in G, using the | 
| 517 |  |  |  |  |  |  | # weights as keys. | 
| 518 | 0 |  |  |  |  |  | my @Q = sort { @{$a}[2] <=> @{$b}[2] } @_; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 519 |  |  |  |  |  |  | # Define a forest T ← Ø; T will ultimately contain the edges of the MST | 
| 520 | 0 |  |  |  |  |  | my @T; | 
| 521 |  |  |  |  |  |  | # Define an elementary cluster C(v) ← {v}. | 
| 522 |  |  |  |  |  |  | my %C; | 
| 523 | 0 |  |  |  |  |  | my $id; | 
| 524 | 0 |  |  |  |  |  | foreach my $edge (@Q) { | 
| 525 |  |  |  |  |  |  | # edge u,v is the minimum weighted route from u to v | 
| 526 | 0 |  |  |  |  |  | my ($u, $v) = @{$edge}; | 
|  | 0 |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | # $u = $u->name; | 
| 528 |  |  |  |  |  |  | # $v = $v->name; | 
| 529 |  |  |  |  |  |  | # prevent cycles in T; add u,v only if T does not already contain | 
| 530 |  |  |  |  |  |  | # a path between u and v; also silence warnings | 
| 531 | 0 | 0 | 0 |  |  |  | if (not $C{$u} or not $C{$v} or $C{$u} != $C{$v}) { | 
|  |  |  | 0 |  |  |  |  | 
| 532 |  |  |  |  |  |  | # Add edge (v,u) to T. | 
| 533 | 0 |  |  |  |  |  | push(@T, $edge); | 
| 534 |  |  |  |  |  |  | # Merge C(v) and C(u) into one cluster, that is, union C(v) and C(u). | 
| 535 | 0 | 0 | 0 |  |  |  | if ($C{$u} and $C{$v}) { | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 | 0 |  |  |  |  | 
| 536 | 0 |  |  |  |  |  | my @group; | 
| 537 | 0 |  |  |  |  |  | foreach (keys %C) { | 
| 538 | 0 | 0 |  |  |  |  | push(@group, $_) if $C{$_} == $C{$v}; | 
| 539 |  |  |  |  |  |  | } | 
| 540 | 0 |  |  |  |  |  | $C{$_} = $C{$u} foreach @group; | 
| 541 |  |  |  |  |  |  | } elsif ($C{$v} and not $C{$u}) { | 
| 542 | 0 |  |  |  |  |  | $C{$u} = $C{$v}; | 
| 543 |  |  |  |  |  |  | } elsif ($C{$u} and not $C{$v}) { | 
| 544 | 0 |  |  |  |  |  | $C{$v} = $C{$u}; | 
| 545 |  |  |  |  |  |  | } elsif (not $C{$u} and not $C{$v}) { | 
| 546 | 0 |  |  |  |  |  | $C{$v} = $C{$u} = ++$id; | 
| 547 |  |  |  |  |  |  | } | 
| 548 |  |  |  |  |  |  | } | 
| 549 |  |  |  |  |  |  | } | 
| 550 | 0 |  |  |  |  |  | return \@T; | 
| 551 |  |  |  |  |  |  | } | 
| 552 |  |  |  |  |  |  |  | 
| 553 |  |  |  |  |  |  | sub route { | 
| 554 |  |  |  |  |  |  | # Compute the shortest route between two hexes no longer than a | 
| 555 |  |  |  |  |  |  | # certain distance and choosing intermediary steps from the array of | 
| 556 |  |  |  |  |  |  | # possible candidates. | 
| 557 | 0 |  |  | 0 | 0 |  | my ($self, $from, $to, $distance, $candidatesref, @seen) = @_; | 
| 558 |  |  |  |  |  |  | # my $indent = ' ' x (4-$distance); | 
| 559 | 0 |  |  |  |  |  | my @options; | 
| 560 | 0 | 0 |  |  |  |  | foreach my $hex (nearby($from, $distance < 2 ? $distance : 2, $candidatesref)) { | 
| 561 | 0 | 0 |  |  |  |  | push (@options, $hex) unless in($hex, @seen); | 
| 562 |  |  |  |  |  |  | } | 
| 563 | 0 | 0 | 0 |  |  |  | return unless @options and $distance; | 
| 564 | 0 | 0 |  |  |  |  | if (in($to, @options)) { | 
| 565 | 0 |  |  |  |  |  | return @seen, $from, $to; | 
| 566 |  |  |  |  |  |  | } | 
| 567 | 0 |  |  |  |  |  | my @routes; | 
| 568 | 0 |  |  |  |  |  | foreach my $hex (@options) { | 
| 569 | 0 |  |  |  |  |  | my @route = $self->route($hex, $to, $distance - distance($from, $hex), | 
| 570 |  |  |  |  |  |  | $candidatesref, @seen, $from); | 
| 571 | 0 | 0 |  |  |  |  | if (@route) { | 
| 572 | 0 |  |  |  |  |  | push(@routes, \@route); | 
| 573 |  |  |  |  |  |  | } | 
| 574 |  |  |  |  |  |  | } | 
| 575 | 0 | 0 |  |  |  |  | return unless @routes; | 
| 576 |  |  |  |  |  |  | # return the shortest one | 
| 577 | 0 |  |  |  |  |  | my @shortest; | 
| 578 | 0 |  |  |  |  |  | foreach my $route (@routes) { | 
| 579 | 0 | 0 | 0 |  |  |  | if ($#{$route} < $#shortest or not @shortest) { | 
|  | 0 |  |  |  |  |  |  | 
| 580 | 0 |  |  |  |  |  | @shortest = @{$route}; | 
|  | 0 |  |  |  |  |  |  | 
| 581 |  |  |  |  |  |  | } | 
| 582 |  |  |  |  |  |  | } | 
| 583 | 0 |  |  |  |  |  | return @shortest; | 
| 584 |  |  |  |  |  |  | } | 
| 585 |  |  |  |  |  |  |  | 
| 586 |  |  |  |  |  |  | sub trade_svg { | 
| 587 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 588 | 0 |  |  |  |  |  | my $data = ''; | 
| 589 | 0 |  |  |  |  |  | my $scale = 100; | 
| 590 | 0 |  |  |  |  |  | foreach my $edge (@{$self->routes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 591 | 0 |  |  |  |  |  | my $u = @{$edge}[0]; | 
|  | 0 |  |  |  |  |  |  | 
| 592 | 0 |  |  |  |  |  | my $v = @{$edge}[1]; | 
|  | 0 |  |  |  |  |  |  | 
| 593 | 0 |  |  |  |  |  | my ($x1, $y1) = ($u->x, $u->y); | 
| 594 | 0 |  |  |  |  |  | my ($x2, $y2) = ($v->x, $v->y); | 
| 595 | 0 |  |  |  |  |  | $data .= sprintf(qq{    \n}, | 
| 596 |  |  |  |  |  |  | (1 + ($x1-1) * 1.5) * $scale, ($y1 - $x1%2/2) * sqrt(3) * $scale, | 
| 597 |  |  |  |  |  |  | (1 + ($x2-1) * 1.5) * $scale, ($y2 - $x2%2/2) * sqrt(3) * $scale); | 
| 598 |  |  |  |  |  |  | } | 
| 599 | 0 |  |  |  |  |  | return $data; | 
| 600 |  |  |  |  |  |  | } | 
| 601 |  |  |  |  |  |  |  | 
| 602 |  |  |  |  |  |  | sub svg { | 
| 603 | 0 |  |  | 0 | 0 |  | my ($self, $width, $height) = @_; | 
| 604 | 0 |  |  |  |  |  | my $data = $self->header($width, $height); | 
| 605 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 606 | 0 |  |  |  |  |  | $data .= $self->background; | 
| 607 | 0 |  |  |  |  |  | $data .= qq{  \n\n}; | 
| 608 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 609 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 610 | 0 |  |  |  |  |  | $data .= $hex->comm_svg(); | 
| 611 |  |  |  |  |  |  | } | 
| 612 | 0 |  |  |  |  |  | $data .= qq{  \n\n}; | 
| 613 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 614 | 0 |  |  |  |  |  | $data .= $self->trade_svg(); | 
| 615 | 0 |  |  |  |  |  | $data .= qq{  \n\n}; | 
| 616 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 617 | 0 |  |  |  |  |  | $data .= $self->grid; | 
| 618 | 0 |  |  |  |  |  | $data .= qq{  \n\n}; | 
| 619 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 620 | 0 |  |  |  |  |  | $data .= $self->legend(); | 
| 621 | 0 |  |  |  |  |  | $data .= qq{  \n\n}; | 
| 622 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 623 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 624 | 0 |  |  |  |  |  | $data .= $hex->system_svg(); | 
| 625 |  |  |  |  |  |  | } | 
| 626 | 0 |  |  |  |  |  | $data .= qq{  \n}; | 
| 627 | 0 |  |  |  |  |  | $data .= $self->footer(); | 
| 628 | 0 |  |  |  |  |  | return $data; | 
| 629 |  |  |  |  |  |  | } | 
| 630 |  |  |  |  |  |  |  | 
| 631 |  |  |  |  |  |  | sub text { | 
| 632 | 0 |  |  | 0 | 0 |  | my ($self) = @_; | 
| 633 | 0 |  |  |  |  |  | my $data = "Trade Routes:\n"; | 
| 634 | 0 |  |  |  |  |  | foreach my $edge (@{$self->routes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 635 | 0 |  |  |  |  |  | my $u = @{$edge}[0]; | 
|  | 0 |  |  |  |  |  |  | 
| 636 | 0 |  |  |  |  |  | my $v = @{$edge}[1]; | 
|  | 0 |  |  |  |  |  |  | 
| 637 | 0 |  |  |  |  |  | $data .= $u->name . " - " . $v->name . "\n"; | 
| 638 |  |  |  |  |  |  | } | 
| 639 | 0 |  |  |  |  |  | $data .= "\n"; | 
| 640 | 0 |  |  |  |  |  | $data .= "Raw Data:\n"; | 
| 641 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 642 | 0 |  |  |  |  |  | foreach my $routeref (@{$hex->routes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 643 | 0 |  |  |  |  |  | $data .= join(' - ', map {$_->name} @{$routeref}) . "\n"; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 644 |  |  |  |  |  |  | } | 
| 645 |  |  |  |  |  |  | } | 
| 646 | 0 |  |  |  |  |  | $data .= "\n"; | 
| 647 | 0 |  |  |  |  |  | $data .= "Communications:\n"; | 
| 648 | 0 |  |  |  |  |  | foreach my $hex (@{$self->hexes}) { | 
|  | 0 |  |  |  |  |  |  | 
| 649 | 0 |  |  |  |  |  | foreach my $comm (@{$hex->comm}) { | 
|  | 0 |  |  |  |  |  |  | 
| 650 | 0 |  |  |  |  |  | $data .= $hex->name . " - " . $comm->name . "\n";; | 
| 651 |  |  |  |  |  |  | } | 
| 652 |  |  |  |  |  |  | } | 
| 653 | 0 |  |  |  |  |  | return $data; | 
| 654 |  |  |  |  |  |  | } | 
| 655 |  |  |  |  |  |  |  | 
| 656 |  |  |  |  |  |  | 1; |