chiark / gitweb /
Fix name in copyright notices
[ypp-sc-tools.web-live.git] / yarrg / web / routetrade
1 <%doc>
2
3  This is part of the YARRG website.  YARRG is a tool and website
4  for assisting players of Yohoho Puzzle Pirates.
5
6  Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
7  Copyright (C) 2009 Clare Boothby
8
9   YARRG's client code etc. is covered by the ordinary GNU GPL (v3 or later).
10   The YARRG website is covered by the GNU Affero GPL v3 or later, which
11    basically means that every installation of the website will let you
12    download the source.
13
14  This program is free software: you can redistribute it and/or modify
15  it under the terms of the GNU Affero General Public License as
16  published by the Free Software Foundation, either version 3 of the
17  License, or (at your option) any later version.
18
19  This program is distributed in the hope that it will be useful,
20  but WITHOUT ANY WARRANTY; without even the implied warranty of
21  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  GNU Affero General Public License for more details.
23
24  You should have received a copy of the GNU Affero General Public License
25  along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
27  Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
28  are used without permission.  This program is not endorsed or
29  sponsored by Three Rings.
30
31
32  This Mason component is the core trade planner for a specific route.
33
34
35 </%doc>
36 <%args>
37 $dbh
38 @islandids
39 @archipelagoes
40 $qa
41 $routeparams
42 </%args>
43 <& query_age:pageload &>
44
45 <%perl>
46
47 my $loss_per_league= defined $routeparams->{LossPerLeaguePct}
48         ? $routeparams->{LossPerLeaguePct}*0.01 : 1e-7;
49 my $loss_per_delay_slot= 1e-8;
50
51 my $now= time;
52
53 my @flow_conds;
54 my @query_params;
55 my %dists;
56 my $expected_total_profit;
57
58 my $sd_condition= sub {
59         my ($bs, $ix) = @_;
60         my $islandid= $islandids[$ix];
61         if (defined $islandid) {
62                 return "${bs}.islandid = $islandid";
63         } else {
64                 push @query_params, $archipelagoes[$ix];
65                 return "${bs}_islands.archipelago = ?";
66         }
67 };
68
69 my $specific= !grep { !defined $_ } @islandids;
70
71 my %ipair2subflowinfs;
72 # $ipair2subflowinfs{$orgi,$dsti}= [ [$orgix,$distix], ... ]
73
74 my @subflows;
75 # $subflows[0]{Flow} = { ... }
76 # $subflows[0]{Org} = $orgix
77 # $subflows[0]{Dst} = $dstix
78
79 foreach my $org_i (0..$#islandids) {
80         my $org_isle= $islandids[$org_i];
81         my $org_cond= $sd_condition->('sell',$org_i);
82         my @dst_conds;
83         foreach my $dst_i ($org_i..$#islandids) {
84                 my $dst_isle= $islandids[$dst_i];
85                 # Don't ever consider sailing things round the houses:
86                 next if defined $dst_isle and
87                         grep { $dst_isle == $_ } @islandids[$org_i..$dst_i-1];
88                 next if defined $org_isle and
89                         grep { $org_isle == $_ } @islandids[$org_i+1..$dst_i];
90                 my $dst_cond;
91                 if ($dst_i==$org_i and !defined $org_isle) {
92                         # we always want arbitrage, but mentioning an arch
93                         # once shouldn't produce intra-arch trades
94                         $dst_cond= "sell.islandid = buy.islandid";
95                 } else {
96                         $dst_cond= $sd_condition->('buy',$dst_i);
97                 }
98                 push @dst_conds, $dst_cond;
99
100                 if ($specific) {
101                         push @{ $ipair2subflowinfs{$org_isle,$dst_isle} },
102                                 [ $org_i, $dst_i ];
103                 }
104         }
105         push @flow_conds, "$org_cond AND (
106                         ".join("
107                      OR ",@dst_conds)."
108                 )";
109 }
110
111 my $stmt= "             
112         SELECT  sell_islands.islandname                         org_name,
113                 sell_islands.islandid                           org_id,
114                 sell.price                                      org_price,
115                 sell.qty                                        org_qty_stall,
116                 sell_stalls.stallname                           org_stallname,
117                 sell.stallid                                    org_stallid,
118                 sell_uploads.timestamp                          org_timestamp,
119                 buy_islands.islandname                          dst_name,
120                 buy_islands.islandid                            dst_id,
121                 buy.price                                       dst_price,
122                 buy.qty                                         dst_qty_stall,
123                 buy_stalls.stallname                            dst_stallname,
124                 buy.stallid                                     dst_stallid,
125                 buy_uploads.timestamp                           dst_timestamp,
126 ".($qa->{ShowStalls} ? "
127                 sell.qty                                        org_qty_agg,
128                 buy.qty                                         dst_qty_agg,
129 " : "
130                 (SELECT sum(qty) FROM sell AS sell_agg
131                   WHERE sell_agg.commodid = commods.commodid
132                   AND   sell_agg.islandid = sell.islandid
133                   AND   sell_agg.price = sell.price)            org_qty_agg,
134                 (SELECT sum(qty) FROM buy AS buy_agg
135                   WHERE buy_agg.commodid = commods.commodid
136                   AND   buy_agg.islandid = buy.islandid
137                   AND   buy_agg.price = buy.price)              dst_qty_agg,
138 ")."
139                 commods.commodname                              commodname,
140                 commods.commodid                                commodid,
141                 commods.unitmass                                unitmass,
142                 commods.unitvolume                              unitvolume,
143                 dist                                            dist,
144                 buy.price - sell.price                          unitprofit
145         FROM commods
146         JOIN sell ON commods.commodid = sell.commodid
147         JOIN buy  ON commods.commodid = buy.commodid
148         JOIN islands AS sell_islands ON sell.islandid = sell_islands.islandid
149         JOIN islands AS buy_islands  ON buy.islandid  = buy_islands.islandid
150         JOIN uploads AS sell_uploads ON sell.islandid = sell_uploads.islandid
151         JOIN uploads AS buy_uploads  ON buy.islandid  = buy_uploads.islandid
152         JOIN stalls  AS sell_stalls  ON sell.stallid  = sell_stalls.stallid
153         JOIN stalls  AS buy_stalls   ON buy.stallid   = buy_stalls.stallid
154         JOIN dists ON aiid = sell.islandid AND biid = buy.islandid
155         WHERE   (
156                 ".join("
157            OR   ", @flow_conds)."
158         )
159           AND   buy.price > sell.price
160         ORDER BY org_name, dst_name, commodname, unitprofit DESC,
161                  org_price, dst_price DESC,
162                  org_stallname, dst_stallname
163      ";
164
165 my $sth= $dbh->prepare($stmt);
166 $sth->execute(@query_params);
167 my @flows;
168
169 my $distquery= $dbh->prepare("
170                 SELECT dist FROM dists WHERE aiid = ? AND biid = ?
171                 ");
172 my $distance= sub {
173         my ($from,$to)= @_;
174         my $d= $dists{$from}{$to};
175         return $d if defined $d;
176         $distquery->execute($from,$to);
177         $d = $distquery->fetchrow_array();
178         defined $d or die "$from $to ?";
179         $dists{$from}{$to}= $d;
180         return $d;
181 };
182
183 my @cols= ({ NoSort => 1 });
184
185 my $addcols= sub {
186         my $base= shift @_;
187         foreach my $name (@_) {
188                 my $col= { Name => $name, %$base };
189                 $col->{Numeric}=1 if !$col->{Text};
190                 push @cols, $col;
191         }
192 };
193
194 if ($qa->{ShowStalls}) {
195         $addcols->({ Text => 1 }, qw(
196                 org_name org_stallname
197                 dst_name dst_stallname
198         ));
199 } else {
200         $addcols->({Text => 1 }, qw(
201                 org_name dst_name
202         ));
203 }
204 $addcols->({ Text => 1 }, qw(commodname));
205 $addcols->({ DoReverse => 1 },
206         qw(     org_price org_qty_agg dst_price dst_qty_agg
207         ));
208 $addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' },
209         qw(     Margin
210         ));
211 $addcols->({ DoReverse => 1 },
212         qw(     unitprofit MaxQty MaxCapital MaxProfit dist
213         ));
214 foreach my $v (qw(MaxMass MaxVolume)) {
215    $addcols->({
216         DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v);
217 }
218
219 </%perl>
220
221 % if ($qa->{'debug'}) {
222 <pre>
223 <% $stmt |h %>
224 <% join(' | ',@query_params) |h %>
225 </pre>
226 % }
227
228 <& dumptable:start, qa => $qa, sth => $sth &>
229 % {
230 %   my $got;
231 %   while ($got= $sth->fetchrow_hashref()) {
232 <%perl>
233
234         my $f= $flows[$#flows];
235         if (    !$f ||
236                 $qa->{ShowStalls} ||
237                 grep { $f->{$_} ne $got->{$_} }
238                         qw(org_id org_price dst_id dst_price commodid)
239         ) {
240                 # Make a new flow rather than adding to the existing one
241
242                 $f= {
243                         Ix => scalar(@flows),
244                         %$got
245                 };
246                 $f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
247                         if !$qa->{ShowStalls};
248                 push @flows, $f;
249         }
250         foreach my $od (qw(org dst)) {
251                 $f->{"${od}Stalls"}{
252                         $got->{"${od}_stallname"}
253                     } =
254                         $got->{"${od}_qty_stall"}
255                     ;
256         }
257
258 </%perl>
259 <& dumptable:row, qa => $qa, sth => $sth, row => $f &>
260 %    }
261 <& dumptable:end, qa => $qa &>
262 % }
263
264 % if (@islandids==1) {
265 %       if (defined $islandids[0]) {
266 Searched for arbitrage trades only.
267 %       } else {
268 Searched for arbitrage trades only, in <% $archipelagoes[0] |h %>
269 <a href="docs#arbitrage">[?]</a>.
270 %       }
271 % }
272
273 <%perl>
274
275 my @sail_total;
276
277 if (!@flows) {
278         print 'No profitable trading opportunities were found.';
279         return;
280 }
281
282 foreach my $f (@flows) {
283
284         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
285                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
286         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
287         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
288
289         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
290         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
291         foreach my $v (qw(Mass Volume)) {
292                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
293         }
294
295         $f->{MarginSortKey}= sprintf "%d",
296                 $f->{'dst_price'} * 10000 / $f->{'org_price'};
297         $f->{Margin}= sprintf "%3.1f%%",
298                 $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
299
300         $f->{ExpectedUnitProfit}=
301                 $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
302                 - $f->{'org_price'};
303
304         $dists{'org_id'}{'dst_id'}= $f->{'dist'};
305
306         my @uid= $f->{commodid};
307         foreach my $od (qw(org dst)) {
308                 push @uid,
309                         $f->{"${od}_id"},
310                         $f->{"${od}_price"};
311                 push @uid,
312                         $f->{"${od}_stallid"}
313                                 if $qa->{ShowStalls};
314         }
315         $f->{UidLong}= join '_', @uid;
316
317         my $base= 31;
318         my $cmpu= '';
319         map {
320                 my $uue= $_;
321                 my $first= $base;
322                 do {
323                         my $this= $uue % $base;
324 #print STDERR "uue=$uue this=$this ";
325                         $uue -= $this;
326                         $uue /= $base;
327                         $this += $first;
328                         $first= 0;
329                         $cmpu .= chr($this + ($this < 26 ? ord('a') :
330                                               $this < 52 ? ord('A')-26
331                                                          : ord('0')-52));
332 #print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
333                         die "$cmpu $uue ?" if length $cmpu > 20;
334                 } while ($uue);
335                 $cmpu;
336         } @uid;
337         $f->{UidShort}= $cmpu;
338
339         if ($qa->{'debug'}) {
340                 my @outuid;
341                 $_= $f->{UidShort};
342                 my $mul;
343                 while (m/./) {
344                         my $v= m/^[a-z]/ ? ord($&)-ord('a') :
345                                m/^[A-Z]/ ? ord($&)-ord('A')+26 :
346                                m/^[0-9]/ ? ord($&)-ord('0')+52 :
347                                die "$_ ?";
348                         if ($v >= $base) {
349                                 push @outuid, 0;
350                                 $v -= $base;
351                                 $mul= 1;
352 #print STDERR "(next)\n";
353                         }
354                         die "$f->{UidShort} $_ ?" unless defined $mul;
355                         $outuid[$#outuid] += $v * $mul;
356
357 #print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
358 #                       "[vs.".ord('a').",".ord('A').",".ord('0')."]".
359 #                       "  outuid=@outuid\n";
360
361                         $mul *= $base;
362                         s/^.//;
363                 }
364                 my $recons_long= join '_', @outuid;
365                 $f->{UidLong} eq $recons_long or
366                         die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
367         }
368
369         if (defined $qa->{"R$f->{UidShort}"} &&
370             !defined $qa->{"T$f->{UidShort}"}) {
371                 $f->{Suppress}= 1;
372         } else {
373                 my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
374                 foreach my $sfi (@$sfis) {
375                         my $subflow= {
376                                 Flow => $f,
377                                 Org => $sfi->[0],
378                                 Dst => $sfi->[1],
379                                 Var => sprintf "f%ss%s_c%d_p%d_%d_p%d_%d",
380                                         $f->{Ix}, $sfi->[0],
381                                         $f->{'commodid'},
382                                         $sfi->[0], $f->{'org_price'},
383                                         $sfi->[1], $f->{'dst_price'}
384                         };
385                         push @{ $f->{Subflows} }, $subflow;
386                         push @subflows, $subflow;
387                 }
388         }
389 }
390 </%perl>
391
392 % my $optimise= $specific;
393 % if (!$optimise) {
394
395 <p>
396 % if (!$specific) {
397 Route contains archipelago(es), not just specific islands.
398 % }
399 Therefore, optimal voyage trade plan not calculated.
400
401 % } else { # ========== OPTMISATION ==========
402 <%perl>
403
404 my $cplex= "
405 Maximize
406
407   totalprofit:
408 ";
409
410 foreach my $sf (@subflows) {
411         my $eup= $sf->{Flow}{ExpectedUnitProfit};
412         $eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
413         $cplex .= sprintf "
414                 %+.20f %s", $eup, $sf->{Var};
415 }
416 $cplex .= "
417
418 Subject To
419 ";
420
421 my %avail_lims;
422 foreach my $flow (@flows) {
423         next if $flow->{Suppress};
424         foreach my $od (qw(org dst)) {
425                 my $limname= join '_', (
426                         $od,
427                         'i'.$flow->{"${od}_id"},
428                         'c'.$flow->{'commodid'},
429                         $flow->{"${od}_price"},
430                         $flow->{"${od}_stallid"},
431                 );
432
433                 push @{ $avail_lims{$limname}{SubflowVars} },
434                         map { $_->{Var} } @{ $flow->{Subflows} };
435                 $avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"};
436         }
437 }
438 foreach my $limname (sort keys %avail_lims) {
439         my $c= $avail_lims{$limname};
440         $cplex .=
441                 sprintf("    %-30s","$limname:")." ".
442                         join("+", @{ $c->{SubflowVars} }).
443                         " <= ".$c->{Qty}."\n";
444 }
445
446 foreach my $ci (0..($#islandids-1)) {
447         my @rel_subflows;
448
449         foreach my $f (@flows) {
450                 next if $f->{Suppress};
451                 my @relsubflow= grep {
452                         $_->{Org} <= $ci &&
453                         $_->{Dst} > $ci;
454                 } @{ $f->{Subflows} };
455                 next unless @relsubflow;
456                 die unless @relsubflow == 1;
457                 push @rel_subflows, @relsubflow;
458 #print " RELEVANT $ci $relsubflow[0]->{Var} ";
459         }
460 #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
461         if (!@rel_subflows) {
462                 foreach my $mv (qw(mass volume)) {
463                         $sail_total[$ci]{$mv}= 0;
464                 }
465                 next;
466         }
467
468         my $applylimit= sub {
469                 my ($mv, $f2val) = @_;
470                 my $max= $routeparams->{"Max".ucfirst $mv};
471                 $max= 1e9 unless defined $max;
472 #print " DEFINED MAX $mv $max ";
473                 $cplex .= "
474    ". sprintf("%-10s","${mv}_$ci:")." ".
475                 join(" + ", map {
476 #print " PART MAX $_->{Var} $_->{Flow}{Ix} ";
477                         $f2val->($_->{Flow}) .' '. $_->{Var};
478                 } @rel_subflows).
479                 " <= $max";
480         };
481
482         $applylimit->('mass',    sub { $_[0]{'unitmass'}  *1e-3 });
483         $applylimit->('volume',  sub { $_[0]{'unitvolume'}*1e-3 });
484         $applylimit->('capital', sub { $_[0]{'org_price'}       });
485         $cplex.= "\n";
486 }
487
488 $cplex.= "
489 Bounds
490         ".(join "
491         ", map { "$_->{Var} >= 0" } @subflows)."
492
493 ";
494
495 $cplex.= "
496 Integer
497         ".(join "
498         ", map { $_->{Var} } @subflows)."
499
500 End
501 ";
502
503 if ($qa->{'debug'}) {
504 </%perl>
505 <pre>
506 <% $cplex |h %>
507 </pre>
508 <%perl>
509 }
510
511 {
512         my $input= pipethrough_prep();
513         print $input $cplex or die $!;
514         my $output= pipethrough_run_along($input, undef, 'glpsol',
515                 qw(glpsol --tmlim 2 --memlim 5 --intopt --cuts --bfs
516                           --cpxlp /dev/stdin -o /dev/stdout));
517         print "<pre>\n" if $qa->{'debug'};
518         my $found_section= 0;
519         my $glpsol_out= '';
520         my $continuation='';
521         while (<$output>) {
522                 $glpsol_out.= $_;
523                 print encode_entities($_) if $qa->{'debug'};
524                 if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
525                         die "$_ $found_section ?" if $found_section>0;
526                         $found_section= 1;
527                         next;
528                 }
529                 if (m/^Objective:\s+totalprofit = (\d+(?:\.\d*)?) /) {
530                         $expected_total_profit= $1;
531                 }
532                 next unless $found_section==1;
533                 if (!length $continuation) {
534                         next if !$continuation &&  m/^[- ]+$/;
535                         if (!/\S/) {
536                                 $found_section= 0;
537                                 next;
538                         }
539                         if (m/^ \s* \d+ \s+ \w+ $/x) {
540                                 $continuation= $&;
541                                 next;
542                         }
543                 }
544                 $_= $continuation.$_;
545                 $continuation= '';
546                 my ($varname, $qty) = m/^
547                         \s* \d+ \s+
548                         (\w+) \s+ (?: [A-Z*]+ \s+ )?
549                         ([0-9.]+) \s
550                         /x or die "$cplex \n==\n $glpsol_out $_ ?";
551                 if ($varname =~ m/^f(\d+)s(\d+)_/) {
552                         my ($ix,$orgix) = ($1,$2);
553                         my $flow= $flows[$ix] or die;
554                         my @relsubflow= grep { $_->{Org} == $orgix }
555                                 @{ $flow->{Subflows} };
556                         die "$ix $orgix @relsubflow" unless @relsubflow == 1;
557                         my $sf= $relsubflow[0];
558                         $sf->{OptQty}= $qty;
559                         $sf->{OptProfit}= $qty * $flow->{'unitprofit'};
560                         $sf->{OptCapital}= $qty * $flow->{'org_price'};
561                 } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
562                         my ($mv,$ix) = ($1,$2);
563                         $sail_total[$ix]{$mv}= $qty;
564                 }
565         }
566         print "</pre>\n" if $qa->{'debug'};
567         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
568         pipethrough_run_finish($output,$prerr);
569         map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
570         defined $expected_total_profit or die "$prerr ?";
571 };
572
573 $addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
574         my ($flow,$col,$v,$spec) = @_;
575         if ($flow->{ExpectedUnitProfit} < 0) {
576                 $spec->{Span}= 3;
577                 $spec->{String}= '(Small margin)';
578                 $spec->{Align}= 'align=center';
579         }
580 } }, qw(
581                 OptQty
582         ));
583 $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
584                 OptCapital OptProfit
585         ));
586
587 </%perl>
588
589 % } # ========== OPTIMISATION ==========
590
591 % if (!printable($m)) {
592 <h2>Contents</h2>
593 <ul>
594 % if ($optimise) {
595  <li><a href="#plan">Voyage trading plan</a>
596   <ul>
597    <li><a href="#summary">Summary statistics</a>
598    <li>Printable:
599          <input type=submit name=printable_pdf value="PDF">
600          <input type=submit name=printable_html value="HTML">
601          <input type=submit name=printable_ps value="PostScript">
602          <input type=submit name=printable_pdf2 value="PDF 2-up">
603          <input type=submit name=printable_ps2 value="PostScript 2-up">
604   </ul>
605 % }
606  <li><a href="#dataage">Data age summary</a>
607  <li><a href="#trades">Relevant trades</a>
608 </ul>
609 % } else {
610 %       my @tl= gmtime $now or die $!;
611 <p>
612 Generated by YARRG at <strong><%
613         sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC",
614                 $tl[5]+1900, @tl[4,3,2,1,0]
615                         |h %></strong>.
616 % }
617
618 % if ($optimise) { # ========== TRADING PLAN ==========
619 %
620 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
621 %                               WHERE islandid = ?');
622 % my %da_ages;
623 % my $total_total= 0;
624 % my $total_dist= 0;
625 %
626 <h2><a name="plan">Voyage trading plan</a></h2>
627
628 <table class="data" rules=groups <% printable($m) ? 'width=100%' : '' %> >
629 % my $tbody= sub {
630 %       if (!printable($m)) { return '<tbody>'; }
631 %#  return "<tr><td colspan=7><hr>";
632 %       my ($c)= qw(40 00)[$_[0]];
633 %       return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
634 % };
635 %
636 % foreach my $i (0..$#islandids) {
637 <% $tbody->(1) %>
638 <tr><td colspan=4>
639 %       $iquery->execute($islandids[$i]);
640 %       my ($islandname) = $iquery->fetchrow_array();
641 %       if (!$i) {
642 <strong>Start at <% $islandname |h %></strong>
643 %       } else {
644 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
645 %               $total_dist += $this_dist;
646 <%perl>
647                 my $total_value= 0;
648                 foreach my $sf (@subflows) {
649                         next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
650                         $total_value +=
651                                 $sf->{OptQty} * $sf->{Flow}{'dst_price'};
652                 }
653 </%perl>
654 <strong>Sail to <% $islandname |h %></strong>
655 - <% $this_dist |h %> leagues,
656  <% $total_value %>poe at risk
657  </td>
658 %       }
659 <%perl>
660      my $age_reported= 0;
661      my %flowlists;
662      #print "<tr><td colspan=6>" if $qa->{'debug'};
663      foreach my $od (qw(org dst)) {
664         #print " [[ i $i od $od " if $qa->{'debug'};
665         foreach my $sf (@subflows) {
666                 my $f= $sf->{Flow};
667                 next unless $sf->{ucfirst $od} == $i;
668                 #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
669                 #       if $qa->{'debug'};
670                 next unless $sf->{OptQty};
671                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
672                 die if $arbitrage and $sf->{Org} != $sf->{Dst};
673                 my $price= $f->{"${od}_price"};
674                 my $stallname= $f->{"${od}_stallname"};
675                 my $todo= \$flowlists{$od}{
676                                 $f->{'commodname'},
677                                 (sprintf "%07d", ($od eq 'dst' ?
678                                                 9999999-$price : $price)),
679                                 $stallname
680                         };
681                 $$todo= {
682                         Qty => 0,
683                         orgArbitrage => 0,
684                         dstArbitrage => 0,
685                 } unless $$todo;
686                 $$todo->{'commodname'}= $f->{'commodname'};
687                 $$todo->{'stallname'}= $stallname;
688                 $$todo->{Price}= $price;
689                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
690                 $$todo->{Qty} += $sf->{OptQty};
691                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
692                 $$todo->{Stalls}= $f->{"${od}Stalls"};
693                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
694         }
695         #print "]] " if $qa->{'debug'};
696      }
697      #print "</tr>" if $qa->{'debug'};
698
699      my ($total, $total_to_show);
700      my $dline= 0;
701      my $show_total= sub {
702         my ($totaldesc, $sign) = @_;
703         if (defined $total) {
704                 die if defined $total_to_show;
705                 $total_total += $sign * $total;
706                 $total_to_show= [ $totaldesc, $total ];
707                 $total= undef;
708         }
709         $dline= 0;
710      };
711      my $show_total_now= sub {
712         my ($xinfo) = @_;
713         return unless defined $total_to_show;
714         my ($totaldesc,$totalwas) = @$total_to_show;
715 </%perl>
716 <tr>
717 <td colspan=1>
718 <td colspan=2><% $xinfo %>
719 <td colspan=2 align=right><% $totaldesc %>
720 <td align=right><% $totalwas |h %> total
721 <%perl>
722         $total_to_show= undef;
723      };
724 </%perl>
725 %    my $show_flows= sub {
726 %       my ($od,$arbitrage,$collectdeliver) = @_;
727 %       my $todo= $flowlists{$od};
728 %       return unless $todo;
729 %       foreach my $tkey (sort keys %$todo) {
730 %               my $t= $todo->{$tkey};
731 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
732 %               $show_total_now->('');
733 %               if (!$age_reported++) {
734 %                       my $age= $now - $t->{Timestamp};
735 %                       my $cellid= "da_${i}";
736 %                       $da_ages{$cellid}= $age;
737 <td colspan=2>\
738 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
739 %               } elsif (!defined $total) {
740 %                       $total= 0;
741 <% $tbody->(0) %>
742 %               }
743 %               $total += $t->{Total};
744 %               my $span= 0 + keys %{ $t->{Stalls} };
745 %               my $td= "td rowspan=$span";
746 % tr_datarow($m,$dline);
747 <<% $td %>><% $collectdeliver %>
748 <<% $td %>><% $t->{'commodname'} |h %>
749 %
750 %               my @stalls= sort keys %{ $t->{Stalls} };
751 %               my $pstall= sub {
752 %                       my $name= $stalls[$_[0]];
753 <td><% $name |h %>
754 %               };
755 %
756 %               $pstall->(0);
757 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
758 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
759 <<% $td %> align=right><% $t->{Total} |h %> total
760 %
761 %               foreach my $stallix (1..$#stalls) {
762 % tr_datarow($m,$dline);
763 %                       $pstall->($stallix);
764 %               }
765 %
766 %               $dline ^= 1;
767 %       }
768 %    };
769 <%perl>
770
771      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
772      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
773      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
774      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
775      my $totals= '';
776      if ($i < $#islandids) {
777         $totals .=      "In hold $sail_total[$i]{mass}kg,".
778                         " $sail_total[$i]{volume} l";
779         my $delim= '; spare ';
780         my $domv= sub {
781                 my ($max, $got, $units) = @_;
782                 return unless defined $max;
783                 $totals .= $delim;
784                 $totals .= sprintf "%g %s", ($max-$got), $units;
785                 $delim= ', ';
786         };
787         $domv->($routeparams->{MaxMass},   $sail_total[$i]{mass},   'kg');
788         $domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l');
789         $totals .= ".\n";
790      }
791      $show_total_now->($totals);
792 }
793 </%perl><a name="summary"></a>
794 <% $tbody->(1) %><tr>
795 <td colspan=2>Total distance: <% $total_dist %> leagues.
796 <td colspan=3 align=right>Overall net cash flow
797 <td align=right><strong><%
798   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
799  %></strong>
800 </table>
801 <& query_age:dataages, id2age => \%da_ages &>
802 Expected average profit:
803  approx. <strong><% sprintf "%d", $expected_total_profit %></strong> poe
804  (considering expected losses, but ignoring rum consumed)
805 %
806 % } # ========== TRADING PLAN ==========
807
808 % if (!printable($m)) {
809 <h2><a name="dataage">Data age summary</a></h2>
810 <%perl>
811         my $sth_i= $dbh->prepare(<<END);
812                 SELECT archipelago, islandid, islandname, timestamp
813                         FROM uploads NATURAL JOIN islands
814                         WHERE islandid = ?
815 END
816         my $sth_a= $dbh->prepare(<<END);
817                 SELECT archipelago, islandid, islandname, timestamp
818                         FROM uploads NATURAL JOIN islands
819                         WHERE archipelago = ?
820                         ORDER BY islandname
821 END
822         my $ix=$#islandids;
823         my $sth_current;
824         my %idone;
825         my $fetchrow= sub {
826                 for (;;) {
827                         if ($sth_current) {
828                                 my $row= $sth_current->fetchrow_hashref();
829                                 if ($row) {
830                                         next if $idone{$row->{'islandid'}}++;
831                                         return $row;
832                                 }
833                         }
834                         return undef if $ix < 0;
835                         my $iid= $islandids[$ix];
836                         if (defined $iid) {
837                                 $sth_i->execute($iid);
838                                 $sth_current= $sth_i;
839                         } else {
840                                 my $arch= $archipelagoes[$ix];
841                                 die unless defined $arch && length $arch;
842                                 $sth_a->execute($arch);
843                                 $sth_current= $sth_a;
844                         }
845                         $ix--;
846                 }
847         };
848 </%perl>
849 <&| query_age:agestable, now => $now, fetchrow => $fetchrow &>
850 Islands shown in reverse order of visits.<br>
851 </&>
852 % }
853
854 % if (!printable($m)) {
855 %   my %ts_sortkeys;
856 %   {
857 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
858 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
859 <h2><a name="trades">Relevant trades</a></h2>
860 <table class="data" id="trades" rules=groups>
861 <colgroup span=1>
862 <colgroup span=2>
863 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
864 <colgroup span=1>
865 <colgroup span=2>
866 <colgroup span=2>
867 <colgroup span=2>
868 <colgroup span=3>
869 <colgroup span=3>
870 %       if ($optimise) {
871 <colgroup span=3>
872 %       }
873 <tr>
874 <th>
875 <th<% $cdspan %>>Collect
876 <th<% $cdspan %>>Deliver
877 <th>
878 <th colspan=2>Collect
879 <th colspan=2>Deliver
880 <th colspan=2>Profit
881 <th colspan=3>Max
882 <th colspan=1>
883 <th colspan=2>Max
884 %       if ($optimise) {
885 <th colspan=3>Planned
886 %       }
887
888 <tr>
889 <th>
890 <th>Island <% $cdstall %>
891 <th>Island <% $cdstall %>
892 <th>Commodity
893 <th>Price
894 <th>Qty
895 <th>Price
896 <th>Qty
897 <th>Margin
898 <th>Unit
899 <th>Qty
900 <th>Capital
901 <th>Profit
902 <th>Dist
903 <th>Mass
904 <th>Vol
905 %       if ($optimise) {
906 <th>Qty
907 <th>Capital
908 <th>Profit
909 %       }
910 %   }
911
912 <tr id="trades_sort">
913 %   foreach my $col (@cols) {
914 <th>
915 %   }
916
917 %   foreach my $flowix (0..$#flows) {
918 %       my $flow= $flows[$flowix];
919 %       my $rowid= "id_row_$flow->{UidShort}";
920 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
921 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
922     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
923        <% $flow->{Suppress} ? '' : 'checked' %> >
924 %       my $ci= 1;
925 %       while ($ci < @cols) {
926 %               my $col= $cols[$ci];
927 %               my $spec= {
928 %                       Span => 1,
929 %                       Align => ($col->{Text} ? '' : 'align=right')
930 %               };
931 %               my $cn= $col->{Name};
932 %               my $v;
933 %               if (!$col->{TotalSubflows}) {
934 %                       $v= $flow->{$cn};
935 %               } else {
936 %                       $v= 0;
937 %                       $v += $_->{$cn} foreach @{ $flow->{Subflows} };
938 %               }
939 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
940 %               $col->{Total} += $v
941 %                       if defined $col->{Total} and not $flow->{Suppress};
942 %               $v='' if !$col->{Text} && !$v;
943 %               my $sortkey= $col->{SortColKey} ?
944 %                       $flow->{$col->{SortColKey}} : $v;
945 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
946 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
947  %> <% $spec->{Align}
948  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
949 %               $ci += $spec->{Span};
950 %       }
951 %   }
952 <tr id="trades_total">
953 <th>
954 <th colspan=2>Total
955 %   foreach my $ci (3..$#cols) {
956 %       my $col= $cols[$ci];
957 <td align=right>
958 %       if (defined $col->{Total}) {
959 <% $col->{Total} |h %>
960 %       }
961 %   }
962 </table>
963
964 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
965         throw => 'trades_sort', tbrow => 'trades_total' &>
966   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
967 </&tabsort>
968 <p>
969 <input type=submit name=update value="Update">
970
971 % } # !printable
972
973 <%init>
974 use CommodsWeb;
975 use Commods;
976 </%init>