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