Replace jsPlumb by writing some SVG arrows ourselves.
[remoteglot] / www / js / remoteglot.js
1 var board = null;
2 var hiddenboard = null;
3 var arrows = [];
4 var occupied_by_arrows = [];
5 var refutation_lines = [];
6 var move_num = 1;
7 var toplay = 'W';
8 var ims = 0;
9 var sort_refutation_lines_by_score = 0;
10 var highlight_from = undefined;
11 var highlight_to = undefined;
12 var unique = Math.random();
13
14 var fen = null;
15 var display_lines = [];
16 var current_display_line = null;
17 var current_display_move = null;
18
19 var request_update = function(board) {
20         $.ajax({
21                 url: "http://analysis.sesse.net/analysis.pl?ims=" + ims + "&unique=" + unique
22                 //url: "http://analysis.sesse.net:5000/analysis.pl?ims=" + ims + "&unique=" + unique
23         }).done(function(data, textstatus, xhr) {
24                 ims = xhr.getResponseHeader('X-Remoteglot-Last-Modified');
25                 var num_viewers = xhr.getResponseHeader('X-Remoteglot-Num-Viewers');
26                 update_board(board, data, num_viewers);
27         }).fail(function() {
28                 // Wait ten seconds, then try again.
29                 setTimeout(function() { request_update(board); }, 10000);
30         });
31 }
32
33 var clear_arrows = function() {
34         for (var i = 0; i < arrows.length; ++i) {
35                 if (arrows[i].svg) {
36                         arrows[i].svg.parentElement.removeChild(arrows[i].svg);
37                         delete arrows[i].svg;
38                 }
39         }
40         arrows = [];
41
42         occupied_by_arrows = [];
43         for (var y = 0; y < 8; ++y) {
44                 occupied_by_arrows.push([false, false, false, false, false, false, false, false]);
45         }
46 }
47
48 var redraw_arrows = function() {
49         for (var i = 0; i < arrows.length; ++i) {
50                 position_arrow(arrows[i]);
51         }
52 }
53
54 var sign = function(x) {
55         if (x > 0) {
56                 return 1;
57         } else if (x < 0) {
58                 return -1;
59         } else {
60                 return 0;
61         }
62 }
63
64 // See if drawing this arrow on the board would cause unduly amount of confusion.
65 var interfering_arrow = function(from, to) {
66         var from_col = from.charCodeAt(0) - "a1".charCodeAt(0);
67         var from_row = from.charCodeAt(1) - "a1".charCodeAt(1);
68         var to_col   = to.charCodeAt(0) - "a1".charCodeAt(0);
69         var to_row   = to.charCodeAt(1) - "a1".charCodeAt(1);
70
71         occupied_by_arrows[from_row][from_col] = true;
72
73         // Knight move: Just check that we haven't been at the destination before.
74         if ((Math.abs(to_col - from_col) == 2 && Math.abs(to_row - from_row) == 1) ||
75             (Math.abs(to_col - from_col) == 1 && Math.abs(to_row - from_row) == 2)) {
76                 return occupied_by_arrows[to_row][to_col];
77         }
78
79         // Sliding piece: Check if anything except the from-square is seen before.
80         var dx = sign(to_col - from_col);
81         var dy = sign(to_row - from_row);
82         var x = from_col;
83         var y = from_row;
84         do {
85                 x += dx;
86                 y += dy;
87                 if (occupied_by_arrows[y][x]) {
88                         return true;
89                 }
90                 occupied_by_arrows[y][x] = true;
91         } while (x != to_col || y != to_row);
92
93         return false;
94 }
95
96 var point_from_start = function(x1, y1, x2, y2, t, u) {
97         var dx = x2 - x1;
98         var dy = y2 - y1;
99
100         var norm = 1.0 / Math.sqrt(dx * dx + dy * dy);
101         dx *= norm;
102         dy *= norm;
103
104         var x = x1 + dx * t + dy * u;
105         var y = y1 + dy * t - dx * u;
106         return x + " " + y;
107 }
108
109 var point_from_end = function(x1, y1, x2, y2, t, u) {
110         var dx = x2 - x1;
111         var dy = y2 - y1;
112
113         var norm = 1.0 / Math.sqrt(dx * dx + dy * dy);
114         dx *= norm;
115         dy *= norm;
116
117         var x = x2 + dx * t + dy * u;
118         var y = y2 + dy * t - dx * u;
119         return x + " " + y;
120 }
121
122 var position_arrow = function(arrow) {
123         if (arrow.svg) {
124                 arrow.svg.parentElement.removeChild(arrow.svg);
125                 delete arrow.svg;
126         }
127         if (current_display_line !== null) {
128                 return;
129         }
130
131         var pos = $(".square-a8").position();
132
133         var zoom_factor = $("#board").width() / 400.0;
134         var line_width = arrow.line_width * zoom_factor;
135         var arrow_size = arrow.arrow_size * zoom_factor;
136
137         var square_width = $(".square-a8").width();
138         var from_y = (7 - arrow.from_row + 0.5)*square_width;
139         var to_y = (7 - arrow.to_row + 0.5)*square_width;
140         var from_x = (arrow.from_col + 0.5)*square_width;
141         var to_x = (arrow.to_col + 0.5)*square_width;
142
143         var SVG_NS = "http://www.w3.org/2000/svg";
144         var XHTML_NS = "http://www.w3.org/1999/xhtml";
145         var svg = document.createElementNS(SVG_NS, "svg");
146         svg.setAttribute("width", $("#board").width());
147         svg.setAttribute("height", $("#board").height());
148         svg.setAttribute("style", "position: absolute");
149         svg.setAttribute("position", "absolute");
150         svg.setAttribute("version", "1.1");
151         svg.setAttribute("class", "c1");
152         svg.setAttribute("xmlns", XHTML_NS);
153
154         var x1 = from_x;
155         var y1 = from_y;
156         var x2 = to_x;
157         var y2 = to_y;
158
159         // Draw the line.
160         var outline = document.createElementNS(SVG_NS, "path");
161         outline.setAttribute("d", "M " + point_from_start(x1, y1, x2, y2, arrow_size / 2, 0) + " L " + point_from_end(x1, y1, x2, y2, -arrow_size / 2, 0));
162         outline.setAttribute("xmlns", XHTML_NS);
163         outline.setAttribute("stroke", "#666");
164         outline.setAttribute("stroke-width", line_width + 2);
165         outline.setAttribute("fill", "none");
166         svg.appendChild(outline);
167
168         var path = document.createElementNS(SVG_NS, "path");
169         path.setAttribute("d", "M " + point_from_start(x1, y1, x2, y2, arrow_size / 2, 0) + " L " + point_from_end(x1, y1, x2, y2, -arrow_size / 2, 0));
170         path.setAttribute("xmlns", XHTML_NS);
171         path.setAttribute("stroke", arrow.fg_color);
172         path.setAttribute("stroke-width", line_width);
173         path.setAttribute("fill", "none");
174         svg.appendChild(path);
175
176         // Then the arrow head.
177         var head = document.createElementNS(SVG_NS, "path");
178         head.setAttribute("d",
179                 "M " +  point_from_end(x1, y1, x2, y2, 0, 0) +
180                 " L " + point_from_end(x1, y1, x2, y2, -arrow_size, -arrow_size / 2) +
181                 " L " + point_from_end(x1, y1, x2, y2, -arrow_size * .623, 0.0) +
182                 " L " + point_from_end(x1, y1, x2, y2, -arrow_size, arrow_size / 2) +
183                 " L " + point_from_end(x1, y1, x2, y2, 0, 0));
184         head.setAttribute("xmlns", XHTML_NS);
185         head.setAttribute("stroke", "#000");
186         head.setAttribute("stroke-width", "1");
187         head.setAttribute("fill", "#f66");
188         svg.appendChild(head);
189
190         $(svg).css({ top: pos.top, left: pos.left });
191         document.body.appendChild(svg);
192         arrow.svg = svg;
193 }
194
195 var create_arrow = function(from_square, to_square, fg_color, line_width, arrow_size) {
196         var from_col = from_square.charCodeAt(0) - "a1".charCodeAt(0);
197         var from_row = from_square.charCodeAt(1) - "a1".charCodeAt(1);
198         var to_col   = to_square.charCodeAt(0) - "a1".charCodeAt(0);
199         var to_row   = to_square.charCodeAt(1) - "a1".charCodeAt(1);
200
201         // Create arrow.
202         var arrow = {
203                 from_col: from_col,
204                 from_row: from_row,
205                 to_col: to_col,
206                 to_row: to_row,
207                 line_width: line_width,
208                 arrow_size: arrow_size,
209                 fg_color: fg_color
210         };
211
212         position_arrow(arrow);
213         arrows.push(arrow);
214 }
215
216 var compare_by_sort_key = function(refutation_lines, a, b) {
217         var ska = refutation_lines[a].sort_key;
218         var skb = refutation_lines[b].sort_key;
219         if (ska < skb) return -1;
220         if (ska > skb) return 1;
221         return 0;
222 };
223         
224 var compare_by_score = function(refutation_lines, a, b) {
225         var sa = parseInt(refutation_lines[b].score_sort_key);
226         var sb = parseInt(refutation_lines[a].score_sort_key);
227         return sa - sb;
228 }
229
230 // Fake multi-PV using the refutation lines. Find all “relevant” moves,
231 // sorted by quality, descending.
232 var find_nonstupid_moves = function(data, margin) {
233         // First of all, if there are any moves that are more than 0.5 ahead of
234         // the primary move, the refutation lines are probably bunk, so just
235         // kill them all. 
236         var best_score = undefined;
237         var pv_score = undefined;
238         for (var move in data.refutation_lines) {
239                 var score = parseInt(data.refutation_lines[move].score_sort_key);
240                 if (move == data.pv_uci[0]) {
241                         pv_score = score;
242                 }
243                 if (best_score === undefined || score > best_score) {
244                         best_score = score;
245                 }
246                 if (!(data.refutation_lines[move].depth >= 8)) {
247                         return [];
248                 }
249         }
250
251         if (best_score - pv_score > 50) {
252                 return [];
253         }
254
255         // Now find all moves that are within “margin” of the best score.
256         // The PV move will always be first.
257         var moves = [];
258         for (var move in data.refutation_lines) {
259                 var score = parseInt(data.refutation_lines[move].score_sort_key);
260                 if (move != data.pv_uci[0] && best_score - score <= margin) {
261                         moves.push(move);
262                 }
263         }
264         moves = moves.sort(function(a, b) { return compare_by_score(data.refutation_lines, a, b) });
265         moves.unshift(data.pv_uci[0]);
266
267         return moves;
268 }
269
270 var thousands = function(x) {
271         return String(x).split('').reverse().join('').replace(/(\d{3}\B)/g, '$1,').split('').reverse().join('');
272 }
273
274 var print_pv = function(fen, uci_pv, pretty_pv, move_num, toplay, limit) {
275         display_lines.push({
276                 start_fen: fen,
277                 uci_pv: uci_pv,
278                 pretty_pv: pretty_pv 
279         });
280
281         var pv = '';
282         var i = 0;
283         if (toplay == 'B') {
284                 var move = "<a class=\"move\" href=\"javascript:show_line(" + (display_lines.length - 1) + ", " + 0 + ");\">" + pretty_pv[0] + "</a>";
285                 pv = move_num + '. … ' + move;
286                 toplay = 'W';
287                 ++i;    
288                 ++move_num;
289         }
290         for ( ; i < pretty_pv.length; ++i) {
291                 var move = "<a class=\"move\" href=\"javascript:show_line(" + (display_lines.length - 1) + ", " + i + ");\">" + pretty_pv[i] + "</a>";
292
293                 if (toplay == 'W') {
294                         if (i > limit) {
295                                 return pv + ' (…)';
296                         }
297                         if (pv != '') {
298                                 pv += ' ';
299                         }
300                         pv += move_num + '. ' + move;
301                         ++move_num;
302                         toplay = 'B';
303                 } else {
304                         pv += ' ' + move;
305                         toplay = 'W';
306                 }
307         }
308         return pv;
309 }
310
311 var update_highlight = function()  {
312         $("#board").find('.square-55d63').removeClass('nonuglyhighlight');
313         if (current_display_line === null && highlight_from !== undefined && highlight_to !== undefined) {
314                 $("#board").find('.square-' + highlight_from).addClass('nonuglyhighlight');
315                 $("#board").find('.square-' + highlight_to).addClass('nonuglyhighlight');
316         }
317 }
318
319 var update_refutation_lines = function(board) {
320         if (display_lines.length > 1) {
321                 display_lines = [ display_lines[0] ];
322         }
323
324         var tbl = $("#refutationlines");
325         tbl.empty();
326
327         var moves = [];
328         for (var move in refutation_lines) {
329                 moves.push(move);
330         }
331         var compare = sort_refutation_lines_by_score ? compare_by_score : compare_by_sort_key;
332         moves = moves.sort(function(a, b) { return compare(refutation_lines, a, b) });
333         for (var i = 0; i < moves.length; ++i) {
334                 var line = refutation_lines[moves[i]];
335
336                 var tr = document.createElement("tr");
337
338                 var move_td = document.createElement("td");
339                 tr.appendChild(move_td);
340                 $(move_td).addClass("move");
341                 if (line.pv_uci.length == 0) {
342                         $(move_td).text(line.pretty_move);
343                 } else {
344                         var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line.pretty_move + "</a>";
345                         $(move_td).html(move);
346                 }
347
348                 var score_td = document.createElement("td");
349                 tr.appendChild(score_td);
350                 $(score_td).addClass("score");
351                 $(score_td).text(line.pretty_score);
352
353                 var depth_td = document.createElement("td");
354                 tr.appendChild(depth_td);
355                 $(depth_td).addClass("depth");
356                 $(depth_td).text("d" + line.depth);
357
358                 var pv_td = document.createElement("td");
359                 tr.appendChild(pv_td);
360                 $(pv_td).addClass("pv");
361                 $(pv_td).html(print_pv(fen, line.pv_uci, line.pv_pretty, move_num, toplay, 10));
362
363                 tbl.append(tr);
364         }
365
366         // Make one of the links clickable and the other nonclickable.
367         if (sort_refutation_lines_by_score) {
368                 $("#sortbyscore0").html("<a href=\"javascript:resort_refutation_lines(0)\">Move</a>");
369                 $("#sortbyscore1").html("<strong>Score</strong>");
370         } else {
371                 $("#sortbyscore0").html("<strong>Move</strong>");
372                 $("#sortbyscore1").html("<a href=\"javascript:resort_refutation_lines(1)\">Score</a>");
373         }
374 }
375
376 var update_board = function(board, data, num_viewers) {
377         display_lines = [];
378
379         // The headline.
380         var headline = 'Analysis';
381         if (data.position.last_move !== 'none') {
382                 headline += ' after '
383                 if (data.position.toplay == 'W') {
384                         headline += (data.position.move_num-1) + '… ';
385                 } else {
386                         headline += data.position.move_num + '. ';
387                 }
388                 headline += data.position.last_move;
389         }
390
391         $("#headline").text(headline);
392
393         if (num_viewers === null) {
394                 $("#numviewers").text("");
395         } else if (num_viewers == 1) {
396                 $("#numviewers").text("You are the only current viewer");
397         } else {
398                 $("#numviewers").text(num_viewers + " current viewers");
399         }
400
401         // The score.
402         if (data.score !== null) {
403                 $("#score").text(data.score);
404         }
405
406         // The search stats.
407         if (data.nodes && data.nps && data.depth) {
408                 var stats = thousands(data.nodes) + ' nodes, ' + thousands(data.nps) + ' nodes/sec, depth ' + data.depth + ' ply';
409                 if (data.seldepth) {
410                         stats += ' (' + data.seldepth + ' selective)';
411                 }
412                 if (data.tbhits && data.tbhits > 0) {
413                         if (data.tbhits == 1) {
414                                 stats += ', one Nalimov hit';
415                         } else {
416                                 stats += ', ' + data.tbhits + ' Nalimov hits';
417                         }
418                 }
419                 
420
421                 $("#searchstats").text(stats);
422         }
423
424         // Update the board itself.
425         fen = data.position.fen;
426         update_displayed_line();
427
428         if (data.position.last_move_uci) {
429                 highlight_from = data.position.last_move_uci.substr(0, 2);
430                 highlight_to = data.position.last_move_uci.substr(2, 4);
431         } else {
432                 highlight_from = highlight_to = undefined;
433         }
434         update_highlight();
435
436         // Print the PV.
437         $("#pv").html(print_pv(data.position.fen, data.pv_uci, data.pv_pretty, data.position.move_num, data.position.toplay));
438
439         // Update the PV arrow.
440         clear_arrows();
441         if (data.pv_uci.length >= 1) {
442                 // draw a continuation arrow as long as it's the same piece
443                 for (var i = 0; i < data.pv_uci.length; i += 2) {
444                         var from = data.pv_uci[i].substr(0, 2);
445                         var to = data.pv_uci[i].substr(2,4);
446                         if ((i >= 2 && from != data.pv_uci[i - 2].substr(2, 4)) ||
447                              interfering_arrow(from, to)) {
448                                 break;
449                         }
450                         create_arrow(from, to, '#f66', 6, 20);
451                 }
452
453                 var alt_moves = find_nonstupid_moves(data, 30);
454                 for (var i = 1; i < alt_moves.length && i < 3; ++i) {
455                         create_arrow(alt_moves[i].substr(0, 2),
456                                      alt_moves[i].substr(2, 4), '#f66', 1, 10);
457                 }
458         }
459
460         // See if all semi-reasonable moves have only one possible response.
461         if (data.pv_uci.length >= 2) {
462                 var nonstupid_moves = find_nonstupid_moves(data, 300);
463                 var response = data.pv_uci[1];
464                 for (var i = 0; i < nonstupid_moves.length; ++i) {
465                         if (nonstupid_moves[i] == data.pv_uci[0]) {
466                                 // ignore the PV move for refutation lines.
467                                 continue;
468                         }
469                         if (!data.refutation_lines ||
470                             !data.refutation_lines[nonstupid_moves[i]] ||
471                             !data.refutation_lines[nonstupid_moves[i]].pv_uci ||
472                             data.refutation_lines[nonstupid_moves[i]].pv_uci.length < 1) {
473                                 // Incomplete PV, abort.
474                                 response = undefined;
475                                 break;
476                         }
477                         var this_response = data.refutation_lines[nonstupid_moves[i]].pv_uci[1];
478                         if (response !== this_response) {
479                                 // Different response depending on lines, abort.
480                                 response = undefined;
481                                 break;
482                         }
483                 }
484
485                 if (nonstupid_moves.length > 0 && response !== undefined) {
486                         create_arrow(response.substr(0, 2),
487                                      response.substr(2, 4), '#66f', 6, 20);
488                 }
489         }
490
491         // Update the refutation lines.
492         fen = data.position.fen;
493         move_num = data.position.move_num;
494         toplay = data.position.toplay;
495         refutation_lines = data.refutation_lines;
496         update_refutation_lines(board);
497
498         // Next update.
499         setTimeout(function() { request_update(board); }, 100);
500 }
501
502 var resort_refutation_lines = function(sort_by_score) {
503         sort_refutation_lines_by_score = sort_by_score;
504         update_refutation_lines(board);
505 }
506
507 var show_line = function(line_num, move_num) {
508         if (line_num == -1) {
509                 current_display_line = null;
510                 current_display_move = null;
511         } else {
512                 current_display_line = display_lines[line_num];
513                 current_display_move = move_num;
514         }
515         update_displayed_line();
516         update_highlight();
517         redraw_arrows();
518 }
519
520 var prev_move = function() {
521         --current_display_move;
522         update_displayed_line();
523 }
524
525 var next_move = function() {
526         ++current_display_move;
527         update_displayed_line();
528 }
529
530 var update_displayed_line = function() {
531         if (current_display_line === null) {
532                 $("#linenav").hide();
533                 $("#linemsg").show();
534                 board.position(fen);
535                 return;
536         }
537
538         $("#linenav").show();
539         $("#linemsg").hide();
540
541         if (current_display_move == 0) {
542                 $("#prevmove").html("Previous");
543         } else {
544                 $("#prevmove").html("<a href=\"javascript:prev_move();\">Previous</a></span>");
545         }
546         if (current_display_move == current_display_line.uci_pv.length - 1) {
547                 $("#nextmove").html("Next");
548         } else {
549                 $("#nextmove").html("<a href=\"javascript:next_move();\">Next</a></span>");
550         }
551
552         hiddenboard.position(current_display_line.start_fen, false);
553         for (var i = 0; i <= current_display_move; ++i) {
554                 var move = current_display_line.uci_pv[i];
555                 move = move.substr(0, 2) + "-" + move.substr(2, 4);
556                 hiddenboard.move(move, false);
557         }
558         board.position(hiddenboard.position());
559 }
560
561 var init = function() {
562         // Create board.
563         board = new ChessBoard('board', 'start');
564         hiddenboard = new ChessBoard('hiddenboard', 'start');
565
566         request_update(board);
567         $(window).resize(function() {
568                 board.resize();
569                 update_highlight();
570                 redraw_arrows();
571         });
572 };
573 $(document).ready(init);