]> git.sesse.net Git - ultimatescore/blob - score.html
Add some code for a score carousel (commented out for now).
[ultimatescore] / score.html
1 <!DOCTYPE html>
2 <html>
3  <head>
4    <meta http-equiv="content-type" value="text/html; charset=utf-8" />
5    <meta charset="utf-8" />
6 <!--   <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"> -->
7    <link href="fonts/lato.css" rel="stylesheet">
8    <style>
9 * {
10   -webkit-box-sizing: border-box;
11   Box-Sizing: border-box;
12   -webkit-backface-visibility: hidden;
13   -webkit-transition: translate3d(0,0,0);
14 }
15 html, body {
16   width: 1280px;
17   height: 720px;
18   margin: 0;
19   padding: 0;
20   background: transparent;
21   overflow: hidden;
22   -webkit-font-smoothing: antialiased !important;
23 }
24 body {
25   font-family: 'Lato', sans-serif;
26   color: white;
27 }
28
29 /* Score */
30 .scorebug {
31   position: fixed;
32   font-size: 25px;
33   left: 10px;
34   top: 10px;
35   border-collapse: collapse;
36   white-space: nowrap;
37   border: 3px solid black;
38   z-index: 1;
39 }
40 .scorebug td {
41   border: 3px solid #008;
42 }
43 .team1color, .team2color {
44   width: 15px;
45   margin: 5px;
46 }
47 .team1color {
48   background: red;
49 }
50 .team2color {
51   background: green;
52 }
53 .team1, .team2 {
54   font-weight: bold;
55   width: 100px;
56   text-align: center;
57   height: 35px;
58   background: #00a;
59 }
60 .score {
61   text-align: center;
62   background: #00c;
63   width: 110px;
64   height: 35px;
65 }
66
67 /* Clock, to the right of score */
68 .clockbug {
69   position: fixed;
70   font-size: 25px;
71   left: 360px;
72   top: 10px;
73   border-collapse: collapse;
74   white-space: nowrap;
75   border: 3px solid black;
76 }
77 .clock {
78   border: 3px solid #008;
79   background: #00a;
80   text-align: center;
81   height: 35px;
82   width: 90px;
83 }
84 .clockbug-hidden {
85   -webkit-transform:translateX(-200%);
86 }
87 .clockbug-animate-in {
88   -webkit-animation: from-left 1s ease;
89 }
90 .clockbug-animate-out {
91   -webkit-animation: to-left 1s ease;
92   -webkit-transform:translateX(-200%);
93 }
94
95 /* Comment, below score */
96 .commentbug {
97   position: fixed;
98   font-size: 20px;
99   left: 10px;
100   top: 52px;
101   border-collapse: collapse;
102   white-space: nowrap;
103   border: 3px solid black;
104 }
105 .comment {
106   border: 3px solid #008;
107   background: #00a;
108   text-align: center;
109   height: 30px;
110   width: 340px;
111 }
112
113 .commentbug-hidden {
114   -webkit-transform:translateY(-40px);
115 }
116 .commentbug-animate-in {
117   -webkit-animation: from-top 0.5s ease;
118 }
119 .commentbug-animate-out {
120   -webkit-animation: to-top 0.5s ease;
121   -webkit-transform:translateY(-40px);
122 }
123
124 /* Lower third */
125 .lowerthird-headline {
126   position: fixed;
127   font-size: 40px;
128   left: 10px;
129   top: 520px;
130   border-collapse: collapse;
131   white-space: nowrap;
132   border: 2px solid #ccc;
133   height: 118px;
134   font-weight: bold;
135   width: 1050px;
136   /*background: linear-gradient(to right, #ccc, #fff 15px); */
137   background-image: url(lowerthird-bg.png);
138   color: black;
139   /* padding-top: 20px; */
140 }
141 .lowerthird-headline-hidden {
142   width: 0px;
143   border-left: 0px;
144   border-right: 0px;
145 }
146 .lowerthird-headline-animate-in {
147   -webkit-animation: scale-out 1.0s;
148 }
149 .lowerthird-headline-animate-out {
150   -webkit-animation: scale-in 1.0s;
151 }
152 .lowerthird-headline-content {
153   padding-left: 20px;
154   position: absolute;
155 }
156 .lowerthird-headline-content-hidden {
157   clip: rect(0px,0px,200px,0px);
158 }
159 .lowerthird-headline-content-animate-in {
160   -webkit-animation: wipe-out 2.0s ease;
161 }
162 .lowerthird-headline-content-animate-out {
163   -webkit-animation: wipe-in 2.0s ease;
164   clip: rect(0px,0px,200px,0px);
165 }
166 .lowerthird-subheading {
167   position: fixed;
168   font-size: 24px;
169   left: 40px;
170   top: 637px;
171   height: 40px;
172   width: 500px;
173   border-collapse: collapse;
174   white-space: nowrap;
175   border: 1px solid black;
176   /*display: flex;
177   align-items: center; */
178   /*background: linear-gradient(to right, #44c, #33a 15px); */
179   background-image: url(lowerthird-bg2.png);
180 }
181 .lowerthird-subheading-hidden {
182   clip: rect(0px,0px,200px,0px);
183 }
184 .lowerthird-subheading-animate-in {
185   -webkit-animation: scale-out-small 1.6s ease;
186 }
187 .lowerthird-subheading-animate-out {
188   -webkit-animation: scale-in-small 1.6s ease;
189   width: 0px;
190   border-left: 0px;
191 }
192 .lowerthird-subheading-content {
193   position: absolute;
194   padding-left: 20px;
195 }
196 .lowerthird-subheading-content-hidden {
197   width: 0px;
198   border: 0px;
199 }
200 .lowerthird-subheading-content-animate-in {
201   -webkit-animation: wipe-out 2.2s ease;
202 }
203 .lowerthird-subheading-content-animate-out {
204   -webkit-animation: wipe-in 2.2s ease;
205   clip: rect(0px,0px,200px,0px);
206 }
207
208 /* these are hidden when actually run in casparcg */
209 #area {
210   position: fixed;
211   left: 0px;
212   top: 0px;
213   width: 1280px;
214   height: 720px;
215   background: cyan;
216 }
217 #carousel {
218   z-index: 3;
219   position: fixed;
220   top: 250px;
221   font-size: 50px;
222   /* background: rgba(0, 0, 170, 0.8); */
223   background: transparent;
224   top: 20px;
225   left: 50px;
226   right: 50px;
227   width: 1180px;
228   border-spacing: 0px 7px;
229   text-align: left;
230   border-collapse: separate;
231   text-transform: uppercase;
232   display: none;
233 }
234 #carousel thead tr {
235   text-transform: none;
236   text-align: center;
237 }
238 #carousel thead th {
239   text-transform: none;
240   text-align: center;
241   font-size: 50px;
242   -webkit-animation: fade-in 1.0s ease;
243   -webkit-animation-delay: 0.0s;
244   -webkit-animation-fill-mode: both;
245 }
246 #carousel tr.subfooter {
247   -webkit-animation: fade-in 1.0s ease;
248   -webkit-animation-delay: 0.25s;
249   -webkit-animation-fill-mode: both;
250 }
251 #carousel tr {
252   /* background: rgba(0, 0, 170, 0.8); */
253   background: linear-gradient(to bottom, rgba(0, 0, 170, 0.8), rgba(0, 0, 130, 0.8));
254   /* -webkit-animation: fade-in calc(counter(rowNumber) * 1.0)s ease; */
255   -webkit-animation: fade-in calc(counter-value(rowNumber) * 1.0s) ease;
256 }
257 #carousel tr.footer {
258   -webkit-animation: fade-in 2.0s ease;
259   -webkit-animation-delay: 0.5s;
260   -webkit-animation-fill-mode: both;
261 }
262 #carousel td, #carousel th {
263   padding: 8px;
264   font-size: 40px;
265 }
266 #carousel tr.footer td {
267   font-size: 20px;
268   text-transform: none;
269   text-align: center;
270 }
271 #carousel th.rank {
272   padding-left: 20px;
273 }
274 #carousel td.team {
275   width: auto;
276 }
277 .nplayed, .gd, .pts {
278   text-align: center;
279 }
280
281 #manualcontrols {
282   z-index: 4;
283   position: fixed;
284   top: 250px;
285 }
286
287 /* Animations */
288 @-webkit-keyframes from-left {
289   0% {
290     -webkit-transform:translateX(-200%);
291   }
292   100% {
293     -webkit-transform:translateX(0);
294   }
295 }
296 @-webkit-keyframes to-left {
297   0% {
298     -webkit-transform:translateX(0);
299   }
300   100% {
301     -webkit-transform:translateX(-200%);
302   }
303 }
304 @-webkit-keyframes from-top {
305   0% {
306     -webkit-transform:translateY(-40px);
307   }
308   100% {
309     -webkit-transform:translateY(0);
310   }
311 }
312 @-webkit-keyframes to-top {
313   0% {
314     -webkit-transform:translateY(0);
315   }
316   100% {
317     -webkit-transform:translateY(-40px);
318   }
319 }
320
321 @-webkit-keyframes scale-out {
322   0% {
323     width: 0px;
324   }
325   100% {
326     width: 1050px;
327   }
328 }
329 @-webkit-keyframes scale-in {
330   0% {
331     width: 1050px;
332     border: 2px solid #ccc;
333   }
334   100% {
335     width: 0px;
336     border: 2px solid #ccc;
337   }
338 }
339 @-webkit-keyframes scale-out-small {
340   0% {
341     width: 0px;
342     border: 0px;
343   }
344   19.99% {
345     border: 0px;
346   }
347   20% {
348     width: 0px;
349     border: 1px solid black;
350   }
351   100% {
352     width: 500px;
353   }
354 }
355 @-webkit-keyframes scale-in-small {
356   0% {
357     width: 500px;
358     border: 1px solid black;
359   }
360   80% {
361     width: 0px;
362     border: 1px solid black;
363   }
364   80.01% {
365     border: 0px;
366   }
367   100% {
368     width: 0px;
369     border: 0px;
370   }
371 }
372 @-webkit-keyframes wipe-out {
373   0% {
374     clip: rect(0px,0px,200px,0px);
375   }
376   20% {
377     clip: rect(0px,0px,200px,0px);
378   }
379   100% {
380     clip: rect(0px,1050px,200px,0px);
381   }
382 }
383 @-webkit-keyframes wipe-in {
384   0% {
385     clip: rect(0px,1050px,200px,0px);
386   }
387   20% {  /* Not symmetrical! */
388     clip: rect(0px,0px,200px,0px);
389   }
390   100% {
391     clip: rect(0px,0px,200px,0px);
392   }
393 }
394 @-webkit-keyframes fade-in {
395   0% {
396     opacity: 0.0001;
397   }
398   100% {
399     opacity: 1;
400   }
401 }
402    </style>
403   </head>
404   <body>
405     <div id="area"></div>
406     <table class="scorebug">
407       <tr>
408         <td class="team1color" id="team1color"></td>
409         <td class="team1" id="team1">PCL</td>
410         <td class="score" id="score">17&nbsp;–&nbsp;11</td>
411         <td class="team2" id="team2">NMBUI</td>
412         <td class="team2color" id="team2color"></td>
413       </tr>
414     </table>
415     <table class="clockbug clockbug-hidden" id="clockbug">
416       <tr>
417         <td class="clock" id="clock">25:00</td>
418       </tr>
419     </table>
420     <table class="commentbug commentbug-hidden" id="commentbug">
421       <tr>
422         <td class="comment" id="comment">Pagacap: First to 9 points</td>
423       </tr>
424     </table>
425     <div class="lowerthird-headline lowerthird-headline-hidden" id="lowerthird-headline"><div class="lowerthird-headline-content lowerthird-headline-content-hidden" id="lowerthird-headline-content">
426       John Doe<br>Ola Nordmann
427     </div></div>
428     <div class="lowerthird-subheading lowerthird-subheading-hidden" id="lowerthird-subheading"><div class="lowerthird-subheading-content lowerthird-subheading-content-hidden" id="lowerthird-subheading-content">
429       Commentators, Trøndisk 2016
430     </div></div>
431     <table id="carousel">
432       <thead>
433         <tr>
434           <th colspan="5">Current standings, TrønDisk 2017<br />Group A</th>
435         </tr> 
436       </thead>
437       <tr class="subfooter">
438         <th class="rank"></th>
439         <th class="team"></th>
440         <th class="nplayed">P</th>
441         <th class="gd">GD</th>
442         <th class="pts">Pts</th>
443       </tr>
444 <!--
445       <tr>
446         <th class="rank">1</th>
447         <td class="team">Trondheim Frisbeeklubb</td>
448         <td class="nplayed">3</td>
449         <td class="gd">+7</td>
450         <td class="pts">6</td>
451       </tr>
452       <tr>
453         <th class="rank">2</th>
454         <td class="team">Pancake Circle Ltd.</td>
455         <td class="nplayed">2</td>
456         <td class="gd">+2</td>
457         <td class="pts">4</td>
458       </tr>
459       <tr>
460         <th class="rank">3</th>
461         <td class="team">Norges Handelshøyskole 1</td>
462         <td class="nplayed">2</td>
463         <td class="gd">-2</td>
464         <td class="pts">2</td>
465       </tr>
466       <tr>
467         <th class="rank">3</th>
468         <td class="team">Pick-up Team</td>
469         <td class="nplayed">2</td>
470         <td class="gd">-2</td>
471         <td class="pts">2</td>
472       </tr>
473       <tr>
474         <th class="rank">5</th>
475         <td class="team">Whatever Team</td>
476         <td class="nplayed">3</td>
477         <td class="gd">-11</td>
478         <td class="pts">0</td>
479       </tr>
480       <tr class="footer">
481         <td colspan="5">www.trondheimfrisbeeklubb.no | #trøndisk</td>
482       </tr>
483 -->
484     </table>
485     <div id="manualcontrols">
486       <p>
487         <a href="javascript:startclock()">start clock</a>
488         <a href="javascript:stopclock()">stop clock</a>
489         <a href="javascript:setclock(25*60)">reset clock</a>
490         <a href="javascript:showclock()">show clock</a>
491         <a href="javascript:hideclock()">hide clock</a>
492       </p>
493       <p>
494         <a href="javascript:goalA()">goal team A</a>
495         <a href="javascript:goalB()">goal team B</a>
496         <a href="javascript:ungoalA()">ungoal team A</a>
497         <a href="javascript:ungoalB()">ungoal team B</a>
498         <a href="javascript:resetscore()">reset score</a>
499       </p>
500       <p>
501         <a href="javascript:showcomment()">show comment</a>
502         <a href="javascript:hidecomment()">hide comment</a>
503       </p>
504       <p>
505         <a href="javascript:showlowerthird()">show lower third</a>
506         <a href="javascript:hidelowerthird()">hide lower third</a>
507       </p>
508     </div>
509
510 <script>
511 var clock_running = false;
512 var clock_visible = false;
513 var comment_visible = false;
514 var lowerthird_visible = false;
515 var clock_left = 25 * 60;
516 var scoreA = 0;
517 var scoreB = 0;
518 var clock_origin;
519 var state = {};
520
521 function setteams()
522 {
523         document.getElementById('team1').innerHTML = state['team1'];
524         document.getElementById('team2').innerHTML = state['team2'];
525 }
526
527 function setcolors()
528 {
529         document.getElementById('team1color').style.backgroundColor = state['team1color'];
530         document.getElementById('team2color').style.backgroundColor = state['team2color'];
531 }
532
533 function setscore()
534 {
535         scoreA = state['score1'];
536         scoreB = state['score2'];
537         update_score();
538 }
539
540 function startclock()
541 {
542         if (!clock_running) {
543                 clock_origin = Date.now();
544                 clock_running = true;
545         }
546         showclock();
547 }
548
549 function stopclock()
550 {
551         if (!clock_running) return;
552         clock_left = time_left();
553         clock_origin = Date.now();
554         clock_running = false;
555 }
556
557 function setclock(amount)
558 {
559         clock_left = amount;
560         clock_origin = Date.now();
561         update_clock();
562 }
563
564 function setclockfromstate()
565 {
566         var amount = parseInt(state['clock_min']) * 60 + parseInt(state['clock_sec']);
567         setclock(amount);
568 }
569
570 function showclock()
571 {
572         if (clock_visible) return;
573         var clockbug = document.getElementById('clockbug');
574         clockbug.className = 'clockbug clockbug-animate-in';
575         clock_visible = true;
576 }
577
578 function hideclock()
579 {
580         if (!clock_visible) return;
581         var clockbug = document.getElementById('clockbug');
582         clockbug.className = 'clockbug clockbug-animate-out';
583         clock_visible = false;
584 }
585
586 function setcomment()
587 {
588         document.getElementById('comment').innerHTML = state['comment'];
589 }
590
591 function showcomment()
592 {
593         if (comment_visible) return;
594         var commentbug = document.getElementById('commentbug');
595         commentbug.className = 'commentbug commentbug-animate-in';
596         comment_visible = true;
597 }
598
599 function hidecomment()
600 {
601         if (!comment_visible) return;
602         var commentbug = document.getElementById('commentbug');
603         commentbug.className = 'commentbug commentbug-animate-out';
604         comment_visible = false;
605 }
606
607 function showlowerthird()
608 {
609         if (lowerthird_visible) return;
610
611         // With no flexbox, this is how it has to be...
612         var f = document.getElementById('lowerthird-headline');
613         var g = document.getElementById('lowerthird-headline-content');
614         f.style.paddingTop = Math.round((f.clientHeight - g.clientHeight) / 2) + 'px';
615
616         f = document.getElementById('lowerthird-subheading');
617         g = document.getElementById('lowerthird-subheading-content');
618         f.style.paddingTop = Math.round((f.clientHeight - g.clientHeight) / 2) + 'px';
619
620         document.getElementById('lowerthird-headline').className = 'lowerthird-headline lowerthird-headline-animate-in';
621         document.getElementById('lowerthird-headline-content').className = 'lowerthird-headline-content lowerthird-headline-content-animate-in';
622         document.getElementById('lowerthird-subheading').className = 'lowerthird-subheading lowerthird-subheading-animate-in';
623         document.getElementById('lowerthird-subheading-content').className = 'lowerthird-subheading-content lowerthird-subheading-content-animate-in';
624         lowerthird_visible = true;
625 }
626
627 function setandshowlowerthird()
628 {
629         document.getElementById('lowerthird-headline-content').innerHTML = state['text1'];
630         document.getElementById('lowerthird-subheading-content').innerHTML = state['text2'];
631         showlowerthird();
632 }
633
634 function hidelowerthird()
635 {
636         if (!lowerthird_visible) return;
637         document.getElementById('lowerthird-headline').className = 'lowerthird-headline lowerthird-headline-hidden lowerthird-headline-animate-out';
638         document.getElementById('lowerthird-headline-content').className = 'lowerthird-headline-content lowerthird-headline-content-animate-out';
639         document.getElementById('lowerthird-subheading').className = 'lowerthird-subheading lowerthird-subheading-animate-out';
640         document.getElementById('lowerthird-subheading-content').className = 'lowerthird-subheading-content lowerthird-subheading-content-animate-out';
641         lowerthird_visible = false;
642 }
643
644 function time_left()
645 {
646         var elapsed = (Date.now() - clock_origin) * 1e-3;
647         if (elapsed > clock_left) return 0;
648         return Math.ceil(clock_left - elapsed);
649 }
650
651 function update_clock()
652 {
653         var left = time_left();
654         var min = Math.floor(left / 60);
655         var sec = left % 60;
656
657         if (sec < 10) sec = "0" + sec;
658         document.getElementById('clock').innerHTML = min + ":" + sec;
659 }
660
661 function goalA()
662 {
663         ++scoreA;
664         update_score();
665 }
666
667 function goalB()
668 {
669         ++scoreB;
670         update_score();
671 }
672
673 function ungoalA()
674 {
675         if (scoreA > 0) --scoreA;
676         update_score();
677 }
678
679 function ungoalB()
680 {
681         if (scoreB > 0) --scoreB;
682         update_score();
683 }
684
685 function resetscore()
686 {
687         scoreA = scoreB = 0;
688         update_score();
689 }
690
691 function update_score()
692 {
693         document.getElementById('score').innerHTML = scoreA + "&nbsp;–&nbsp;" + scoreB;
694 }
695
696 /* called by caspar only */
697 function play()
698 {
699         document.getElementById('manualcontrols').style.display = 'none';
700         document.getElementById('area').style.display = 'none';
701
702         // Old CEF workaround
703         document.getElementById('lowerthird-subheading').style.top = '638px';
704 }
705
706 function update(v)
707 {
708         console.log('[[[' + v + ']]]');
709         var j = JSON.parse(v);
710         for(var key in j) state[key] = j[key];
711 }
712
713 setInterval(function() {
714         if (clock_running) {
715                 update_clock();
716         }
717 }, 100);
718 update_score();
719
720 //play();
721 //startclock();
722
723 addtd = function(tr, className, content) {
724   var td = document.createElement("td");
725   td.appendChild(document.createTextNode(content));
726   td.className = className;
727   tr.appendChild(td);
728 };
729 addth = function(tr, className, content) {
730   var th = document.createElement("th");
731   th.appendChild(document.createTextNode(content));
732   th.className = className;
733   tr.appendChild(th);
734 };
735
736 subrank_partitions = function(games, parts, start_rank, tiebreakers) {
737   var result = [];
738   for (var i = 0; i < parts.length; ++i) {
739     var part = rank(games, parts[i], start_rank, tiebreakers);
740     for (var j = 0; j < part.length; ++j) {
741       result.push(part[j]);
742     }
743     start_rank += part.length;
744   }
745   return result;
746 };
747
748 partition = function(teams, compare)
749 {
750   teams.sort(compare);
751
752   var parts = [];
753   var curr_part = [teams[0]];
754   for (var i = 1; i < teams.length; ++i) {
755     if (compare(teams[i], curr_part[0]) != 0) {
756       parts.push(curr_part);
757       curr_part = [];
758     }
759     curr_part.push(teams[i]);
760   }
761   if (curr_part.length != 0) {
762     parts.push(curr_part);
763   }
764   return parts;
765 };
766
767 explain_tiebreaker = function(parts, rule_name)
768 {
769   var result = [];
770   for (var i = 0; i < parts.length; ++i) {
771     result.push(parts[i].map(function(x) { return x.shortname; }).join("/"));
772   }
773   return result.join(" > ") + " (" + rule_name + ")";
774 }
775
776 make_teams_to_idx = function(teams)
777 {
778   var teams_to_idx = [];
779   for (var i = 0; i < teams.length; i++) {
780     teams_to_idx[teams[i].name] = i;
781   }
782   return teams_to_idx;
783 }
784
785 partition_by_beat = function(games, teams)
786 {
787   // Head-to-head score by way of components. First construct the beat matrix.
788   var n = teams.length;
789   var beat = new Array(n);
790   var teams_to_idx = make_teams_to_idx(teams);
791   for (var i = 0; i < n; i++) {
792     beat[i] = new Array(n);
793     for (var j = 0; j < n; j++) {
794       beat[i][j] = 0;
795     }
796   }
797   for (i = 0; i < games.length; ++i) {
798     var idx1 = teams_to_idx[games[i].name1];
799     var idx2 = teams_to_idx[games[i].name2];
800     if (idx1 !== undefined && idx2 !== undefined) {
801       if (games[i].score1 > games[i].score2) {
802         beat[idx1][idx2] = 1;
803       }
804       if (games[i].score1 < games[i].score2) {
805         beat[idx2][idx1] = 1;
806       }
807     }
808   }
809   // Floyd-Warshall for transitive closure.
810   for (var k = 0; k < n; ++k) {
811     for (var i = 0; i < n; ++i) {
812       for (var j = 0; j < n; ++j) {
813         if (beat[i][k] && beat[k][j]) {
814           beat[i][j] = 1;
815         }
816       }
817     }
818   }
819
820   // See if we can find any team that is comparable to all others.
821   for (var pivot_idx = 0; pivot_idx < n; pivot_idx++) {
822     var incomparable = false;
823     for (var i = 0; i < n; ++i) {
824       if (i != pivot_idx && beat[pivot_idx][i] == 0 && beat[i][pivot_idx] == 0) {
825         incomparable = true;
826         break;
827       }
828     }
829     if (!incomparable) {
830       // Split the teams into three partitions:
831       var better_than_pivot = [], equal = [], worse_than_pivot = [];
832       for (var i = 0; i < n; ++i) {
833         var we_beat = (beat[pivot_idx][i] == 1);
834         var they_beat = (beat[i][pivot_idx] == 1);
835         if ((i == pivot_idx) || (we_beat && they_beat)) {
836           equal.push(teams[i]);
837         } else if (we_beat && !they_beat) {
838           worse_than_pivot.push(teams[i]);
839         } else if (they_beat && !we_beat) {
840           better_than_pivot.push(teams[i]);
841         } else {
842           console.log("this shouldn't happen");
843         }
844       } 
845       var result = [];
846       if (better_than_pivot.length > 0) {
847         result = partition_by_beat(games, better_than_pivot);
848       }
849       result.push(equal);  // Obviously can't be partitioned further.
850       if (worse_than_pivot.length > 0) {
851         result = result.concat(partition_by_beat(games, worse_than_pivot));
852       }
853       return result;
854     }
855   }
856
857   // No usable pivot was found, so the graph is inherently
858   // disconnected, and we cannot partition it.
859   return [teams];
860 }
861
862 // Takes in an array, gives every element a rank starting with 1, and returns.
863 rank = function(games, teams, start_rank, tiebreakers) {
864   if (teams.length <= 1) {
865     // Only one team, so trivial.
866     teams[0].rank = start_rank;
867     return teams;
868   }
869
870   // Rule #0: Partition the teams by score.
871   var score_parts = partition(teams, function(a, b) { return b.pts - a.pts });
872   if (score_parts.length > 1) {
873     return subrank_partitions(games, score_parts, start_rank, tiebreakers);
874   }
875
876   // Rule #1: Head-to-head wins.
877   var beat_parts = partition_by_beat(games, teams);
878   if (beat_parts.length > 1) {
879     tiebreakers.push(explain_tiebreaker(beat_parts, 'head-to-head'));
880     return subrank_partitions(games, beat_parts, start_rank, tiebreakers);
881   }
882
883   // Rule #2: Number of games played (fewer is better).
884   // Actually the rule says “fewest losses”, but fewer games is equivalent
885   // as long as teams have the same amount of points and ties don't exist.
886   var nplayed_parts = partition(teams, function(a, b) { return a.nplayed - b.nplayed });
887   if (nplayed_parts.length > 1) {
888     tiebreakers.push(explain_tiebreaker(nplayed_parts, 'fewer losses'));
889     return subrank_partitions(games, nplayed_parts, start_rank, tiebreakers);
890   }
891
892   // Rule #3: Head-to-head goal difference. 
893   var teams_to_idx = make_teams_to_idx(teams);
894   for (var i = 0; i < teams.length; i++) {
895     teams[i].h2h_gd = 0;
896     teams[i].h2h_goals = 0;
897     teams[i].goals = 0;
898   }
899   for (i = 0; i < games.length; ++i) {
900     var idx1 = teams_to_idx[games[i].name1];
901     var idx2 = teams_to_idx[games[i].name2];
902     if (idx1 !== undefined && idx2 !== undefined) {
903       teams[idx1].h2h_gd += games[i].score1;
904       teams[idx1].h2h_gd -= games[i].score2;
905       teams[idx2].h2h_gd += games[i].score2;
906       teams[idx2].h2h_gd -= games[i].score1;
907
908       teams[idx1].h2h_goals += games[i].score1;
909       teams[idx2].h2h_goals += games[i].score2;
910     }
911     if (idx1 !== undefined) {
912       teams[idx1].goals += games[i].score1;
913     }
914     if (idx2 !== undefined) {
915       teams[idx2].goals += games[i].score2;
916     }
917   }
918   var h2h_gd_parts = partition(teams, function(a, b) { return b.h2h_gd - a.h2h_gd });
919   if (h2h_gd_parts.length > 1) {
920     tiebreakers.push(explain_tiebreaker(h2h_gd_parts, 'head-to-head goal difference'));
921     return subrank_partitions(games, h2h_gd_parts, start_rank, tiebreakers);
922   }
923
924   // Rule #4: Global goal difference. (Well, not strictly, but good enough.)
925   var gd_parts = partition(teams, function(a, b) { return b.gd - a.gd });
926   if (gd_parts.length > 1) {
927     tiebreakers.push(explain_tiebreaker(gd_parts, 'overall goal difference'));
928     return subrank_partitions(games, gd_parts, start_rank, tiebreakers);
929   }
930
931   // Rule #5: Head-to-head scored goals.
932   var h2h_goals_parts = partition(teams, function(a, b) { return b.h2h_goals - a.h2h_goals });
933   if (h2h_goals_parts.length > 1) {
934     tiebreakers.push(explain_tiebreaker(h2h_goals_parts, 'head-to-head scored goals'));
935     return subrank_partitions(games, h2h_goals_parts, start_rank, tiebreakers);
936   }
937
938   // Rule #6: Overall scored goals. (Same caveat as #4.)
939   var goals_parts = partition(teams, function(a, b) { return b.goals - a.goals });
940   if (goals_parts.length > 1) {
941     tiebreakers.push(explain_tiebreaker(goals_parts, 'scored goals'));
942     return subrank_partitions(games, goals_parts, start_rank, tiebreakers);
943   }
944
945   // OK, it's a tie. Give them all the same rank.
946   var result = [];
947   for (var i = 0; i < teams.length; ++i) {
948     result.push(teams[i]);
949     result[i].rank = start_rank;
950   }
951   return result; 
952 }; 
953
954 var req = new XMLHttpRequest();
955 req.onload = function(e) {
956   var response = JSON.parse(req.responseText);
957   console.log(response.values);
958   var carousel = document.getElementById('carousel');
959   var teams = [];
960   for (var i = 2; response.values[i].length >= 1; ++i) {
961     teams.push({
962       "name": response.values[i][0],
963       "shortname": response.values[i][1],
964       "nplayed": parseInt(response.values[i][2]),
965       "gd": parseInt(response.values[i][3].replace(/−/, '-')),
966       "pts": parseInt(response.values[i][4])
967     });
968   }
969   var games = [];
970   for (var i = 12; response.values[i] !== undefined && response.values[i].length >= 1; ++i) {
971     if (response.values[i][2] && response.values[i][3]) {
972       games.push({
973         "name1": response.values[i][0],
974         "name2": response.values[i][1],
975         "score1": parseInt(response.values[i][2]),
976         "score2": parseInt(response.values[i][3])
977       });
978     }
979   }
980   console.log(games);
981
982   tiebreakers = [];
983   teams = rank(games, teams, 1, tiebreakers);
984   console.log(tiebreakers. join(", "));
985
986   var row_num = 2;
987   for (i = 0; i < teams.length; ++i) {
988     var tr = document.createElement("tr");
989
990     addth(tr, "rank", teams[i].rank);
991     addtd(tr, "team", teams[i].name);
992     addtd(tr, "nplayed", teams[i].nplayed);
993     addtd(tr, "gd", teams[i].gd.toString().replace(/-/, '−'));
994     addtd(tr, "pts", teams[i].pts);
995
996     tr.style = "-webkit-animation: fade-in 1.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
997     carousel.appendChild(tr);
998   }
999
1000   if (tiebreakers.length > 0) {
1001     var tie_tr = document.createElement("tr");
1002     tie_tr.className = "footer";
1003     tie_tr.style = "-webkit-animation: fade-in 2.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
1004     var td = document.createElement("td");
1005     td.appendChild(document.createTextNode("Tiebreaks applied: " + tiebreakers.join(', ')));
1006     td.setAttribute("colspan", "5");
1007     tie_tr.appendChild(td);
1008     carousel.appendChild(tie_tr);
1009   }
1010   
1011   var footer_tr = document.createElement("tr");
1012   footer_tr.className = "footer";
1013   footer_tr.style = "-webkit-animation: fade-in 2.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
1014   var td = document.createElement("td");
1015   td.appendChild(document.createTextNode("www.trondheimfrisbeeklubb.no | #trøndisk"));
1016   td.setAttribute("colspan", "5");
1017   footer_tr.appendChild(td);
1018   carousel.appendChild(footer_tr);
1019   console.log(footer_tr);
1020
1021   carousel.style.display = 'table';
1022 }
1023 req.open('GET', 'https://sheets.googleapis.com/v4/spreadsheets/1CwRHQtpokVMGTPJu2FYYG-6rnG7OfISIcEHwBfXh-Y4/values/A1:E22?key=AIzaSyAuP9yQn8g0bSay6r_RpGtpFeIbwprH1TU');
1024 if (false) {
1025         req.send();
1026 }
1027
1028 </script>
1029   </body>
1030 </html>