10 my $ocean= 'Midnight';
13 my $widists= Graph::Undirected->new();
14 my $wiarchs= Graph::Undirected->new();
25 my $dbdists= Graph::Undirected->new();
29 sub pmsg ($$) { push @{ $msgs{$_[0]} }, "$_[0]: $_[1]\n"; }
30 sub warning ($) { pmsg("warning",$_[0]); }
31 sub error ($) { pmsg("error", $_[0]); }
32 sub change ($) { pmsg("change", $_[0]); }
33 sub print_messages () {
34 foreach my $k (qw(change warning error)) {
37 print sort @$m or die $!;
40 sub progress ($) { print "($_[0])\n"; }
42 if (@ARGV && $ARGV[0] eq '--debug') {
44 open DEBUG, ">&STDOUT" or die $!;
47 open DEBUG, ">/dev/null" or die $!;
54 my $tp= (0+$x ^ 0+$y) & 1;
55 defined $parity or $parity=$tp;
56 $tp==$parity or warning("line $.: parity error $x,$y is $tp not $parity");
58 $winode2lines{$n}{$.}++;
62 sub yppedia_chart_parse () {
63 # We don't even bother with tag soup; instead we do line-oriented parsing.
67 s/^\s*//; chomp; s/\s+$//; s/\s+/ /g;
69 s/\{\{Chart\ style\|[^{}]*\}\}//g;
70 next unless m/\{\{/; # only interested in chart template stuff
72 my ($x,$y, $arch,$island,$solid,$dirn);
73 my $nn= sub { return nn_xy($x,$y) };
76 m/^\{\{ chart\ label \|(\d+)\|(\d+)\| .*
77 \'\[\[ [^][\']* \| (\S+)\ archipelago \]\]\'*\}\}$/xi) {
78 printf DEBUG "%2d,%-2d arch %s\n", $x,$y,$arch;
79 push @wiarchlabels, [ $x,$y,$arch ];
80 } elsif (($x,$y,$island) =
81 m/^\{\{ chart\ island\ icon \|(\d+)\|(\d+)\|
82 ([^| ][^|]*[^| ]) \| .*\}\}$/xi) {
84 $wiisland2node{$island}= $n;
85 $winode2island{$n}= $island;
86 $widists->add_vertex($n);
87 $wiarchs->add_vertex($n);
88 printf DEBUG "%2d,%-2d island %s\n", $x,$y,$island;
89 } elsif (($solid,$x,$y,$dirn) =
90 m/^\{\{ chart\ league((?:\ solid)?) \|(\d+)\|(\d+)\|
91 ([-\/\\o]) \| .*\}\}$/xi) {
94 my ($bx,$by) = ($x,$y);
95 if ($dirn eq '-') { $bx+=2; }
96 elsif ($dirn eq '\\') { $bx++; $by++; }
97 elsif ($dirn eq '/') { $x++; $by++; }
100 my $nb= nn_xy($bx,$by);
101 $widists->add_weighted_edge($nn->(), $nb, 1);
102 $wiarchs->add_edge($nn->(), $nb) if $solid;
103 $wiarchs->add_edge($nn->(), $nb) if $solid;
105 printf DEBUG "%2d,%-2d league %-6s %s %s\n", $x,$y,
106 $solid?'solid':'dotted', $dirn, $nb;
108 m/^\{\{ chart\ head \}\}$/xi
112 warning("line $.: ignoring incomprehensible: $_");
117 sub database_fetch_ocean () {
119 $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
121 while ($row= $sth->fetchrow_hashref) {
122 print DEBUG "database-island $row->{'islandname'}".
123 " $row->{'archipelago'}\n";
124 $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
126 $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
128 JOIN islands AS a ON dists.aiid==a.islandid
129 JOIN islands AS b ON dists.biid==b.islandid');
131 while ($row= $sth->fetchrow_hashref) {
132 $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
136 sub database_graph_spr () {
137 $dbspr= shortest_path_reduction('db',$dbdists);
140 sub yppedia_graphs_add_shortcuts () {
141 # We add edges between LPs we know about, as you can chart
142 # between them. Yppedia often lacks these edges.
144 foreach my $p ($widists->vertices) {
145 my ($ax,$ay) = $p =~ m/^(\d+)\,(\d+)$/ or die;
146 my $add_shortcut= sub {
147 my $q= sprintf "%d,%d", $ax+$_[0], $ay+$_[1];
148 return unless $widists->has_vertex($q);
149 return if $widists->has_edge($p,$q);
150 printf DEBUG "%-5s league-shortcut %-5s\n", $p, $q;
151 $widists->add_weighted_edge($p,$q,1);
153 $add_shortcut->( 2,0);
154 $add_shortcut->(+1,1);
155 $add_shortcut->(-1,1);
159 sub yppedia_graphs_prune_boring () {
160 # Prune the LP database by eliminating boring intermediate vertices
161 foreach my $delete ($widists->vertices()) {
162 next if exists $winode2island{$delete};
163 my @neigh= $widists->neighbours($delete);
164 next unless @neigh==2;
166 map { $weight += $widists->get_edge_weight($delete, $_) } @neigh;
167 $widists->add_weighted_edge(@neigh, $weight);
168 $widists->delete_vertex($delete);
169 printf DEBUG "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
173 sub yppedia_graphs_check () {
174 # Check that it's connected.
175 foreach my $cc ($widists->connected_components()) {
176 next if 2*@$cc > $widists->vertices();
177 my $m= "disconnected league point(s):";
178 foreach my $n (@$cc) {
179 $m .= "\n LP $n, def. yppedia line(s): ".
180 join(',', sort keys %{ $winode2lines{$n} });
186 sub yppedia_archs_sourceinfo () {
187 # Assign archipelagoes according to the source-info file
188 foreach my $arch (sort keys %{ $oceans{$ocean} }) {
189 foreach my $islename (sort keys %{ $oceans{$ocean}{$arch} }) {
190 my $islenode= $wiisland2node{$islename};
191 if (!defined $islenode) {
192 error("island $islename in source-info but not in WP map");
195 my $ccix= $wiarchs->connected_component_by_vertex($islenode);
196 my $oldarch= $wiccix2arch{$ccix};
197 error("island in $arch in source-info".
198 " connected to $oldarch as well: $islename")
199 if defined $oldarch && $oldarch ne $arch;
200 printf DEBUG "%-5s force-island-arch cc%-2d %-10s %s\n",
201 $islenode, $ccix, $arch, $islename;
202 $wiccix2arch{$ccix}= $arch;
207 sub yppedia_archs_chart_labels () {
208 # Assign archipelago labels to groups of islands
210 foreach my $label (@wiarchlabels) {
211 my ($ax,$ay,$arch) = @$label;
212 my $best_ccmulti= -1;
215 # print DEBUG "$ax,$ay arch-island-search $arch\n";
216 $ay += 1; $ax += 2; # coords are rather to the top left of label
217 foreach my $vertex ($wiarchs->vertices()) {
218 next unless exists $winode2island{$vertex};
219 my $ccix= $wiarchs->connected_component_by_vertex($vertex);
220 my @cc= $wiarchs->connected_component_by_index($ccix);
221 my $ccmulti= @cc > 1;
222 my ($vx,$vy) = split /,/, $vertex;
223 my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
224 my $cmp= $ccmulti <=> $best_ccmulti
226 printf DEBUG "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
227 " #cc=%2d ccmulti=%d cmp=%2d %s\n",
228 $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $ccmulti, $cmp,
229 $winode2island{$vertex};
230 next unless $cmp > 0;
233 $best_ccmulti= $ccmulti;
235 die 'no island vertices?!' unless defined $best_n;
236 my $ccix= $wiarchs->connected_component_by_vertex($best_n);
238 "%2d,%-2d arch-island-select %-5s d2=%4d cc%-2d %-10s %s\n",
239 $ax,$ay, $best_n, $ccix, $best_d2, $arch, $winode2island{$best_n};
240 my $desc= join "\n", map {
241 my $in= $winode2island{$_};
242 " LP $_". (defined $in ? ", $in" : "");
243 } sort $wiarchs->connected_component_by_index($ccix);
245 if (exists $wiccix2arch{$ccix} and $wiccix2arch{$ccix} ne $arch) {
246 error("archipelago determination failed, wrongly merged:\n".
247 " archipelago $arch\n".
248 " archipelago $wiccix2arch{$ccix}\n".
252 $wiccix2arch{$ccix}= $arch;
253 # print "$ccix $arch ::\n$desc\n";
257 sub yppedia_archs_fillbynearest() {
258 # Assign islands not labelled above to archipelagoes.
260 # We do this by, for each connected component (set of islands
261 # linked by purchaseable charts), searching for the nearest other
262 # connected component which has already been assigned an arch.
263 # `Nearest' means shortest distance of unpurchaseable charts, in
266 # we need only consider vertices which weren't `boring intermediate
267 # vertices' (removed during optimisation as being of order 2)
268 my @ccs_useful= map {
269 [ grep { $widists->has_vertex($_) } @$_ ]
270 } $wiarchs->connected_components();
274 foreach my $sourceccix (0..$#ccs_useful) {
275 next if defined $wiccix2arch{$sourceccix};
276 next unless $ccs_useful[$sourceccix];
278 my @sourcecc= $wiarchs->connected_component_by_index($sourceccix);
279 my @islandnodes= grep { $winode2island{$_} } @sourcecc;
280 next unless @islandnodes; # don't care, then
282 foreach my $islandnode (@islandnodes) {
283 printf DEBUG "%-5s arch-join-need cc%-2d %s\n",
284 $islandnode, $sourceccix, $winode2island{$islandnode};
286 my $best_dist= 9999999;
287 my ($best_target, $best_targetccix, $best_source);
288 foreach my $targetccix (0..$#ccs_useful) {
289 next unless defined $wiccix2arch{$targetccix}; # not helpful
290 next unless $ccs_useful[$targetccix];
291 foreach my $target ($wiarchs->
292 connected_component_by_index($targetccix)) {
293 next unless $widists->has_vertex($target);
294 foreach my $source (@sourcecc) {
295 my $target_dist= widist($target,$source);
296 next unless defined $target_dist;
297 next if $target_dist >= $best_dist;
298 $best_dist= $target_dist;
299 $best_source= $source;
300 $best_target= $target;
301 $best_targetccix= $targetccix;
305 die "no possible target ?!" unless defined $best_target;
307 my $arch= $wiccix2arch{$best_targetccix};
308 my $best_island= $winode2island{$best_target};
309 printf DEBUG "%-5s arch-join-to %-5s dist=%2d cc%-2d %-10s %s\n",
310 $best_source, $best_target, $best_dist,
311 $best_targetccix, $arch,
312 defined($best_island) ? $best_island : "-";
314 push @assignments, [ $sourceccix, $arch ];
316 foreach my $assign (@assignments) {
317 $wiccix2arch{$assign->[0]}= $assign->[1];
321 sub yppedia_graph_shortest_paths () {
322 $wialldists= $widists->APSP_Floyd_Warshall();
327 my $pl= $wialldists->path_length($p,$q);
328 # die "$p $q" unless defined $pl;
329 # my @pv= $wialldists->path_vertices($p,$q);
330 # if (@pv == $pl) { return $pl; }
331 # printf DEBUG "%-5s PATHLENGTH %-5s pl=%s pv=%s\n", $p,$q,$pl,join('|',@pv);
335 sub winode2arch ($) {
337 my $ccix= $wiarchs->connected_component_by_vertex($node);
338 return $wiccix2arch{$ccix};
340 sub wiisland2arch ($) {
342 my $node= $wiisland2node{$island};
343 die "$island ?" unless defined $node;
344 return winode2arch($node);
347 sub compare_island_lists () {
348 foreach my $island (sort keys %dbisland2arch) {
349 my $node= $wiisland2node{$island};
350 if (!defined $node) {
351 error("would delete island: $island");
354 my $wiarch= winode2arch($node);
355 if (!defined $wiarch) {
356 error("island has no arch: $island");
359 my $dbarch= $dbisland2arch{$island};
360 if ($wiarch ne $dbarch) {
361 change("archipelago change from $dbarch to $wiarch".
362 " for island $island");
365 foreach my $island (sort keys %wiisland2node) {
366 my $wtarch= $wtisland2arch{$island};
367 my $wiarch= wiisland2arch($island);
368 if (!defined $wtarch) {
369 error("island from chart not found on ocean page: $island");
370 } elsif (defined $wiarch and $wtarch ne $wiarch) {
371 error("island in $wtarch on ocean page but".
372 " concluded $wiarch from chart: $island");
375 my $dbarch= $dbisland2arch{$island};
376 if (!defined $dbarch) {
377 my $wiarch= wiisland2arch($island);
378 if (!defined $wiarch) {
379 error("new island has no arch: $island");
381 # We check arches of non-new islands above
383 change("island new in $wiarch: $island");
386 foreach my $island (sort keys %wtisland2arch) {
387 my $node= $wiisland2node{$island};
388 next if defined $node;
389 error("island on ocean page but not in chart: $island");
393 sub shortest_path_reduction ($$) {
396 # Takes a graph $g (and a string for messages $what) and returns
397 # a new graph which is the miminal shortest path transient reduction
400 # We also check that the shortest path closure of the intended result
401 # is the same graph as the input. Thus the input must itself be
402 # a shortest path closure; if it isn't, we die.
404 my $proof=<<'END'; # way to make a big comment
406 Premises and definitions:
408 1. F is an undirected weighted graph with positive edge weights.
410 2. All graphs we will consider have the same vertices as F.
412 3. G = Closure(F) is the graph of cliques whose edge weights
413 are the shortest paths in F, one clique for each connected
416 3a. |XY| for vertices X, Y is the weight of the edge XY in G.
417 If XY is not in G, |XY| is infinite.
419 4. A `reduction' of G is a subgraph K of G such that Closure(K) = G.
420 The reduction is `minimal' if there is no strict subgraph K'
421 of K such that Closure(K') = G.
423 5. Now each edge of G may be:
424 - `unnecessary': included in no minimal reductions of G.
425 - `essential': included in all minimal reductions of G.
426 - `contingent': included in some but not all.
428 6. Consider for any edge AC between the vertices A and C,
429 whether there is any B such that |AB|+|BC| = |AC| ?
430 (There can be no B such that the sum < |AC| since that would
431 mean that |AC| wasn't equal to the shortest path length.)
433 6a. No such B: AC is therefore the only shortest path from A to C
434 (since G is not a multigraph). AC is thus an essential edge.
436 6b. Some such B: Call all such edges AC `questionable'.
438 6c. Thus all edges are essential or questionable.
440 7. Suppose AC is a shortest contingent edge. AC must be
441 questionable since it is not essential. Suppose it is
442 made questionable by the existence of B such that |AB|+|BC| =
443 |AC|. Consider AB and BC. Since |AB| and |BC| are positive,
444 |BC| and |AB| must be < |AC| ie AB and BC are shorter than AC.
445 Since AC is a shortest contingent edge, there must be shortest
446 paths in G for AB and BC consisting entirely of essential edges.
448 8. Therefore it is always safe to remove AC since the paths
449 A..B and B..C will definitely still remain and provide a path
450 A..B..C with length |AB|+|BC| = |AC|.
452 9. Thus AC is unnecessary, contradicting the assumption in 7.
453 There are therefore no shortest contingent edges, and
454 thus no contingent edges.
456 10. We can construct a minimal reduction directly: for each edge
457 AC in G, search for a vertex B such that |AB|+|BC| = |AC|.
458 If we find none, AC is essential. If we find one then AC is
459 not essential and is therefore unnecessary.
463 printf DEBUG "spr %s before %d\n", $what, scalar($g->edges());
465 my $result= Graph::Undirected->new();
466 foreach my $edge_ac ($g->edges()) {
467 my $edgename_ac= join ' .. ', @$edge_ac;
468 printf DEBUG "spr %s edge %s\n", $what, $edgename_ac;
469 my $w_ac= $g->get_edge_weight(@$edge_ac);
471 foreach my $vertex_b ($g->vertices()) {
472 next if grep { $_ eq $vertex_b } @$edge_ac;
473 my $w_ab= $g->get_edge_weight($edge_ac->[0], $vertex_b);
474 next unless defined $w_ab;
475 next if $w_ab >= $w_ac;
476 my $w_bc= $g->get_edge_weight($vertex_b, $edge_ac->[1]);
477 next unless defined $w_ac;
478 next if $w_ab + $w_bc > $w_ac;
480 printf DEBUG "spr %s edge %s unnecessary %s\n",
481 $what, $edgename_ac, $vertex_b;
486 printf DEBUG "spr %s edge %s essential\n", $what, $edgename_ac;
487 $result->add_weighted_edge(@$edge_ac,$w_ac);
490 printf DEBUG "spr %s result %d\n", $what, scalar($result->edges());
492 my $apsp= $result->APSP_Floyd_Warshall();
493 foreach my $ia (sort $g->vertices()) {
494 foreach my $ib (sort $g->vertices()) {
495 my $din= $g->get_edge_weight($ia,$ib);
496 my $dout= $apsp->path_length($ia,$ib);
497 $din= defined($din) ? $din : 'infinity';
498 $dout= defined($dout) ? $dout : 'infinity';
499 error("$what spr apsp discrepancy in=$din out=$dout".
507 sub yppedia_graph_spr () {
508 my $base= Graph::Undirected->new();
509 foreach my $na (sort keys %winode2island) {
510 my $ia= $winode2island{$na};
511 foreach my $nb (sort keys %winode2island) {
512 my $ib= $winode2island{$nb};
513 $base->add_weighted_edge($ia,$ib, widist($na,$nb));
516 $wispr= shortest_path_reduction('wi',$base);
519 sub yppedia_ocean_fetch () {
520 open OCEAN, '-|', "./yppedia-ocean-scraper", $ocean or die $!;
527 die unless defined $arch;
528 $wtisland2arch{$'}= $arch;
535 $?=0; $!=0; close OCEAN; $? and die $?; $! and die $!;
538 sub compare_distances () {
539 foreach my $ia (sort keys %dbisland2arch) {
540 my $na= $wiisland2node{$ia};
541 next unless defined $na;
542 foreach my $ib (sort keys %dbisland2arch) {
543 next unless $ia le $ib; # do every pair only once
544 my $dbdist= $dbspr->get_edge_weight($ia,$ib);
545 my $widist= $wispr->get_edge_weight($ia,$ib);
546 next unless defined $dbdist || defined $widist;
548 if (!defined $widist) {
549 warning(sprintf "route delete %2d for %s .. %s",
551 } elsif (!defined $dbdist) {
552 change(sprintf "route new %2d for %s .. %s",
554 } elsif ($dbdist != $widist) {
555 change(sprintf "route change %2d to %2d for %s .. %s",
556 $dbdist, $widist, $ia,$ib);
562 parse_info_serverside();
564 progress("reading database");
568 database_fetch_ocean();
570 progress("computing database spr"); database_graph_spr();
572 progress("reading yppedia chart"); yppedia_chart_parse();
573 progress("adding shortcuts"); yppedia_graphs_add_shortcuts();
574 progress("pruning boring vertices"); yppedia_graphs_prune_boring();
575 progress("checking yppedia graphs"); yppedia_graphs_check();
576 progress("setting archs from source-info"); yppedia_archs_sourceinfo();
577 progress("computing shortest paths"); yppedia_graph_shortest_paths();
578 progress("setting archs from labels"); yppedia_archs_chart_labels();
579 progress("setting archs from nearby"); yppedia_archs_fillbynearest();
580 progress("computing yppedia spr"); yppedia_graph_spr();
582 progress("fetching yppedia ocean text"); yppedia_ocean_fetch();
584 progress("comparing islands"); compare_island_lists();
585 progress("comparing distances"); compare_distances();