chiark / gitweb /
Revert "In hold" to "Hold:" change
[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 $max_mass
42 $max_volume
43 $lossperleaguepct
44 $max_capital
45 </%args>
46 <&| script &>
47   da_pageload= Date.now();
48 </&script>
49
50 <%perl>
51
52 my $loss_per_league= defined $lossperleaguepct ? $lossperleaguepct*0.01 : 1e-7;
53 my $loss_per_delay_slot= 1e-8;
54
55 my $now= time;
56
57 my %flow_conds;
58 my @query_params;
59 my %dists;
60
61 my $sd_condition= sub {
62         my ($bs, $ix) = @_;
63         my $islandid= $islandids[$ix];
64         if (defined $islandid) {
65                 return "${bs}.islandid = $islandid";
66         } else {
67                 push @query_params, $archipelagoes[$ix];
68                 return "${bs}_islands.archipelago = ?";
69         }
70 };
71
72 my $specific= !grep { !defined $_ } @islandids;
73
74 my %ipair2subflowinfs;
75 # $ipair2subflowinfs{$orgi,$dsti}= [ [$orgix,$distix], ... ]
76
77 my @subflows;
78 # $subflows[0]{Flow} = { ... }
79 # $subflows[0]{Org} = $orgix
80 # $subflows[0]{Dst} = $dstix
81
82 foreach my $org_i (0..$#islandids) {
83         my $org_isle= $islandids[$org_i];
84         my $org_cond= $sd_condition->('sell',$org_i);
85         my %dst_conds;
86         foreach my $dst_i ($org_i..$#islandids) {
87                 my $dst_isle= $islandids[$dst_i];
88                 # Don't ever consider sailing things round the houses:
89                 next if grep { $dst_isle == $_ } @islandids[$org_i..$dst_i-1];
90                 next if grep { $org_isle == $_ } @islandids[$org_i+1..$dst_i];
91                 my $dst_cond= $sd_condition->('buy',$dst_i);
92                 if ($dst_i==$org_i and !defined $org_isle) {
93                         # we always want arbitrage, but mentioning an arch
94                         # once shouldn't produce intra-arch trades
95                         $dst_cond=
96                                 "($dst_cond AND sell.islandid = buy.islandid)";
97                 }
98                 $dst_conds{$dst_cond}= 1;
99
100                 if ($specific) {
101                         push @{ $ipair2subflowinfs{$org_isle,$dst_isle} },
102                                 [ $org_i, $dst_i ];
103                 }
104         }
105         $flow_conds{ "$org_cond AND (
106                         ".join("
107                      OR ", sort keys %dst_conds)."
108                 )" }= 1;
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   ", sort keys %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 <%perl>
265
266 my @sail_total;
267
268 if (!@flows) {
269         print 'No profitable trading opportunities were found.';
270         return;
271 }
272
273 foreach my $f (@flows) {
274
275         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
276                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
277         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
278         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
279
280         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
281         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
282         foreach my $v (qw(Mass Volume)) {
283                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
284         }
285
286         my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
287         foreach my $sfi (@$sfis) {
288                 my $subflow= {
289                         Flow => $f,
290                         Org => $sfi->[0],
291                         Dst => $sfi->[1],
292                         Var => sprintf "f%ss%s", $f->{Ix}, $sfi->[0]
293                 };
294                 push @{ $f->{Subflows} }, $subflow;
295                 push @subflows, $subflow;
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         }
376
377 }
378 </%perl>
379
380 % my $optimise= $specific;
381 % if (!$optimise) {
382
383 <p>
384 % if (!$specific) {
385 Route contains archipelago(es), not just specific islands.
386 % }
387 Therefore, optimal voyage trade plan not calculated.
388
389 % } else { # ========== OPTMISATION ==========
390 <%perl>
391
392 my $cplex= "
393 Maximize
394
395   totalprofit:
396 ";
397
398 foreach my $sf (@subflows) {
399         my $eup= $sf->{Flow}{ExpectedUnitProfit};
400         $eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
401         $cplex .= sprintf "
402                 %+.20f %s", $eup, $sf->{Var};
403 }
404 $cplex .= "
405
406 Subject To
407 ";
408
409 my %avail_lims;
410 foreach my $flow (@flows) {
411         if ($flow->{Suppress}) {
412                 foreach my $sf (@{ $flow->{Subflows} }) {
413                         $cplex .= "
414    $sf->{Var} = 0";
415                 }
416                 next;
417         }
418         foreach my $od (qw(org dst)) {
419                 my $limname= join '_', (
420                         'avail',
421                         $flow->{'commodid'},
422                         $od,
423                         $flow->{"${od}_id"},
424                         $flow->{"${od}_price"},
425                         $flow->{"${od}_stallid"},
426                 );
427
428                 push @{ $avail_lims{$limname}{SubflowVars} },
429                         map { $_->{Var} } @{ $flow->{Subflows} };
430                 $avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"};
431         }
432 }
433 foreach my $limname (sort keys %avail_lims) {
434         my $c= $avail_lims{$limname};
435         $cplex .=
436                 sprintf("    %-30s","$limname:")." ".
437                         join("+", @{ $c->{SubflowVars} }).
438                         " <= ".$c->{Qty}."\n";
439 }
440
441 foreach my $ci (0..($#islandids-1)) {
442         my @rel_subflows;
443
444         foreach my $f (@flows) {
445                 next if $f->{Suppress};
446                 my @relsubflow= grep {
447                         $_->{Org} <= $ci &&
448                         $_->{Dst} > $ci;
449                 } @{ $f->{Subflows} };
450                 next unless @relsubflow;
451                 die unless @relsubflow == 1;
452                 push @rel_subflows, @relsubflow;
453 #print " RELEVANT $ci $relsubflow[0]->{Var} ";
454         }
455 #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
456         if (!@rel_subflows) {
457                 foreach my $mv (qw(mass volume)) {
458                         $sail_total[$ci]{$mv}= 0;
459                 }
460                 next;
461         }
462
463         my $applylimit= sub {
464                 my ($mv, $max, $f2val) = @_;
465                 $max= 1e9 unless defined $max;
466 #print " DEFINED MAX $mv $max ";
467                 $cplex .= "
468    ". sprintf("%-10s","${mv}_$ci:")." ".
469                 join(" + ", map {
470 #print " PART MAX $_->{Var} $_->{Flow}{Ix} ";
471                         $f2val->($_->{Flow}) .' '. $_->{Var};
472                 } @rel_subflows).
473                 " <= $max";
474         };
475
476         $applylimit->('mass',   $max_mass,   sub { $_[0]{'unitmass'}  *1e-3 });
477         $applylimit->('volume', $max_volume, sub { $_[0]{'unitvolume'}*1e-3 });
478         $applylimit->('capital',$max_capital,sub { $_[0]{'org_price'}       });
479         $cplex.= "\n";
480 }
481
482 $cplex.= "
483 Bounds
484         ".(join "
485         ", map { "$_->{Var} >= 0" } @subflows)."
486
487 ";
488
489 $cplex.= "
490 Integer
491         ".(join "
492         ", map { $_->{Var} } @subflows)."
493
494 End
495 ";
496
497 if ($qa->{'debug'}) {
498 </%perl>
499 <pre>
500 <% $cplex |h %>
501 </pre>
502 <%perl>
503 }
504
505 {
506         my $input= pipethrough_prep();
507         print $input $cplex or die $!;
508         my $output= pipethrough_run_along($input, undef, 'glpsol',
509                 qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
510         print "<pre>\n" if $qa->{'debug'};
511         my $found_section= 0;
512         my $glpsol_out= '';
513         my $continuation='';
514         while (<$output>) {
515                 $glpsol_out.= $_;
516                 print encode_entities($_) if $qa->{'debug'};
517                 if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
518                         die "$_ $found_section ?" if $found_section>0;
519                         $found_section= 1;
520                         next;
521                 }
522                 next unless $found_section==1;
523                 if (!length $continuation) {
524                         next if !$continuation &&  m/^[- ]+$/;
525                         if (!/\S/) {
526                                 $found_section= 0;
527                                 next;
528                         }
529                         if (m/^ \s* \d+ \s+ \w+ $/x) {
530                                 $continuation= $&;
531                                 next;
532                         }
533                 }
534                 $_= $continuation.$_;
535                 $continuation= '';
536                 my ($varname, $qty) = m/^
537                         \s* \d+ \s+
538                         (\w+) \s+ (?: [A-Z*]+ \s+ )?
539                         ([0-9.]+) \s
540                         /x or die "$_ ?";
541                 if ($varname =~ m/^f(\d+)s(\d+)$/) {
542                         my ($ix,$orgix) = ($1,$2);
543                         my $flow= $flows[$ix] or die;
544                         my @relsubflow= grep { $_->{Org} == $orgix }
545                                 @{ $flow->{Subflows} };
546                         die "$ix $orgix @relsubflow" unless @relsubflow == 1;
547                         my $sf= $relsubflow[0];
548                         $sf->{OptQty}= $qty;
549                         $sf->{OptProfit}= $qty * $flow->{'unitprofit'};
550                         $sf->{OptCapital}= $qty * $flow->{'org_price'};
551                 } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
552                         my ($mv,$ix) = ($1,$2);
553                         $sail_total[$ix]{$mv}= $qty;
554                 }
555         }
556         print "</pre>\n" if $qa->{'debug'};
557         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
558         pipethrough_run_finish($output,$prerr);
559         map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
560 };
561
562 $addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
563         my ($flow,$col,$v,$spec) = @_;
564         if ($flow->{ExpectedUnitProfit} < 0) {
565                 $spec->{Span}= 3;
566                 $spec->{String}= '(Small margin)';
567                 $spec->{Align}= 'align=center';
568         }
569 } }, qw(
570                 OptQty
571         ));
572 $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
573                 OptCapital OptProfit
574         ));
575
576 </%perl>
577
578 % } # ========== OPTIMISATION ==========
579
580 % my %ts_sortkeys;
581 % {
582 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
583 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
584 <table id="trades" rules=groups>
585 <colgroup span=1>
586 <colgroup span=2>
587 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
588 <colgroup span=1>
589 <colgroup span=2>
590 <colgroup span=2>
591 <colgroup span=2>
592 <colgroup span=3>
593 <colgroup span=3>
594 %       if ($optimise) {
595 <colgroup span=3>
596 %       }
597 <tr class="spong">
598 <th>
599 <th<% $cdspan %>>Collect
600 <th<% $cdspan %>>Deliver
601 <th>
602 <th colspan=2>Collect
603 <th colspan=2>Deliver
604 <th colspan=2>Profit
605 <th colspan=3>Max
606 <th colspan=1>
607 <th colspan=2>Max
608 %       if ($optimise) {
609 <th colspan=3>Planned
610 %       }
611
612 <tr>
613 <th>
614 <th>Island <% $cdstall %>
615 <th>Island <% $cdstall %>
616 <th>Commodity
617 <th>Price
618 <th>Qty
619 <th>Price
620 <th>Qty
621 <th>Margin
622 <th>Unit
623 <th>Qty
624 <th>Capital
625 <th>Profit
626 <th>Dist
627 <th>Mass
628 <th>Vol
629 %       if ($optimise) {
630 <th>Qty
631 <th>Capital
632 <th>Profit
633 %       }
634 % }
635
636 <tr id="trades_sort">
637 % foreach my $col (@cols) {
638 <th>
639 % }
640
641 % foreach my $flowix (0..$#flows) {
642 %       my $flow= $flows[$flowix];
643 %       my $rowid= "id_row_$flow->{UidShort}";
644 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
645 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
646     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
647        <% $flow->{Suppress} ? '' : 'checked' %> >
648 %       my $ci= 1;
649 %       while ($ci < @cols) {
650 %               my $col= $cols[$ci];
651 %               my $spec= {
652 %                       Span => 1,
653 %                       Align => ($col->{Text} ? '' : 'align=right')
654 %               };
655 %               my $cn= $col->{Name};
656 %               my $v;
657 %               if (!$col->{TotalSubflows}) {
658 %                       $v= $flow->{$cn};
659 %               } else {
660 %                       $v= 0;
661 %                       $v += $_->{$cn} foreach @{ $flow->{Subflows} };
662 %               }
663 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
664 %               $col->{Total} += $v
665 %                       if defined $col->{Total} and not $flow->{Suppress};
666 %               $v='' if !$col->{Text} && !$v;
667 %               my $sortkey= $col->{SortColKey} ?
668 %                       $flow->{$col->{SortColKey}} : $v;
669 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
670 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
671  %> <% $spec->{Align}
672  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
673 %               $ci += $spec->{Span};
674 %       }
675 % }
676 <tr id="trades_total">
677 <th>
678 <th colspan=2>Total
679 % foreach my $ci (3..$#cols) {
680 %       my $col= $cols[$ci];
681 <td align=right>
682 %       if (defined $col->{Total}) {
683 <% $col->{Total} |h %>
684 %       }
685 % }
686 </table>
687
688 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
689         throw => 'trades_sort', tbrow => 'trades_total' &>
690   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
691 </&tabsort>
692
693 <input type=submit name=update value="Update">
694
695 % if ($optimise) { # ========== TRADING PLAN ==========
696 %
697 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
698 %                               WHERE islandid = ?');
699 % my %da_ages;
700 % my $total_total= 0;
701 % my $total_dist= 0;
702 %
703 <h1>Voyage trading plan</h1>
704 <table rules=groups>
705 % foreach my $i (0..$#islandids) {
706 <tbody>
707 <tr><td colspan=4>
708 %       $iquery->execute($islandids[$i]);
709 %       my ($islandname) = $iquery->fetchrow_array();
710 %       if (!$i) {
711 <strong>Start at <% $islandname |h %></strong>
712 %       } else {
713 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
714 %               $total_dist += $this_dist;
715 <%perl>
716                 my $total_value= 0;
717                 foreach my $sf (@subflows) {
718                         next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
719                         $total_value +=
720                                 $sf->{OptQty} * $sf->{Flow}{'dst_price'};
721                 }
722 </%perl>
723 <strong>Sail to <% $islandname |h %></strong>
724 - <% $this_dist |h %> leagues,
725  <% $total_value %>poe at risk
726  </td>
727 %       }
728 <%perl>
729      my $age_reported= 0;
730      my %flowlists;
731      #print "<tr><td colspan=6>" if $qa->{'debug'};
732      foreach my $od (qw(org dst)) {
733         #print " [[ i $i od $od " if $qa->{'debug'};
734         foreach my $sf (@subflows) {
735                 my $f= $sf->{Flow};
736                 next if $f->{Suppress};
737                 next unless $sf->{ucfirst $od} == $i;
738                 #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
739                 #       if $qa->{'debug'};
740                 next unless $sf->{OptQty};
741                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
742                 die if $arbitrage and $sf->{Org} != $sf->{Dst};
743                 my $price= $f->{"${od}_price"};
744                 my $stallname= $f->{"${od}_stallname"};
745                 my $todo= \$flowlists{$od}{
746                                 $f->{'commodname'},
747                                 (sprintf "%07d", ($od eq 'dst' ?
748                                                 9999999-$price : $price)),
749                                 $stallname
750                         };
751                 $$todo= {
752                         Qty => 0,
753                         orgArbitrage => 0,
754                         dstArbitrage => 0,
755                 } unless $$todo;
756                 $$todo->{'commodname'}= $f->{'commodname'};
757                 $$todo->{'stallname'}= $stallname;
758                 $$todo->{Price}= $price;
759                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
760                 $$todo->{Qty} += $sf->{OptQty};
761                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
762                 $$todo->{Stalls}= $f->{"${od}Stalls"};
763                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
764         }
765         #print "]] " if $qa->{'debug'};
766      }
767      #print "</tr>" if $qa->{'debug'};
768
769      my ($total, $total_to_show);
770      my $dline= 0;
771      my $show_total= sub {
772         my ($totaldesc, $sign) = @_;
773         if (defined $total) {
774                 die if defined $total_to_show;
775                 $total_total += $sign * $total;
776                 $total_to_show= [ $totaldesc, $total ];
777                 $total= undef;
778         }
779         $dline= 0;
780      };
781      my $show_total_now= sub {
782         my ($xinfo) = @_;
783         return unless defined $total_to_show;
784         my ($totaldesc,$totalwas) = @$total_to_show;
785 </%perl>
786 <tr>
787 <td colspan=1>
788 <td colspan=2><% $xinfo %>
789 <td colspan=2 align=right><% $totaldesc %>
790 <td align=right><% $totalwas |h %> total
791 <%perl>
792         $total_to_show= undef;
793      };
794 </%perl>
795 %    my $show_flows= sub {
796 %       my ($od,$arbitrage,$collectdeliver) = @_;
797 %       my $todo= $flowlists{$od};
798 %       return unless $todo;
799 %       foreach my $tkey (sort keys %$todo) {
800 %               my $t= $todo->{$tkey};
801 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
802 %               $show_total_now->('');
803 %               if (!$age_reported++) {
804 %                       my $age= $now - $t->{Timestamp};
805 %                       my $cellid= "da_${i}";
806 %                       $da_ages{$cellid}= $age;
807 <td colspan=2>\
808 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
809 %               } elsif (!defined $total) {
810 %                       $total= 0;
811 <tbody>
812 %               }
813 %               $total += $t->{Total};
814 %               my $span= 0 + keys %{ $t->{Stalls} };
815 %               my $td= "td rowspan=$span";
816 <tr class="datarow<% $dline %>">
817 <<% $td %>><% $collectdeliver %>
818 <<% $td %>><% $t->{'commodname'} |h %>
819 %
820 %               my @stalls= sort keys %{ $t->{Stalls} };
821 %               my $pstall= sub {
822 %                       my $name= $stalls[$_[0]];
823 <td><% $name |h %>
824 %               };
825 %
826 %               $pstall->(0);
827 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
828 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
829 <<% $td %> align=right><% $t->{Total} |h %> total
830 %
831 %               foreach my $stallix (1..$#stalls) {
832 <tr class="datarow<% $dline %>">
833 %                       $pstall->($stallix);
834 %               }
835 %
836 %               $dline ^= 1;
837 %       }
838 %    };
839 <%perl>
840
841      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
842      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
843      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
844      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
845      my $totals= '';
846      if ($i < $#islandids) {
847         $totals .=      "In hold $sail_total[$i]{mass}kg,".
848                         " $sail_total[$i]{volume} l";
849         my $delim= '; spare ';
850         my $domv= sub {
851                 my ($max, $got, $units) = @_;
852                 return unless defined $max;
853                 $totals .= $delim;
854                 $totals .= sprintf "%g %s", ($max-$got), $units;
855                 $delim= ', ';
856         };
857         $domv->($max_mass,   $sail_total[$i]{mass},   'kg');
858         $domv->($max_volume, $sail_total[$i]{volume}, 'l');
859         $totals .= ".\n";
860      }
861      $show_total_now->($totals);
862 }
863 </%perl>
864 <tbody><tr>
865 <td colspan=2>Total distance: <% $total_dist %> leagues.
866 <td colspan=3 align=right>Overall net cash flow
867 <td align=right><strong><%
868   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
869  %></strong>
870 </table>
871 <& query_age:dataages, id2age => \%da_ages &>
872 %
873 % } # ========== TRADING PLAN ==========
874
875 <%init>
876 use CommodsWeb;
877 use Commods;
878 </%init>