chiark / gitweb /
e3150611cb36eb75a6a3eb117e276982ad5aebd0
[ypp-sc-tools.db-live.git] / yarrg / yppedia-chart-parser
1 #!/usr/bin/perl
2
3 use strict (qw(vars));
4 use warnings;
5
6 use Graph::Undirected;
7 use Commods;
8 use CommodsDatabase;
9
10 my $ocean= 'Midnight';
11
12
13 my $widists= Graph::Undirected->new();
14 my $wiarchs= Graph::Undirected->new();
15 my @wiarchlabels;
16 my %wiisland2node;
17 my %winode2island;
18 my %winode2lines;
19 my %wiccix2arch;
20 my $wialldists;
21
22 my $dbdists= Graph::Undirected->new();
23 my %dbisland2arch;
24
25 my %msgs;
26 sub pmsg ($$) { push @{ $msgs{$_[0]} }, "$_[0]: $_[1]\n"; }
27 sub warning ($) { pmsg("warning",$_[0]); }
28 sub error   ($) { pmsg("error",  $_[0]); }
29 sub change  ($) { pmsg("change", $_[0]); }
30 sub print_messages () {
31     foreach my $k (qw(change warning error)) {
32         my $m= $msgs{$k};
33         next unless $m;
34         print sort @$m or die $!;
35     }
36 }
37
38 if (@ARGV && $ARGV[0] eq '--debug') {
39     shift @ARGV;
40     open DEBUG, ">&STDOUT" or die $!;
41     select(DEBUG); $|=1;
42 } else {
43     open DEBUG, ">/dev/null" or die $!;
44 }
45 select(STDOUT); $|=1;
46
47 my $parity;
48 sub nn_xy ($$) {
49     my ($x,$y) = @_;
50     my $tp= (0+$x ^ 0+$y) & 1;
51     defined $parity or $parity=$tp;
52     $tp==$parity or warning("line $.: parity error $x,$y is $tp not $parity");
53     my $n= "$_[0],$_[1]";
54     $winode2lines{$n}{$.}++;
55     return $n;
56 }
57
58 sub parse_yppedia_map () {
59     # We don't even bother with tag soup; instead we do line-oriented parsing.
60
61     while (<>) {
62         s/\<--.*--\>//g;
63         s/^\s*//; chomp; s/\s+$//; s/\s+/ /g;
64         s/\<\/?(?:b|em)\>//g;
65         s/\{\{Chart\ style\|[^{}]*\}\}//g;
66         next unless m/\{\{/; # only interested in chart template stuff
67
68         my ($x,$y, $arch,$island,$solid,$dirn);
69         my $nn= sub { return nn_xy($x,$y) };
70     
71         if (($x,$y,$arch) =
72             m/^\{\{ chart\ label \|(\d+)\|(\d+)\| .*
73                     \'\[\[ [^][\']* \| (\S+)\ archipelago \]\]\'*\}\}$/xi) {
74             printf DEBUG "%2d,%-2d arch %s\n", $x,$y,$arch;
75             push @wiarchlabels, [ $x,$y,$arch ];
76         } elsif (($x,$y,$island) =
77             m/^\{\{ chart\ island\ icon \|(\d+)\|(\d+)\|
78                     ([^| ][^|]*[^| ]) \| .*\}\}$/xi) {
79             my $n= $nn->();
80             $wiisland2node{$island}= $n;
81             $winode2island{$n}= $island;
82             $widists->add_vertex($n);
83             $wiarchs->add_vertex($n);
84 #print "\$g->add_vertex('$n');\n";
85             printf DEBUG "%2d,%-2d island %s\n", $x,$y,$island;
86         } elsif (($solid,$x,$y,$dirn) =
87             m/^\{\{ chart\ league((?:\ solid)?) \|(\d+)\|(\d+)\|
88                     ([-\/\\o]) \| .*\}\}$/xi) {
89             next if $dirn eq 'o';
90
91             my ($bx,$by) = ($x,$y);
92             if ($dirn eq '-') { $bx+=2; }
93             elsif ($dirn eq '\\') { $bx++; $by++; }
94             elsif ($dirn eq '/') { $x++; $by++; }
95             else { die; }
96
97             $widists->add_weighted_edge($nn->(), nn_xy($bx,$by), 1);
98             $wiarchs->add_edge($nn->(), nn_xy($bx,$by)) if $solid;
99             $wiarchs->add_edge($nn->(), nn_xy($bx,$by)) if $solid;
100 #print "\$g->add_edge('".$nn->()."','".nn_xy($bx,$by)."');\n" if $solid;
101
102             printf DEBUG "%2d,%-2d league %-6s %s\n", $x,$y,
103                 $solid?'solid':'dotted', $dirn;
104         } elsif (
105             m/^\{\{ chart\ head \}\}$/xi
106                  ) {
107             next;
108         } else {
109             warning("line $.: ignoring incomprehensible: $_");
110         }
111     }
112 }
113
114 sub parse_database_map () {
115     my ($row,$sth);
116     $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
117     $sth->execute();
118     while ($row= $sth->fetchrow_hashref) {
119         print DEBUG "database-island $row->{'islandname'}".
120                      " $row->{'archipelago'}\n";
121         $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
122     }
123     $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
124                                 FROM dists
125                                 JOIN islands AS a ON dists.aiid==a.islandid
126                                 JOIN islands AS b ON dists.biid==b.islandid');
127     $sth->execute();
128     while ($row= $sth->fetchrow_hashref) {
129         $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
130     }
131 }                        
132
133 sub process_yppedia_graphs () {
134
135     # Prune the LP database by eliminating boring intermediate vertices
136     #
137     foreach my $delete ($widists->vertices()) {
138         next if exists $winode2island{$delete};
139         my @neigh= $widists->neighbours($delete);
140         next unless @neigh==2;
141 #       my @aneigh= $wiarchs->has_vertex($delete)
142 #           ? $wiarchs->neighbours($delete) : ();
143 #       next unless @aneigh==0 || @aneigh==2;
144         my $weight= 0;
145         map { $weight += $widists->get_edge_weight($delete, $_) } @neigh;
146         $widists->add_weighted_edge(@neigh, $weight);
147         $widists->delete_vertex($delete);
148         printf DEBUG "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
149     }
150
151     # Check that it's connected.
152     #
153     foreach my $cc ($widists->connected_components()) {
154         next if 2*@$cc > $widists->vertices();
155         my $m= "disconnected league point(s):";
156         foreach my $n (@$cc) {
157             $m .= "\n    LP $n, def. yppedia line(s): ".
158                 join(',', sort keys %{ $winode2lines{$n} });
159         }
160         warning($m);
161     }
162
163     # Assign archipelagoes according to the source-info file
164     #
165     foreach my $arch (sort keys %{ $oceans{$ocean} }) {
166         foreach my $islename (sort keys %{ $oceans{$ocean}{$arch} }) {
167             my $islenode= $wiisland2node{$islename};
168             defined $islenode or
169                 error("island $islename in source-info but not in WP map");
170             my $ccix= $wiarchs->connected_component_by_vertex($islenode);
171             my $oldarch= $wiccix2arch{$ccix};
172             error("island $islename in $arch in source-info".
173                   " connected to $oldarch as well")
174                 if defined $oldarch && $oldarch ne $arch;
175             printf DEBUG "%-5s force-island-arch cc%-2d %-10s %s\n",
176                 $islenode, $ccix, $arch, $islename;
177             $wiccix2arch{$ccix}= $arch;
178         }
179     }
180
181     # Compute all-pairs-shortest-paths on dist, which is the
182     # actual distances between all LPs.
183     #
184     $wialldists= $widists->APSP_Floyd_Warshall();
185
186     # Assign archipelago labels to groups of islands
187     #
188     foreach my $label (@wiarchlabels) {
189         my ($ax,$ay,$arch) = @$label;
190         my $best_ccmulti= -1;
191         my $best_d2= 0;
192         my $best_n;
193 #       print DEBUG "$ax,$ay arch-island-search $arch\n";
194         $ay += 1;  $ax += 2;  # coords are rather to the top left of label
195         foreach my $vertex ($wiarchs->vertices()) {
196             next unless exists $winode2island{$vertex};
197             my $ccix= $wiarchs->connected_component_by_vertex($vertex);
198             my @cc= $wiarchs->connected_component_by_index($ccix);
199             my $ccmulti= @cc > 1;
200             my ($vx,$vy) = split /,/, $vertex;
201             my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
202             my $cmp= $ccmulti <=> $best_ccmulti
203                 ||   $best_d2 <=> $d2;
204             printf DEBUG "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
205                          " #cc=%2d ccmulti=%d cmp=%2d %s\n",
206                 $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $ccmulti, $cmp,
207                 $winode2island{$vertex};
208             next unless $cmp > 0;
209             $best_n=       $vertex;
210             $best_d2=      $d2;
211             $best_ccmulti= $ccmulti;
212         }
213         die 'no island vertices?!' unless defined $best_n;
214         my $ccix= $wiarchs->connected_component_by_vertex($best_n);
215         printf DEBUG
216             "%2d,%-2d arch-island-select %-5s d2=%4d cc%-2d     %-10s %s\n",
217             $ax,$ay, $best_n, $ccix, $best_d2, $arch, $winode2island{$best_n};
218         my $desc= join "\n", map {
219             my $in= $winode2island{$_};
220             "    LP $_". (defined $in ? ", $in" : "");
221         } sort $wiarchs->connected_component_by_index($ccix);
222
223         if (exists $wiccix2arch{$ccix} and $wiccix2arch{$ccix} ne $arch) {
224             error("archipelago determination failed, wrongly merged:\n".
225                   "    archipelago $arch\n".
226                   "    archipelago $wiccix2arch{$ccix}\n".
227                   $desc);
228             next;
229         }
230         $wiccix2arch{$ccix}= $arch;
231 #       print "$ccix $arch ::\n$desc\n";
232     }
233
234     # Assign islands not labelled above to archipelagoes.
235     #
236     # We do this by, for each connected component (set of islands
237     # linked by purchaseable charts), searching for the nearest other
238     # connected component which has already been assigned an arch.
239     # `Nearest' means shortest distance of unpurchaseable charts, in
240     # leagues.
241     #
242     # we need only consider vertices which weren't `boring intermediate
243     # vertices' (removed during optimisation as being of order 2)
244     my @ccs_useful= map {
245         [ grep { $widists->has_vertex($_) } @$_ ]
246     } $wiarchs->connected_components();
247
248     my @assignments;
249
250     foreach my $sourceccix (0..$#ccs_useful) {
251         next if defined $wiccix2arch{$sourceccix};
252         next unless $ccs_useful[$sourceccix];
253
254         my @sourcecc= $wiarchs->connected_component_by_index($sourceccix);
255         my @islandnodes= grep { $winode2island{$_} } @sourcecc;
256         next unless @islandnodes; # don't care, then
257
258         foreach my $islandnode (@islandnodes) {
259             printf DEBUG "%-5s arch-join-need cc%-2d             %s\n",
260                 $islandnode, $sourceccix, $winode2island{$islandnode};
261         }
262         my $best_dist= 9999999;
263         my ($best_target, $best_targetccix, $best_source);
264         foreach my $targetccix (0..$#ccs_useful) {
265             next unless defined $wiccix2arch{$targetccix}; # not helpful
266             next unless $ccs_useful[$targetccix];
267             foreach my $target ($wiarchs->
268                          connected_component_by_index($targetccix)) {
269                 foreach my $source (@sourcecc) {
270                     my $target_dist= $wialldists->path_length($target,$source);
271                     next unless defined $target_dist;
272                     next if $target_dist >= $best_dist;
273                     $best_dist= $target_dist;
274                     $best_source= $source;
275                     $best_target= $target;
276                     $best_targetccix= $targetccix;
277                 }
278             }
279         }
280         die "no possible target ?!" unless defined $best_target;
281
282         my $arch= $wiccix2arch{$best_targetccix};
283         my $best_island= $winode2island{$best_target};
284         printf DEBUG "%-5s arch-join-to %-5s dist=%2d cc%-2d  %-10s %s\n",
285             $best_source, $best_target, $best_dist,
286             $best_targetccix, $arch,
287             defined($best_island) ? $best_island : "-";
288
289         push @assignments, [ $sourceccix, $arch ];
290     }
291     foreach my $assign (@assignments) {
292         $wiccix2arch{$assign->[0]}= $assign->[1];
293     }
294 }
295
296 sub winode2arch ($) {
297     my ($node) = @_;
298     my $ccix= $wiarchs->connected_component_by_vertex($node);
299     return $wiccix2arch{$ccix};
300 }
301 sub wiisland2arch ($) {
302     my ($island) = @_;
303     my $node= $wiisland2node{$island};
304     die "$island ?" unless defined $node;
305     return winode2arch($node);
306 }
307
308 sub compare_island_lists () {
309     foreach my $island (sort keys %dbisland2arch) {
310         my $node= $wiisland2node{$island};
311         if (!defined $node) {
312             error("would delete island: $island");
313             next;
314         }
315         my $wiarch= winode2arch($node);
316         if (!defined $wiarch) {
317             error("island has no arch: $island");
318             next;
319         }
320         my $dbarch= $dbisland2arch{$island};
321         if ($wiarch ne $dbarch) {
322             change("change archipelago from $dbarch to $wiarch".
323                    " for island $island");
324         }
325     }
326     foreach my $island (sort keys %wiisland2node) {
327         my $dbarch= $dbisland2arch{$island};
328         if (!defined $dbarch) {
329             my $wiarch= wiisland2arch($island);
330             if (!defined $wiarch) {
331                 error("new island has no arch: $island");
332                 next;
333                 # We check arches of non-new islands above
334             }
335             change("new island in $wiarch: $island");
336         }
337     }
338 }
339
340 sub compare_distances () {
341     foreach my $ia (sort keys %dbisland2arch) {
342         my $na= $wiisland2node{$ia};
343         next unless defined $na;
344         foreach my $ib (sort keys %dbisland2arch) {
345             next unless $ia le $ib; # do every pair only once
346             my $nb= $wiisland2node{$ib};
347             next unless defined $nb;
348             my $dbdist= $dbdists->get_edge_weight($ia,$ib);
349             my $widist= $wialldists->path_length($na,$nb);
350             if (!defined $dbdist) {
351                 change(sprintf "define distance %2d for %s..%s",
352                        $widist, $ia,$ib);
353             } elsif ($dbdist != $widist) {
354                 change(sprintf "change distance %2d to %2d for %s..%s",
355                        $dbdist, $widist, $ia,$ib);
356             }
357         }
358     }
359 }
360
361 parse_info_serverside();
362 db_setocean($ocean);
363 db_connect();
364 parse_yppedia_map();
365 parse_database_map();
366 process_yppedia_graphs();
367
368 compare_island_lists();
369 compare_distances();
370
371 print_messages();