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