chiark / gitweb /
Fixes etc. for ypp map parser and distance processor
[ypp-sc-tools.main.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 yppedia_chart_parse () {
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             printf DEBUG "%2d,%-2d island %s\n", $x,$y,$island;
85         } elsif (($solid,$x,$y,$dirn) =
86             m/^\{\{ chart\ league((?:\ solid)?) \|(\d+)\|(\d+)\|
87                     ([-\/\\o]) \| .*\}\}$/xi) {
88             next if $dirn eq 'o';
89
90             my ($bx,$by) = ($x,$y);
91             if ($dirn eq '-') { $bx+=2; }
92             elsif ($dirn eq '\\') { $bx++; $by++; }
93             elsif ($dirn eq '/') { $x++; $by++; }
94             else { die; }
95
96             my $nb= nn_xy($bx,$by);
97             $widists->add_weighted_edge($nn->(), $nb, 1);
98             $wiarchs->add_edge($nn->(), $nb) if $solid;
99             $wiarchs->add_edge($nn->(), $nb) if $solid;
100
101             printf DEBUG "%2d,%-2d league %-6s %s %s\n", $x,$y,
102                 $solid?'solid':'dotted', $dirn, $nb;
103         } elsif (
104             m/^\{\{ chart\ head \}\}$/xi
105                  ) {
106             next;
107         } else {
108             warning("line $.: ignoring incomprehensible: $_");
109         }
110     }
111 }
112
113 sub database_fetch_ocean () {
114     my ($row,$sth);
115     $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
116     $sth->execute();
117     while ($row= $sth->fetchrow_hashref) {
118         print DEBUG "database-island $row->{'islandname'}".
119                      " $row->{'archipelago'}\n";
120         $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
121     }
122     $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
123                                 FROM dists
124                                 JOIN islands AS a ON dists.aiid==a.islandid
125                                 JOIN islands AS b ON dists.biid==b.islandid');
126     $sth->execute();
127     while ($row= $sth->fetchrow_hashref) {
128         $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
129     }
130 }                        
131
132 sub yppedia_graphs_add_shortcuts () {
133     # We add edges between LPs we know about, as you can chart
134     # between them.  Yppedia often lacks these edges.
135     #
136     foreach my $p ($widists->vertices) {
137         my ($ax,$ay) = $p =~ m/^(\d+)\,(\d+)$/ or die;
138         my $add_shortcut= sub {
139             my $q= sprintf "%d,%d", $ax+$_[0], $ay+$_[1];
140             return unless $widists->has_vertex($q);
141             return if $widists->has_edge($p,$q);
142             printf DEBUG "%-5s league-shortcut %-5s\n", $p, $q;
143             $widists->add_weighted_edge($p,$q,1);
144         };
145         $add_shortcut->( 2,0);
146         $add_shortcut->(+1,1);
147         $add_shortcut->(-1,1);
148     }
149 }
150
151 sub yppedia_graphs_prune_boring () {
152     # Prune the LP database by eliminating boring intermediate vertices
153     foreach my $delete ($widists->vertices()) {
154         next if exists $winode2island{$delete};
155         my @neigh= $widists->neighbours($delete);
156         next unless @neigh==2;
157         my $weight= 0;
158         map { $weight += $widists->get_edge_weight($delete, $_) } @neigh;
159         $widists->add_weighted_edge(@neigh, $weight);
160         $widists->delete_vertex($delete);
161         printf DEBUG "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
162     }
163 }
164
165 sub yppedia_graphs_check () {
166     # Check that it's connected.
167     foreach my $cc ($widists->connected_components()) {
168         next if 2*@$cc > $widists->vertices();
169         my $m= "disconnected league point(s):";
170         foreach my $n (@$cc) {
171             $m .= "\n    LP $n, def. yppedia line(s): ".
172                 join(',', sort keys %{ $winode2lines{$n} });
173         }
174         warning($m);
175     }
176 }
177
178 sub yppedia_archs_sourceinfo () {
179     # Assign archipelagoes according to the source-info file
180     foreach my $arch (sort keys %{ $oceans{$ocean} }) {
181         foreach my $islename (sort keys %{ $oceans{$ocean}{$arch} }) {
182             my $islenode= $wiisland2node{$islename};
183             defined $islenode or
184                 error("island $islename in source-info but not in WP map");
185             my $ccix= $wiarchs->connected_component_by_vertex($islenode);
186             my $oldarch= $wiccix2arch{$ccix};
187             error("island $islename in $arch in source-info".
188                   " connected to $oldarch as well")
189                 if defined $oldarch && $oldarch ne $arch;
190             printf DEBUG "%-5s force-island-arch cc%-2d %-10s %s\n",
191                 $islenode, $ccix, $arch, $islename;
192             $wiccix2arch{$ccix}= $arch;
193         }
194     }
195 }
196
197 sub yppedia_archs_chart_labels () {
198     # Assign archipelago labels to groups of islands
199     #
200     foreach my $label (@wiarchlabels) {
201         my ($ax,$ay,$arch) = @$label;
202         my $best_ccmulti= -1;
203         my $best_d2= 0;
204         my $best_n;
205 #       print DEBUG "$ax,$ay arch-island-search $arch\n";
206         $ay += 1;  $ax += 2;  # coords are rather to the top left of label
207         foreach my $vertex ($wiarchs->vertices()) {
208             next unless exists $winode2island{$vertex};
209             my $ccix= $wiarchs->connected_component_by_vertex($vertex);
210             my @cc= $wiarchs->connected_component_by_index($ccix);
211             my $ccmulti= @cc > 1;
212             my ($vx,$vy) = split /,/, $vertex;
213             my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
214             my $cmp= $ccmulti <=> $best_ccmulti
215                 ||   $best_d2 <=> $d2;
216             printf DEBUG "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
217                          " #cc=%2d ccmulti=%d cmp=%2d %s\n",
218                 $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $ccmulti, $cmp,
219                 $winode2island{$vertex};
220             next unless $cmp > 0;
221             $best_n=       $vertex;
222             $best_d2=      $d2;
223             $best_ccmulti= $ccmulti;
224         }
225         die 'no island vertices?!' unless defined $best_n;
226         my $ccix= $wiarchs->connected_component_by_vertex($best_n);
227         printf DEBUG
228             "%2d,%-2d arch-island-select %-5s d2=%4d cc%-2d     %-10s %s\n",
229             $ax,$ay, $best_n, $ccix, $best_d2, $arch, $winode2island{$best_n};
230         my $desc= join "\n", map {
231             my $in= $winode2island{$_};
232             "    LP $_". (defined $in ? ", $in" : "");
233         } sort $wiarchs->connected_component_by_index($ccix);
234
235         if (exists $wiccix2arch{$ccix} and $wiccix2arch{$ccix} ne $arch) {
236             error("archipelago determination failed, wrongly merged:\n".
237                   "    archipelago $arch\n".
238                   "    archipelago $wiccix2arch{$ccix}\n".
239                   $desc);
240             next;
241         }
242         $wiccix2arch{$ccix}= $arch;
243 #       print "$ccix $arch ::\n$desc\n";
244     }
245 }
246
247 sub yppedia_archs_fillbynearest() {
248     # Assign islands not labelled above to archipelagoes.
249     #
250     # We do this by, for each connected component (set of islands
251     # linked by purchaseable charts), searching for the nearest other
252     # connected component which has already been assigned an arch.
253     # `Nearest' means shortest distance of unpurchaseable charts, in
254     # leagues.
255     #
256     # we need only consider vertices which weren't `boring intermediate
257     # vertices' (removed during optimisation as being of order 2)
258     my @ccs_useful= map {
259         [ grep { $widists->has_vertex($_) } @$_ ]
260     } $wiarchs->connected_components();
261
262     my @assignments;
263
264     foreach my $sourceccix (0..$#ccs_useful) {
265         next if defined $wiccix2arch{$sourceccix};
266         next unless $ccs_useful[$sourceccix];
267
268         my @sourcecc= $wiarchs->connected_component_by_index($sourceccix);
269         my @islandnodes= grep { $winode2island{$_} } @sourcecc;
270         next unless @islandnodes; # don't care, then
271
272         foreach my $islandnode (@islandnodes) {
273             printf DEBUG "%-5s arch-join-need cc%-2d             %s\n",
274                 $islandnode, $sourceccix, $winode2island{$islandnode};
275         }
276         my $best_dist= 9999999;
277         my ($best_target, $best_targetccix, $best_source);
278         foreach my $targetccix (0..$#ccs_useful) {
279             next unless defined $wiccix2arch{$targetccix}; # not helpful
280             next unless $ccs_useful[$targetccix];
281             foreach my $target ($wiarchs->
282                          connected_component_by_index($targetccix)) {
283                 next unless $widists->has_vertex($target);
284                 foreach my $source (@sourcecc) {
285                     my $target_dist= widist($target,$source);
286                     next unless defined $target_dist;
287                     next if $target_dist >= $best_dist;
288                     $best_dist= $target_dist;
289                     $best_source= $source;
290                     $best_target= $target;
291                     $best_targetccix= $targetccix;
292                 }
293             }
294         }
295         die "no possible target ?!" unless defined $best_target;
296
297         my $arch= $wiccix2arch{$best_targetccix};
298         my $best_island= $winode2island{$best_target};
299         printf DEBUG "%-5s arch-join-to %-5s dist=%2d cc%-2d  %-10s %s\n",
300             $best_source, $best_target, $best_dist,
301             $best_targetccix, $arch,
302             defined($best_island) ? $best_island : "-";
303
304         push @assignments, [ $sourceccix, $arch ];
305     }
306     foreach my $assign (@assignments) {
307         $wiccix2arch{$assign->[0]}= $assign->[1];
308     }
309 }
310
311 sub yppedia_graphs_shortest_paths () {
312     $wialldists= $widists->APSP_Floyd_Warshall();
313 }
314
315 sub widist ($$) {
316     my ($p,$q) = @_;
317     my $pl= $wialldists->path_length($p,$q);
318 #    die "$p $q" unless defined $pl;
319 #    my @pv= $wialldists->path_vertices($p,$q);
320 #    if (@pv == $pl) { return $pl; }
321 #   printf DEBUG "%-5s PATHLENGTH %-5s pl=%s pv=%s\n", $p,$q,$pl,join('|',@pv);
322     return $pl;
323 }
324                         
325 sub winode2arch ($) {
326     my ($node) = @_;
327     my $ccix= $wiarchs->connected_component_by_vertex($node);
328     return $wiccix2arch{$ccix};
329 }
330 sub wiisland2arch ($) {
331     my ($island) = @_;
332     my $node= $wiisland2node{$island};
333     die "$island ?" unless defined $node;
334     return winode2arch($node);
335 }
336
337 sub compare_island_lists () {
338     foreach my $island (sort keys %dbisland2arch) {
339         my $node= $wiisland2node{$island};
340         if (!defined $node) {
341             error("would delete island: $island");
342             next;
343         }
344         my $wiarch= winode2arch($node);
345         if (!defined $wiarch) {
346             error("island has no arch: $island");
347             next;
348         }
349         my $dbarch= $dbisland2arch{$island};
350         if ($wiarch ne $dbarch) {
351             change("change archipelago from $dbarch to $wiarch".
352                    " for island $island");
353         }
354     }
355     foreach my $island (sort keys %wiisland2node) {
356         my $dbarch= $dbisland2arch{$island};
357         if (!defined $dbarch) {
358             my $wiarch= wiisland2arch($island);
359             if (!defined $wiarch) {
360                 error("new island has no arch: $island");
361                 next;
362                 # We check arches of non-new islands above
363             }
364             change("new island in $wiarch: $island");
365         }
366     }
367 }
368
369 sub compare_distances () {
370     foreach my $ia (sort keys %dbisland2arch) {
371         my $na= $wiisland2node{$ia};
372         next unless defined $na;
373         foreach my $ib (sort keys %dbisland2arch) {
374             next unless $ia le $ib; # do every pair only once
375             my $nb= $wiisland2node{$ib};
376             next unless defined $nb;
377             my $dbdist= $dbdists->get_edge_weight($ia,$ib);
378             my $widist= widist($na,$nb);
379             if (!defined $dbdist) {
380                 change(sprintf "define distance %2d for %s..%s",
381                        $widist, $ia,$ib);
382             } elsif ($dbdist != $widist) {
383                 change(sprintf "change distance %2d to %2d for %s..%s",
384                        $dbdist, $widist, $ia,$ib);
385             }
386         }
387     }
388 }
389
390 parse_info_serverside();
391
392 print "reading database\n";
393
394 db_setocean($ocean);
395 db_connect();
396 database_fetch_ocean();
397
398 print "reading yppedia chart\n";          yppedia_chart_parse();
399 print "adding shortcuts\n";               yppedia_graphs_add_shortcuts();
400 print "pruning bording vertices\n";       yppedia_graphs_prune_boring();
401 print "checking yppedia graphs\n";        yppedia_graphs_check();
402 print "setting archs from source-info\n"; yppedia_archs_sourceinfo();
403 print "computing shortest paths\n";       yppedia_graphs_shortest_paths();
404 print "setting archs from labels\n";      yppedia_archs_chart_labels();
405 print "setting archs from nearby\n";      yppedia_archs_fillbynearest();
406
407 print "comparing\n";
408
409 compare_island_lists();
410 compare_distances();
411
412 print_messages();