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