chiark / gitweb /
Merge branch 'stable-3.x'
[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 $max_mass
42 $max_volume
43 $lossperleaguepct
44 </%args>
45 <&| script &>
46   da_pageload= Date.now();
47 </&script>
48
49 <%perl>
50
51 my $loss_per_league= defined $lossperleaguepct ? $lossperleaguepct*0.01 : 1e-7;
52
53 my $now= time;
54
55 my @flow_conds;
56 my @query_params;
57 my %dists;
58
59 my $sd_condition= sub {
60         my ($bs, $ix) = @_;
61         my $islandid= $islandids[$ix];
62         if (defined $islandid) {
63                 return "${bs}.islandid = $islandid";
64         } else {
65                 push @query_params, $archipelagoes[$ix];
66                 return "${bs}_islands.archipelago = ?";
67         }
68 };
69
70 my %islandpair;
71 # $islandpair{$a,$b}= [ $start_island_ix, $end_island_ix ]
72
73 my $specific= !grep { !defined $_ } @islandids;
74 my $confusing= 0;
75
76 foreach my $src_i (0..$#islandids) {
77         my $src_isle= $islandids[$src_i];
78         my $src_cond= $sd_condition->('sell',$src_i);
79         my @dst_conds;
80         foreach my $dst_i ($src_i..$#islandids) {
81                 my $dst_isle= $islandids[$dst_i];
82                 my $dst_cond= $sd_condition->('buy',$dst_i);
83                 if ($dst_i==$src_i and !defined $src_isle) {
84                         # we always want arbitrage, but mentioning an arch
85                         # once shouldn't produce intra-arch trades
86                         $dst_cond=
87                                 "($dst_cond AND sell.islandid = buy.islandid)";
88                 }
89                 push @dst_conds, $dst_cond;
90
91                 if ($specific && !$confusing &&
92                     # With a circular route, do not carry goods round the loop
93                     !(($src_i==0 || $src_i==$#islandids) &&
94                       $dst_i==$#islandids &&
95                       $src_isle == $islandids[$dst_i])) {
96                         if ($islandpair{$src_isle,$dst_isle}) {
97                                 $confusing= 1;
98 print "confusing $src_i $src_isle  $dst_i $dst_isle\n";
99                         } else {
100                                 $islandpair{$src_isle,$dst_isle}=
101                                         [ $src_i, $dst_i ];
102                         }
103                 }
104         }
105         push @flow_conds, "$src_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                         Var => "f".@flows,
245                         %$got
246                 };
247                 $f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
248                         if !$qa->{ShowStalls};
249                 push @flows, $f;
250         }
251         foreach my $od (qw(org dst)) {
252                 $f->{"${od}Stalls"}{
253                         $got->{"${od}_stallname"}
254                     } =
255                         $got->{"${od}_qty_stall"}
256                     ;
257         }
258
259 </%perl>
260 <& dumptable:row, qa => $qa, sth => $sth, row => $f &>
261 %    }
262 <& dumptable:end, qa => $qa &>
263 % }
264
265 <%perl>
266
267 my @total_massvol;
268
269 if (!@flows) {
270         print 'No profitable trading opportunities were found.';
271         return;
272 }
273
274 foreach my $f (@flows) {
275
276         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
277                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
278         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
279         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
280
281         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
282         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
283         foreach my $v (qw(Mass Volume)) {
284                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
285         }
286
287         $f->{MarginSortKey}= sprintf "%d",
288                 $f->{'dst_price'} * 10000 / $f->{'org_price'};
289         $f->{Margin}= sprintf "%3.1f%%",
290                 $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
291
292         $f->{ExpectedUnitProfit}=
293                 $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
294                 - $f->{'org_price'};
295
296         $dists{'org_id'}{'dst_id'}= $f->{'dist'};
297
298         my @uid= $f->{commodid};
299         foreach my $od (qw(org dst)) {
300                 push @uid,
301                         $f->{"${od}_id"},
302                         $f->{"${od}_price"};
303                 push @uid,
304                         $f->{"${od}_stallid"}
305                                 if $qa->{ShowStalls};
306         }
307         $f->{UidLong}= join '_', @uid;
308
309         my $base= 31;
310         my $cmpu= '';
311         map {
312                 my $uue= $_;
313                 my $first= $base;
314                 do {
315                         my $this= $uue % $base;
316 print STDERR "uue=$uue this=$this ";
317                         $uue -= $this;
318                         $uue /= $base;
319                         $this += $first;
320                         $first= 0;
321                         $cmpu .= chr($this + ($this < 26 ? ord('a') :
322                                               $this < 52 ? ord('A')-26
323                                                          : ord('0')-52));
324 print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
325 die "$cmpu $uue ?" if length $cmpu > 20;
326                 } while ($uue);
327                 $cmpu;
328         } @uid;
329         $f->{UidShort}= $cmpu;
330
331         if ($qa->{'debug'}) {
332                 my @outuid;
333                 $_= $f->{UidShort};
334                 my $mul;
335                 while (m/./) {
336                         my $v= m/^[a-z]/ ? ord($&)-ord('a') :
337                                m/^[A-Z]/ ? ord($&)-ord('A')+26 :
338                                m/^[0-9]/ ? ord($&)-ord('0')+52 :
339                                die "$_ ?";
340                         if ($v >= $base) {
341                                 push @outuid, 0;
342                                 $v -= $base;
343                                 $mul= 1;
344 #print STDERR "(next)\n";
345                         }
346                         die "$f->{UidShort} $_ ?" unless defined $mul;
347                         $outuid[$#outuid] += $v * $mul;
348
349 #print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
350 #                       "[vs.".ord('a').",".ord('A').",".ord('0')."]".
351 #                       "  outuid=@outuid\n";
352
353                         $mul *= $base;
354                         s/^.//;
355                 }
356                 my $recons_long= join '_', @outuid;
357                 $f->{UidLong} eq $recons_long or
358                         die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
359         }
360
361         if (defined $qa->{"R$f->{UidShort}"} &&
362             !defined $qa->{"T$f->{UidShort}"}) {
363                 $f->{Suppress}= 1;
364         }
365
366 }
367 </%perl>
368
369 % my $optimise= $specific && !$confusing && @islandids>1;
370 % if (!$optimise) {
371
372 <p>
373 % if (@islandids<=1) {
374 Route contains only one location.
375 % }
376 % if (!$specific) {
377 Route contains archipelago(es), not just specific islands.
378 % }
379 % if ($confusing) {
380 Route is complex - it visits the same island several times
381 and isn't a simple loop.
382 % }
383 Therefore, optimal voyage trade plan not calculated.
384
385 % } else { # ========== OPTMISATION ==========
386 <%perl>
387
388 my $cplex= "
389 Maximize
390
391   totalprofit:
392                   ".(join "
393                   ", map {
394                         sprintf "%+.20f %s", $_->{ExpectedUnitProfit}, $_->{Var}
395                         } @flows)."
396
397 Subject To
398 ";
399
400 my %avail_csts;
401 foreach my $flow (@flows) {
402         if ($flow->{Suppress}) {
403                 $cplex .= "
404    $flow->{Var} = 0
405 ";
406                 next;
407         }
408         foreach my $od (qw(org dst)) {
409                 my $cstname= join '_', (
410                         'avail',
411                         $flow->{'commodid'},
412                         $od,
413                         $flow->{"${od}_id"},
414                         $flow->{"${od}_price"},
415                         $flow->{"${od}_stallid"},
416                 );
417                         
418                 push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var};
419                 $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty_agg"};
420         }
421 }
422 foreach my $cstname (sort keys %avail_csts) {
423         my $c= $avail_csts{$cstname};
424         $cplex .= "
425    ". sprintf("%-30s","$cstname:")." ".
426         join("+", @{ $c->{Flows} }).
427         " <= ".$c->{Qty}."\n";
428 }
429
430 foreach my $ci (0..($#islandids-1)) {
431         my @rel_flows;
432         foreach my $f (@flows) {
433                 next if $f->{Suppress};
434                 next if $f->{'org_id'} == $f->{'dst_id'};
435                 next unless grep { $f->{'org_id'} == $_ }
436                         @islandids[0..$ci];
437                 next unless grep { $f->{'dst_id'} == $_ }
438                         @islandids[$ci+1..@islandids-1];
439                 push @rel_flows, $f;
440 #print " RELEVANT $ci $f->{Ix}  ";
441         }
442 #print " RELEVANT $ci COUNT ".scalar(@rel_flows)."  ";
443         next unless @rel_flows;
444         foreach my $mv (qw(mass volume)) {
445                 my $max_vn= "max_$mv";
446                 my $max= $mv eq 'mass' ? $max_mass : $max_volume;
447                 $max= 1e9 unless defined $max;
448 #print " DEFINED MAX $mv $max ";
449                 $cplex .= "
450    ". sprintf("%-10s","${mv}_$ci:")." ".
451         join(" + ", map { ($_->{"unit$mv"}*1e-3).' f'.$_->{Ix} } @rel_flows).
452         " <= $max";
453         }
454         $cplex.= "\n";
455 }
456
457 $cplex.= "
458 Bounds
459         ".(join "
460         ", map { "$_->{Var} >= 0" } @flows)."
461
462 ";
463
464 $cplex.= "
465 Integer
466         ".(join "
467         ", map { "f$_" } (0..$#flows))."
468
469 End
470 ";
471
472 if ($qa->{'debug'}) {
473 </%perl>
474 <pre>
475 <% $cplex |h %>
476 </pre>
477 <%perl>
478 }
479
480 {
481         my $input= pipethrough_prep();
482         print $input $cplex or die $!;
483         my $output= pipethrough_run_along($input, undef, 'glpsol',
484                 qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
485         print "<pre>\n" if $qa->{'debug'};
486         my $found_section= 0;
487         my $glpsol_out= '';
488         my $continuation='';
489         while (<$output>) {
490                 $glpsol_out.= $_;
491                 print encode_entities($_) if $qa->{'debug'};
492                 if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
493                         die "$_ $found_section ?" if $found_section>0;
494                         $found_section= 1;
495                         next;
496                 }
497                 next unless $found_section==1;
498                 if (!length $continuation) {
499                         next if !$continuation &&  m/^[- ]+$/;
500                         if (!/\S/) {
501                                 $found_section= 0;
502                                 next;
503                         }
504                         if (m/^ \s* \d+ \s+ \w+ $/x) {
505                                 $continuation= $&;
506                                 next;
507                         }
508                 }
509                 $_= $continuation.$_;
510                 $continuation= '';
511                 my ($varname, $qty) = m/^
512                         \s* \d+ \s+
513                         (\w+) \s+ (?: [A-Z*]+ \s+ )?
514                         ([0-9.]+) \s
515                         /x or die "$_ ?";
516                 if ($varname =~ m/^f(\d+)$/) {
517                         my ($ix) = $1;
518                         my $flow= $flows[$ix] or die;
519                         $flow->{OptQty}= $qty;
520                         $flow->{OptProfit}= $flow->{'unitprofit'} * $qty;
521                         $flow->{OptCapital}= $flow->{OptQty} *
522                                 $flow->{'org_price'};
523                 } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
524                         my ($mv,$ix) = ($1,$2);
525                         $total_massvol[$ix]{$mv}= $qty;
526                 }
527         }
528         print "</pre>\n" if $qa->{'debug'};
529         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
530         pipethrough_run_finish($output,$prerr);
531         map { defined $_->{OptQty} or die "$prerr $_->{Ix}" } @flows;
532 #       map { defined 
533 #       die $prerr if grep { ! } @flows;
534 #       map { die 
535 #       die $prerr if map { 
536 };
537
538 $addcols->({ DoReverse => 1, Special => sub {
539         my ($flow,$col,$v,$spec) = @_;
540         if ($flow->{ExpectedUnitProfit} < 0) {
541                 $spec->{Span}= 3;
542                 $spec->{String}= '(Small margin)';
543                 $spec->{Align}= 'align=center';
544         }
545 } }, qw(
546                 OptQty
547         ));
548 $addcols->({ Total => 0, DoReverse => 1 }, qw(
549                 OptCapital OptProfit
550         ));
551
552 </%perl>
553
554 % } # ========== OPTIMISATION ==========
555
556 % my %ts_sortkeys;
557 % {
558 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
559 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
560 <table id="trades" rules=groups>
561 <colgroup span=1>
562 <colgroup span=2>
563 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
564 <colgroup span=1>
565 <colgroup span=2>
566 <colgroup span=2>
567 <colgroup span=2>
568 <colgroup span=3>
569 <colgroup span=3>
570 %       if ($optimise) {
571 <colgroup span=3>
572 %       }
573 <tr class="spong">
574 <th>
575 <th<% $cdspan %>>Collect
576 <th<% $cdspan %>>Deliver
577 <th>
578 <th colspan=2>Collect
579 <th colspan=2>Deliver
580 <th colspan=2>Profit
581 <th colspan=3>Max
582 <th colspan=1>
583 <th colspan=2>Max
584 %       if ($optimise) {
585 <th colspan=3>Planned
586 %       }
587
588 <tr>
589 <th>
590 <th>Island <% $cdstall %>
591 <th>Island <% $cdstall %>
592 <th>Commodity
593 <th>Price
594 <th>Qty
595 <th>Price
596 <th>Qty
597 <th>Margin
598 <th>Unit
599 <th>Qty
600 <th>Capital
601 <th>Profit
602 <th>Dist
603 <th>Mass
604 <th>Vol
605 %       if ($optimise) {
606 <th>Qty
607 <th>Capital
608 <th>Profit
609 %       }
610 % }
611
612 <tr id="trades_sort">
613 % foreach my $col (@cols) {
614 <th>
615 % }
616
617 % foreach my $flowix (0..$#flows) {
618 %       my $flow= $flows[$flowix];
619 %       my $rowid= "id_row_$flow->{UidShort}";
620 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
621 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
622     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
623        <% $flow->{Suppress} ? '' : 'checked' %> >
624 %       my $ci= 1;
625 %       while ($ci < @cols) {
626 %               my $col= $cols[$ci];
627 %               my $spec= {
628 %                       Span => 1,
629 %                       Align => ($col->{Text} ? '' : 'align=right')
630 %               };
631 %               my $v= $flow->{$col->{Name}};
632 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
633 %               $col->{Total} += $v
634 %                       if defined $col->{Total} and not $flow->{Suppress};
635 %               $v='' if !$col->{Text} && !$v;
636 %               my $sortkey= $col->{SortColKey} ?
637 %                       $flow->{$col->{SortColKey}} : $v;
638 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
639 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
640  %> <% $spec->{Align}
641  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
642 %               $ci += $spec->{Span};
643 %       }
644 % }
645 <tr id="trades_total">
646 <th>
647 <th colspan=2>Total
648 % foreach my $ci (3..$#cols) {
649 %       my $col= $cols[$ci];
650 <td align=right>
651 %       if (defined $col->{Total}) {
652 <% $col->{Total} |h %>
653 %       }
654 % }
655 </table>
656
657 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
658         throw => 'trades_sort', tbrow => 'trades_total' &>
659   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
660 </&tabsort>
661
662 <input type=submit name=update value="Update">
663
664 % if ($optimise) { # ========== TRADING PLAN ==========
665 %
666 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
667 %                               WHERE islandid = ?');
668 % my %da_ages;
669 % my $total_total= 0;
670 % my $total_dist= 0;
671 %
672 <h1>Voyage trading plan</h1>
673 <table rules=groups>
674 % foreach my $i (0..$#islandids) {
675 <tbody>
676 <tr><td colspan=3>
677 %       $iquery->execute($islandids[$i]);
678 %       my ($islandname) = $iquery->fetchrow_array();
679 %       if (!$i) {
680 <strong>Start at <% $islandname |h %></strong>
681 %       } else {
682 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
683 %               $total_dist += $this_dist;
684 <strong>Sail to <% $islandname |h %></strong>
685 - <% $this_dist |h %> leagues </td>
686 %       }
687 <%perl>
688      my $age_reported= 0;
689      my %flowlists;
690      foreach my $od (qw(org dst)) {
691         foreach my $f (@flows) {
692                 next if $f->{Suppress};
693                 next unless $f->{"${od}_id"} == $islandids[$i];
694                 next unless $f->{OptQty};
695                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
696                 my $loop= $islandids[0] == $islandids[-1] &&
697                           ($i==0 || $i==$#islandids);
698                 next if $loop and ($arbitrage ? $i :
699                         !!$i == !!($od eq 'org'));
700                 my $price= $f->{"${od}_price"};
701                 my $stallname= $f->{"${od}_stallname"};
702                 my $todo= \$flowlists{$od}{
703                                 $f->{'commodname'},
704                                 (sprintf "%07d", ($od eq 'dst' ?
705                                                 9999999-$price : $price)),
706                                 $stallname
707                         };
708                 $$todo= {
709                         Qty => 0,
710                         orgArbitrage => 0,
711                         dstArbitrage => 0,
712                 } unless $$todo;
713                 $$todo->{'commodname'}= $f->{'commodname'};
714                 $$todo->{'stallname'}= $stallname;
715                 $$todo->{Price}= $price;
716                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
717                 $$todo->{Qty} += $f->{OptQty};
718                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
719                 $$todo->{Stalls}= $f->{"${od}Stalls"};
720                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
721         }
722      }
723
724      my ($total, $total_to_show);
725      my $dline= 0;
726      my $show_total= sub {
727         my ($totaldesc, $sign) = @_;
728         if (defined $total) {
729                 die if defined $total_to_show;
730                 $total_total += $sign * $total;
731                 $total_to_show= [ $totaldesc, $total ];
732                 $total= undef;
733         }
734         $dline= 0;
735      };
736      my $show_total_now= sub {
737         my ($xinfo) = @_;
738         return unless defined $total_to_show;
739         my ($totaldesc,$totalwas) = @$total_to_show;
740 </%perl>
741 <tr>
742 <td colspan=1>
743 <td colspan=2><% $xinfo %>
744 <td colspan=2 align=right><% $totaldesc %>
745 <td align=right><% $totalwas |h %> total
746 <%perl>
747         $total_to_show= undef;
748      };
749 </%perl>
750 %    my $show_flows= sub {
751 %       my ($od,$arbitrage,$collectdeliver) = @_;
752 %       my $todo= $flowlists{$od};
753 %       return unless $todo;
754 %       foreach my $tkey (sort keys %$todo) {
755 %               my $t= $todo->{$tkey};
756 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
757 %               $show_total_now->('');
758 %               if (!$age_reported++) {
759 %                       my $age= $now - $t->{Timestamp};
760 %                       my $cellid= "da_${i}";
761 %                       $da_ages{$cellid}= $age;
762 <td colspan=3>\
763 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
764 %               } elsif (!defined $total) {
765 %                       $total= 0;
766 <tbody>
767 %               }
768 %               $total += $t->{Total};
769 %               my $span= 0 + keys %{ $t->{Stalls} };
770 %               my $td= "td rowspan=$span";
771 <tr class="datarow<% $dline %>">
772 <<% $td %>><% $collectdeliver %>
773 <<% $td %>><% $t->{'commodname'} |h %>
774 %
775 %               my @stalls= sort keys %{ $t->{Stalls} };
776 %               my $pstall= sub {
777 %                       my $name= $stalls[$_[0]];
778 <td><% $name |h %>
779 %               };
780 %
781 %               $pstall->(0);
782 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
783 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
784 <<% $td %> align=right><% $t->{Total} |h %> total
785 %
786 %               foreach my $stallix (1..$#stalls) {
787 <tr class="datarow<% $dline %>">
788 %                       $pstall->($stallix);
789 %               }
790 %
791 %               $dline ^= 1;
792 %       }
793 %    };
794 <%perl>
795
796      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
797      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
798      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
799      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
800      my $totals= '';
801      if ($i < $#islandids) {
802         $totals .=      "In hold $total_massvol[$i]{mass} kg,".
803                         " $total_massvol[$i]{volume} l";
804         my $delim= '; spare ';
805         my $domv= sub {
806                 my ($max, $got, $units) = @_;
807                 return unless defined $max;
808                 $totals .= $delim;
809                 $totals .= sprintf "%g %s", ($max-$got), $units;
810                 $delim= ', ';
811         };
812         $domv->($max_mass,   $total_massvol[$i]{mass},   'kg');
813         $domv->($max_volume, $total_massvol[$i]{volume}, 'l');
814         $totals .= ".\n";
815      }
816      $show_total_now->($totals);
817 }
818 </%perl>
819 <tbody><tr>
820 <td colspan=2>Total distance: <% $total_dist %> leagues.
821 <td colspan=3 align=right>Overall net cash flow
822 <td align=right><strong><%
823   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
824  %></strong>
825 </table>
826 <& query_age:dataages, id2age => \%da_ages &>
827 %
828 % } # ========== TRADING PLAN ==========
829
830 <%init>
831 use CommodsWeb;
832 use Commods;
833 </%init>