6 # usage: ./yppedia-chart-parser <Oceanname>
7 # updates OCEAN-Oceanname.db and _ocean-<oceanname>.txt
8 # from YPPedia (chart and ocean page) and source-info.txt
10 # This is part of ypp-sc-tools, a set of third-party tools for assisting
11 # players of Yohoho Puzzle Pirates.
13 # Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
28 # Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
29 # are used without permission. This program is not endorsed or
30 # sponsored by Three Rings.
32 use strict (qw(vars));
35 use Graph::Undirected;
39 my $widists= Graph::Undirected->new();
40 my $wiarchs= Graph::Undirected->new();
54 my @msgkinds= qw(change warning error);
58 sub pmsg ($$) { push @{ $msgs{$_[0]} }, "$_[0]: $_[1]\n"; }
59 sub warning ($) { pmsg("warning",$_[0]); }
60 sub error ($) { pmsg("error", $_[0]); }
61 sub change ($) { pmsg("change", $_[0]); }
62 sub print_messages () {
63 foreach my $k (@msgkinds) {
66 foreach my $m (sort @$ms) {
67 next if $msgprinted{$m};
70 $msgkindprinted{$k}++;
74 sub progress ($) { print "($_[0])\n"; }
76 if (@ARGV && $ARGV[0] eq '--debug') {
78 open DEBUG, ">&STDOUT" or die $!;
81 open DEBUG, ">/dev/null" or die $!;
86 $ARGV[0] =~ m/^\-/ and die;
87 my $ocean= shift @ARGV;
93 my $tp= (0+$x ^ 0+$y) & 1;
94 defined $parity or $parity=$tp;
95 $tp==$parity or warning("line $.: parity error $x,$y is $tp not $parity");
97 $winode2lines{$n}{$.}++;
101 sub yppedia_chart_parse () {
102 # We don't even bother with tag soup; instead we do line-oriented parsing.
106 s/^\s*//; chomp; s/\s+$//; s/\s+/ /g;
107 s/\<\/?(?:b|em)\>//g;
108 s/\{\{Chart\ style\|[^{}]*\}\}//g;
109 next unless m/\{\{/; # only interested in chart template stuff
111 my ($x,$y, $arch,$island,$solid,$dirn);
112 my $nn= sub { return nn_xy($x,$y) };
115 m/^\{\{ chart\ label \|(\d+)\|(\d+)\| .*
116 \'\[\[ [^][\']* \| (\S+)\ archipelago \]\]\'*\}\}$/xi) {
117 printf DEBUG "%2d,%-2d arch %s\n", $x,$y,$arch;
118 push @wiarchlabels, [ $x,$y,$arch ];
119 } elsif (($x,$y,$island) =
120 m/^\{\{ chart\ island\ icon \|(\d+)\|(\d+)\|
121 ([^| ][^|]*[^| ]) \| .*\}\}$/xi) {
123 $wiisland2node{$island}= $n;
124 $winode2island{$n}= $island;
125 $widists->add_vertex($n);
126 $wiarchs->add_vertex($n);
127 printf DEBUG "%2d,%-2d island %s\n", $x,$y,$island;
128 } elsif (($solid,$x,$y,$dirn) =
129 m/^\{\{ chart\ league((?:\ solid)?) \|(\d+)\|(\d+)\|
130 ([-\/\\o]) \| .*\}\}$/xi) {
131 next if $dirn eq 'o';
133 my ($bx,$by) = ($x,$y);
134 if ($dirn eq '-') { $bx+=2; }
135 elsif ($dirn eq '\\') { $bx++; $by++; }
136 elsif ($dirn eq '/') { $x++; $by++; }
139 my $nb= nn_xy($bx,$by);
140 $widists->add_weighted_edge($nn->(), $nb, 1);
141 $wiarchs->add_edge($nn->(), $nb) if $solid;
142 $wiarchs->add_edge($nn->(), $nb) if $solid;
144 printf DEBUG "%2d,%-2d league %-6s %s %s\n", $x,$y,
145 $solid?'solid':'dotted', $dirn, $nb;
147 m/^\{\{ chart\ head \}\}$/xi
151 warning("line $.: ignoring incomprehensible: $_");
156 sub yppedia_graphs_add_shortcuts () {
157 # We add edges between LPs we know about, as you can chart
158 # between them. Yppedia often lacks these edges.
160 foreach my $p ($widists->vertices) {
161 my ($ax,$ay) = $p =~ m/^(\d+)\,(\d+)$/ or die;
162 my $add_shortcut= sub {
163 my $q= sprintf "%d,%d", $ax+$_[0], $ay+$_[1];
164 return unless $widists->has_vertex($q);
165 return if $widists->has_edge($p,$q);
166 printf DEBUG "%-5s league-shortcut %-5s\n", $p, $q;
167 $widists->add_weighted_edge($p,$q,1);
169 $add_shortcut->( 2,0);
170 $add_shortcut->(+1,1);
171 $add_shortcut->(-1,1);
175 sub yppedia_graphs_prune_boring () {
176 # Prune the LP database by eliminating boring intermediate vertices
177 foreach my $delete ($widists->vertices()) {
178 next if exists $winode2island{$delete};
179 my @neigh= $widists->neighbours($delete);
180 next unless @neigh==2;
182 map { $weight += $widists->get_edge_weight($delete, $_) } @neigh;
183 $widists->add_weighted_edge(@neigh, $weight);
184 $widists->delete_vertex($delete);
185 printf DEBUG "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
189 sub yppedia_graphs_check () {
190 # Check that it's connected.
191 foreach my $cc ($widists->connected_components()) {
192 next if 2*@$cc > $widists->vertices();
193 my $m= "disconnected league point(s):";
194 foreach my $n (@$cc) {
195 $m .= "\n LP $n, def. yppedia line(s): ".
196 join(',', sort keys %{ $winode2lines{$n} });
202 sub yppedia_archs_sourceinfo () {
203 # Assign archipelagoes according to the source-info file
204 foreach my $arch (sort keys %{ $oceans{$ocean} }) {
205 foreach my $islename (sort keys %{ $oceans{$ocean}{$arch} }) {
206 my $islenode= $wiisland2node{$islename};
207 if (!defined $islenode) {
208 error("island $islename in source-info but not in WP map");
211 my $ccix= $wiarchs->connected_component_by_vertex($islenode);
212 my $oldarch= $wiccix2arch{$ccix};
213 error("island in $arch in source-info".
214 " connected to $oldarch as well: $islename")
215 if defined $oldarch && $oldarch ne $arch;
216 printf DEBUG "%-5s force-island-arch cc%-2d %-10s %s\n",
217 $islenode, $ccix, $arch, $islename;
218 $wiccix2arch{$ccix}= $arch;
223 sub yppedia_archs_chart_labels () {
224 # Assign archipelago labels to groups of islands
226 foreach my $label (@wiarchlabels) {
227 my ($ax,$ay,$arch) = @$label;
228 my $best_ccmulti= -1;
231 # print DEBUG "$ax,$ay arch-island-search $arch\n";
232 $ay += 1; $ax += 2; # coords are rather to the top left of label
233 foreach my $vertex ($wiarchs->vertices()) {
234 next unless exists $winode2island{$vertex};
235 my $ccix= $wiarchs->connected_component_by_vertex($vertex);
236 my @cc= $wiarchs->connected_component_by_index($ccix);
237 my $ccmulti= @cc > 1;
238 my ($vx,$vy) = split /,/, $vertex;
239 my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
240 my $cmp= $ccmulti <=> $best_ccmulti
242 printf DEBUG "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
243 " #cc=%2d ccmulti=%d cmp=%2d %s\n",
244 $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $ccmulti, $cmp,
245 $winode2island{$vertex};
246 next unless $cmp > 0;
249 $best_ccmulti= $ccmulti;
251 die 'no island vertices?!' unless defined $best_n;
252 my $ccix= $wiarchs->connected_component_by_vertex($best_n);
254 "%2d,%-2d arch-island-select %-5s d2=%4d cc%-2d %-10s %s\n",
255 $ax,$ay, $best_n, $ccix, $best_d2, $arch, $winode2island{$best_n};
256 my $desc= join "\n", map {
257 my $in= $winode2island{$_};
258 " LP $_". (defined $in ? ", $in" : "");
259 } sort $wiarchs->connected_component_by_index($ccix);
261 if (exists $wiccix2arch{$ccix} and $wiccix2arch{$ccix} ne $arch) {
262 error("archipelago determination failed, wrongly merged:\n".
263 " archipelago $arch\n".
264 " archipelago $wiccix2arch{$ccix}\n".
268 $wiccix2arch{$ccix}= $arch;
269 # print "$ccix $arch ::\n$desc\n";
273 sub yppedia_archs_fillbynearest() {
274 # Assign islands not labelled above to archipelagoes.
276 # We do this by, for each connected component (set of islands
277 # linked by purchaseable charts), searching for the nearest other
278 # connected component which has already been assigned an arch.
279 # `Nearest' means shortest distance of unpurchaseable charts, in
282 # we need only consider vertices which weren't `boring intermediate
283 # vertices' (removed during optimisation as being of order 2)
284 my @ccs_useful= map {
285 [ grep { $widists->has_vertex($_) } @$_ ]
286 } $wiarchs->connected_components();
290 foreach my $sourceccix (0..$#ccs_useful) {
291 next if defined $wiccix2arch{$sourceccix};
292 next unless $ccs_useful[$sourceccix];
294 my @sourcecc= $wiarchs->connected_component_by_index($sourceccix);
295 my @islandnodes= grep { $winode2island{$_} } @sourcecc;
296 next unless @islandnodes; # don't care, then
298 foreach my $islandnode (@islandnodes) {
299 printf DEBUG "%-5s arch-join-need cc%-2d %s\n",
300 $islandnode, $sourceccix, $winode2island{$islandnode};
302 my $best_dist= 9999999;
303 my ($best_target, $best_targetccix, $best_source);
304 foreach my $targetccix (0..$#ccs_useful) {
305 next unless defined $wiccix2arch{$targetccix}; # not helpful
306 next unless $ccs_useful[$targetccix];
307 foreach my $target ($wiarchs->
308 connected_component_by_index($targetccix)) {
309 next unless $widists->has_vertex($target);
310 foreach my $source (@sourcecc) {
311 my $target_dist= widist($target,$source);
312 next unless defined $target_dist;
313 next if $target_dist >= $best_dist;
314 $best_dist= $target_dist;
315 $best_source= $source;
316 $best_target= $target;
317 $best_targetccix= $targetccix;
321 die "no possible target ?!" unless defined $best_target;
323 my $arch= $wiccix2arch{$best_targetccix};
324 my $best_island= $winode2island{$best_target};
325 printf DEBUG "%-5s arch-join-to %-5s dist=%2d cc%-2d %-10s %s\n",
326 $best_source, $best_target, $best_dist,
327 $best_targetccix, $arch,
328 defined($best_island) ? $best_island : "-";
330 push @assignments, [ $sourceccix, $arch ];
332 foreach my $assign (@assignments) {
333 $wiccix2arch{$assign->[0]}= $assign->[1];
337 sub yppedia_graph_shortest_paths () {
338 $wialldists= $widists->APSP_Floyd_Warshall();
343 my $pl= $wialldists->path_length($p,$q);
344 # die "$p $q" unless defined $pl;
345 # my @pv= $wialldists->path_vertices($p,$q);
346 # if (@pv == $pl) { return $pl; }
347 # printf DEBUG "%-5s PATHLENGTH %-5s pl=%s pv=%s\n", $p,$q,$pl,join('|',@pv);
351 sub winode2arch ($) {
353 my $ccix= $wiarchs->connected_component_by_vertex($node);
354 return $wiccix2arch{$ccix};
356 sub wiisland2arch ($) {
358 my $node= $wiisland2node{$island};
359 die "$island ?" unless defined $node;
360 return winode2arch($node);
363 sub compare_island_lists () {
364 foreach my $island (sort keys %dbisland2arch) {
365 my $node= $wiisland2node{$island};
366 if (!defined $node) {
367 error("would delete island: $island");
370 my $wiarch= winode2arch($node);
371 if (!defined $wiarch) {
372 error("island has no arch: $island");
375 my $dbarch= $dbisland2arch{$island};
376 if ($wiarch ne $dbarch) {
377 change("archipelago change from $dbarch to $wiarch".
378 " for island $island");
381 foreach my $island (sort keys %wiisland2node) {
382 my $wtarch= $wtisland2arch{$island};
383 my $wiarch= wiisland2arch($island);
384 if (!defined $wtarch) {
385 error("island from chart not found on ocean page: $island");
386 } elsif (defined $wiarch and $wtarch ne $wiarch) {
387 error("island in $wtarch on ocean page but".
388 " concluded $wiarch from chart: $island");
391 my $dbarch= $dbisland2arch{$island};
392 if (!defined $dbarch) {
393 my $wiarch= wiisland2arch($island);
394 if (!defined $wiarch) {
395 error("new island has no arch: $island");
397 # We check arches of non-new islands above
399 change("island new in $wiarch: $island");
402 foreach my $island (sort keys %wtisland2arch) {
403 my $node= $wiisland2node{$island};
404 next if defined $node;
405 error("island on ocean page but not in chart: $island");
409 sub shortest_path_reduction ($$) {
412 # Takes a graph $g (and a string for messages $what) and returns
413 # a new graph which is the miminal shortest path transient reduction
416 # We also check that the shortest path closure of the intended result
417 # is the same graph as the input. Thus the input must itself be
418 # a shortest path closure; if it isn't, we die.
420 my $proof=<<'END'; # way to make a big comment
422 Premises and definitions:
424 1. F is an undirected weighted graph with positive edge weights.
426 2. All graphs we will consider have the same vertices as F
427 and none have self-edges.
429 3. G = Closure(F) is the graph of cliques whose edge weights
430 are the shortest paths in F, one clique for each connected
433 3a. |XY| for vertices X, Y is the weight of the edge XY in G.
434 If XY is not in G, |XY| is infinite.
436 4. A `reduction' of G is a subgraph K of G such that Closure(K) = G.
437 The reduction is `minimal' if there is no strict subgraph K'
438 of K such that Closure(K') = G.
440 5. Now each edge of G may be:
441 - `unnecessary': included in no minimal reductions of G.
442 - `essential': included in all minimal reductions of G.
443 - `contingent': included in some but not all.
445 6. Consider for any edge AC between the vertices A and C,
446 whether there is any B such that |AB|+|BC| = |AC| ?
447 (There can be no B such that the sum < |AC| since that would
448 mean that |AC| wasn't equal to the shortest path length.)
450 6a. No such B: AC is therefore the only shortest path from A to C
451 (since G is not a multigraph). AC is thus an essential edge.
453 6b. Some such B: Call all such edges AC `questionable'.
455 6c. Thus all edges are essential or questionable.
457 7. Suppose AC is a shortest contingent edge. AC must be
458 questionable since it is not essential. Suppose it is
459 made questionable by the existence of B such that |AB|+|BC| =
460 |AC|. Consider AB and BC. Since |AB| and |BC| are positive,
461 |BC| and |AB| must be < |AC| ie AB and BC are shorter than AC.
462 Since AC is a shortest contingent edge, there must be shortest
463 paths in G for AB and BC consisting entirely of essential edges.
465 8. Therefore it is always safe to remove AC since the paths
466 A..B and B..C will definitely still remain and provide a path
467 A..B..C with length |AB|+|BC| = |AC|.
469 9. Thus AC is unnecessary, contradicting the assumption in 7.
470 There are therefore no shortest contingent edges, and
471 thus no contingent edges.
473 10. We can construct a minimal reduction directly: for each edge
474 AC in G, search for a vertex B such that |AB|+|BC| = |AC|.
475 If we find none, AC is essential. If we find one then AC is
476 not essential and is therefore unnecessary.
480 printf DEBUG "spr %s before %d\n", $what, scalar($g->edges());
482 my $result= Graph::Undirected->new();
483 foreach my $edge_ac ($g->edges()) {
484 next if $edge_ac->[0] eq $edge_ac->[1];
485 my $edgename_ac= join ' .. ', @$edge_ac;
486 printf DEBUG "spr %s edge %s\n", $what, $edgename_ac;
487 my $w_ac= $g->get_edge_weight(@$edge_ac);
489 foreach my $vertex_b ($g->vertices()) {
490 next if grep { $_ eq $vertex_b } @$edge_ac;
491 my $w_ab= $g->get_edge_weight($edge_ac->[0], $vertex_b);
492 next unless defined $w_ab;
493 next if $w_ab >= $w_ac;
494 my $w_bc= $g->get_edge_weight($vertex_b, $edge_ac->[1]);
495 next unless defined $w_ac;
496 next if $w_ab + $w_bc > $w_ac;
498 printf DEBUG "spr %s edge %s unnecessary %s\n",
499 $what, $edgename_ac, $vertex_b;
504 printf DEBUG "spr %s edge %s essential\n", $what, $edgename_ac;
505 $result->add_weighted_edge(@$edge_ac,$w_ac);
508 printf DEBUG "spr %s result %d\n", $what, scalar($result->edges());
510 my $apsp= $result->APSP_Floyd_Warshall();
511 foreach my $ia (sort $g->vertices()) {
512 foreach my $ib (sort $g->vertices()) {
513 my $din= $g->get_edge_weight($ia,$ib);
514 my $dout= $apsp->path_length($ia,$ib);
515 $din= defined($din) ? $din : 'infinity';
516 $dout= defined($dout) ? $dout : 'infinity';
517 error("$what spr apsp discrepancy in=$din out=$dout".
525 sub yppedia_graph_spr () {
526 my $base= Graph::Undirected->new();
527 foreach my $na (sort keys %winode2island) {
528 my $ia= $winode2island{$na};
529 foreach my $nb (sort keys %winode2island) {
530 my $ib= $winode2island{$nb};
531 $base->add_weighted_edge($ia,$ib, widist($na,$nb));
534 $wispr= shortest_path_reduction('wi',$base);
537 sub yppedia_ocean_fetch_start ($) {
540 push @args, '--chart' if $chart;
542 open OCEAN, '-|', "./yppedia-ocean-scraper", @args or die $!;
544 sub yppedia_ocean_fetch_done () {
545 $?=0; $!=0; close OCEAN; $? and die $?; $! and die $!;
548 sub yppedia_ocean_fetch_chart () {
549 yppedia_ocean_fetch_start(1);
550 yppedia_chart_parse();
551 yppedia_ocean_fetch_done();
554 sub yppedia_ocean_fetch_text () {
555 yppedia_ocean_fetch_start(0);
562 die unless defined $arch;
563 $wtisland2arch{$'}= $arch;
570 yppedia_ocean_fetch_done();
573 sub compare_distances () {
574 foreach my $ia (sort keys %dbisland2arch) {
575 my $na= $wiisland2node{$ia};
576 next unless defined $na;
577 foreach my $ib (sort keys %dbisland2arch) {
578 next unless $ia le $ib; # do every pair only once
579 my $dbdist= $dbspr->get_edge_weight($ia,$ib);
580 my $widist= $wispr->get_edge_weight($ia,$ib);
581 next unless defined $dbdist || defined $widist;
583 if (!defined $widist) {
584 warning(sprintf "route delete %2d for %s .. %s",
586 } elsif (!defined $dbdist) {
587 change(sprintf "route new %2d for %s .. %s",
589 } elsif ($dbdist != $widist) {
590 change(sprintf "route change %2d to %2d for %s .. %s",
591 $dbdist, $widist, $ia,$ib);
597 #========== database handling ==========
599 sub database_fetch_ocean () {
601 $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
603 undef %dbisland2arch;
604 $dbdists= Graph::Undirected->new();
605 while ($row= $sth->fetchrow_hashref) {
606 print DEBUG "database-island $row->{'islandname'}".
607 " $row->{'archipelago'}\n";
608 $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
610 $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
612 JOIN islands AS a ON dists.aiid==a.islandid
613 JOIN islands AS b ON dists.biid==b.islandid');
615 while ($row= $sth->fetchrow_hashref) {
616 $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
620 sub database_graph_spr () {
621 $dbspr= shortest_path_reduction('db',$dbdists);
624 sub database_do_updates () {
625 my $addisland= $dbh->prepare(<<'END')
626 INSERT OR IGNORE INTO islands (islandname, archipelago) VALUES (?, ?);
629 foreach my $island (sort keys %wiisland2node) {
630 my $wiarch= wiisland2arch($island);
631 $addisland->execute($island, $wiarch);
639 my $adddist= $dbh->prepare(<<'END')
640 INSERT INTO dists VALUES
641 ((SELECT islandid FROM islands WHERE islandname == ?),
642 (SELECT islandid FROM islands WHERE islandname == ?),
646 my $addroute= $dbh->prepare(<<'END')
647 INSERT INTO routes VALUES
648 ((SELECT islandid FROM islands WHERE islandname == ?),
649 (SELECT islandid FROM islands WHERE islandname == ?),
653 foreach my $ia (sort keys %wiisland2node) {
654 my $na= $wiisland2node{$ia};
655 foreach my $ib (sort keys %wiisland2node) {
656 my $nb= $wiisland2node{$ib};
657 my $apdist= $ia eq $ib ? 0 : widist($na,$nb);
658 die "$ia $ib" unless defined $apdist;
659 my $sprdist= $wispr->get_edge_weight($ia,$ib);
660 die "$ia $ib $apdist $sprdist" if
661 defined($sprdist) && $sprdist != $apdist;
663 $adddist->execute($ia,$ib,$apdist);
664 $addroute->execute($ia,$ib,$sprdist) if defined $sprdist;
668 # select ia.islandname, ib.islandname, d.dist from dists as d, islands as ia on d.aiid = ia.islandid, islands as ib on d.biid = ib.islandid order by ia.islandname, ib.islandname;
672 #========== update _ocean-*.txt ==========
676 sub localtopo_rewrite () {
677 $localtopo_path= '_ocean-'.(lc $ocean).'.txt';
678 my $fh= new IO::File "$localtopo_path.tmp", 'w';
679 print $fh "# autogenerated - do not edit\n" or die $!;
680 print $fh "ocean $ocean\n" or die $!;
682 foreach my $isle (sort keys %wtisland2arch) {
683 my $arch= $wtisland2arch{$isle};
684 push @{ $arches{$arch} }, $isle;
686 foreach my $arch (sort keys %arches) {
687 print $fh " $arch\n" or die $!;
688 foreach my $isle (@{ $arches{$arch} }) {
689 print $fh " $isle\n" or die $!;
692 print $fh "\n" or die $!;
696 sub localtopo_commit () {
697 rename "$localtopo_path.tmp", $localtopo_path or die $!;
700 #========== main program ==========
702 parse_info_serverside();
704 progress("fetching yppedia chart"); yppedia_ocean_fetch_chart();
705 progress("adding shortcuts"); yppedia_graphs_add_shortcuts();
706 progress("pruning boring vertices"); yppedia_graphs_prune_boring();
707 progress("checking yppedia graphs"); yppedia_graphs_check();
708 progress("setting archs from source-info"); yppedia_archs_sourceinfo();
709 progress("computing shortest paths"); yppedia_graph_shortest_paths();
710 progress("setting archs from labels"); yppedia_archs_chart_labels();
711 progress("setting archs from nearby"); yppedia_archs_fillbynearest();
712 progress("computing yppedia spr"); yppedia_graph_spr();
713 progress("fetching yppedia ocean text"); yppedia_ocean_fetch_text();
719 progress("reading database");
720 database_fetch_ocean();
721 progress("computing database spr"); database_graph_spr();
723 progress("comparing islands"); compare_island_lists();
724 progress("comparing distances"); compare_distances();
729 foreach my $k (@msgkinds) {
730 my $n= $msgkindprinted{$k};
732 printf STDERR "*** %d%s %ss\n", $n, $iteration?' additional':'', $k;
735 if ($msgs{'error'}) {
736 print STDERR "*** errors, aborting update\n";
740 if (!%msgkindprinted) {
741 progress("updating database"); database_do_updates();
742 progress("updating _ocean-*.txt"); localtopo_rewrite();
743 progress("committing database"); $dbh->commit();
744 progress("committing _ocean-*.txt"); localtopo_commit();
749 my $default= !$msgkindprinted{'warning'};
750 printf STDERR "*** confirm update %s ? ", $default?'(y/n)':'(n/y)';
752 $!=0; my $result= <STDIN>; defined $result or die $!;
754 $result= $default?'y':'n' if !length $result;
755 $result= $result =~ m/^y/i;
758 printf STDERR "*** updated abandoned at your request\n";
763 undef %msgkindprinted;