1<?php
2
3/*
4 *  pgn4web javascript chessboard
5 *  copyright (C) 2009-2015 Paolo Casaschi
6 *  see README file and http://pgn4web.casaschi.net
7 *  for credits, license and more details
8 */
9
10error_reporting(E_ALL | E_STRICT);
11
12/*
13 *  URL parameters:
14 *
15 *  headlessPage = true | false (default false)
16 *  hideForm = true | false (default false)
17 *  pgnData (or pgnUrl) = (default null)
18 *  pgnText = (default null)
19 *
20 */
21
22$pgnDebugInfo = "";
23
24$tmpDir = "php://temp";
25$fileUploadLimitBytes = 4194304;
26$fileUploadLimitText = round(($fileUploadLimitBytes / 1048576), 0) . "MB";
27$fileUploadLimitIniText = ini_get("upload_max_filesize");
28if ($fileUploadLimitIniText === "") { $fileUploadLimitIniText = "unknown"; }
29
30// it would be nice here to evaluate ini_get('allow_fopen_url') and flag the issue (possibly disabling portions of the input forms), but the return values of ini_get() for boolean values are totally unreliable, so we have to leave with the generic server error message when trying to load a remote URL while allow_fopen_url is disabled in php.ini
31
32$zipSupported = function_exists('zip_open');
33if (!$zipSupported) { $pgnDebugInfo = $pgnDebugInfo . "\\n" . "ZIP support unavailable from server, missing php ZIP library"; }
34
35$http_response_header_status = "";
36$http_response_header_last_modified = "";
37
38$debugHelpText = "a flashing chessboard signals errors in the PGN data, click on the top left chessboard square for debug messages";
39
40$headlessPage = strtolower(get_param("headlessPage", "hp", ""));
41
42$hideForm = strtolower(get_param("hideForm", "hf", ""));
43$hideFormCss = ($hideForm == "true") || ($hideForm == "t") ? "display:none;" : "";
44
45$forceEncodingFrom = get_param("forceEncodingFrom", "fef", "");
46
47$startPosition = '[Event ""] [Site ""] [Date ""] [Round ""] [White ""] [Black ""] [Result ""] ' . ((($hideForm == "true") || ($hideForm == "t")) ? '' : '{ please enter chess games in PGN format using the form at the top of the page }');
48
49
50$presetURLsArray = array();
51function addPresetURL($label, $javascriptCode) {
52  global $presetURLsArray;
53  array_push($presetURLsArray, array('label' => $label, 'javascriptCode' => $javascriptCode));
54}
55
56// modify the viewer-preset-URLs.php file to add preset URLs for the viewer's form
57include 'viewer-preset-URLs.php';
58
59
60$pgnOnly = get_param("pgnOnly", "po", "");
61$generateParameter = get_param("generateParameter", "gp", "");
62if (($pgnOnly == "true") || ($pgnOnly == "t")) {
63
64  if (!get_pgn()) { header("HTTP/1.1 204 No Content"); }
65  header("content-type: application/x-chess-pgn");
66  header("content-disposition: inline; filename=games.pgn");
67  if ($http_response_header_last_modified) { header($http_response_header_last_modified); }
68  if ($pgnText) { print $pgnText; }
69
70} elseif (($generateParameter == "true") || ($generateParameter == "t")) {
71
72  header("content-type: text/html; charset=utf-8");
73  $pgnUrl = get_param("pgnData", "pd", "");
74  if ($pgnUrl == "") { $pgnUrl = get_param("pgnUrl", "pu", ""); }
75  $pgnLink = $_SERVER['SCRIPT_NAME'] . urlencode("?po=t&pd=" . $pgnUrl);
76  print("<div style='font-family:sans-serif; padding:1em;'><a style='text-decoration:none; color:black;' href='" . $pgnLink . "'>" . $pgnLink . "</a></div>");
77
78} else {
79
80  header("content-type: text/html; charset=utf-8");
81  if ($goToView = get_pgn()) {
82    $pgnText = str_replace(array("&", "<", ">"), array("&amp;", "&lt;", "&gt;"), $pgnText);
83  } else {
84    $pgnText = preg_match("/^error:/", $pgnStatus) ? '[Event ""] [Site ""] [Date ""] [Round ""] [White ""] [Black ""] [Result ""] { error loading PGN data, click square A8 for more details }' : $startPosition;
85  }
86  print_header();
87  print_form();
88  check_tmpDir();
89  print_menu("board");
90  print_chessboard_one();
91  print_menu("moves");
92  print_chessboard_two();
93  print_footer();
94  print_menu("bottom");
95  print_html_close();
96
97}
98
99
100function get_param($param, $shortParam, $default) {
101  if (isset($_REQUEST[$param])) { return $_REQUEST[$param]; }
102  if (isset($_REQUEST[$shortParam])) { return $_REQUEST[$shortParam]; }
103  return $default;
104}
105
106
107function http_parse_headers($headerFields) {
108
109  global $http_response_header_status, $http_response_header_last_modified;
110
111  $retVal = array();
112  foreach ($headerFields as $field) {
113    if (preg_match('/([^:]+): (.+)/m', $field, $match)) {
114      $match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
115      if (isset($retVal[$match[1]])) {
116        $retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
117      } else {
118        $retVal[$match[1]] = trim($match[2]);
119      }
120    } else if (preg_match('/^\S+\s+\d+\s/m', $field)) {
121      $retVal["status"] = $field;
122    }
123  }
124
125  if (isset($retVal["status"])) { $http_response_header_status = $retVal["status"]; }
126  if (isset($retVal["Last-Modified"])) { $http_response_header_last_modified = "Last-Modified: " . $retVal["Last-Modified"]; }
127
128  return $retVal;
129}
130
131
132function http_response_header_isInvalid() {
133   global $http_response_header_status;
134   return $http_response_header_status ? preg_match("/^\S+\s+[45]\d\d\s/", $http_response_header_status) : FALSE;
135}
136
137
138function get_pgn() {
139
140  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
141  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
142  global $http_response_header_status, $http_response_header_last_modified;
143
144  $pgnDebugInfo = $pgnDebugInfo . get_param("debug", "d", "");
145
146  $pgnText = get_param("pgnText", "pt", "");
147
148  $pgnUrl = get_param("pgnData", "pd", "");
149  if ($pgnUrl == "") { $pgnUrl = get_param("pgnUrl", "pu", ""); }
150
151  if ($pgnText) {
152    $pgnStatus = "info: games from textbox input";
153    $pgnTextbox = $pgnText = str_replace("\\\"", "\"", $pgnText);
154
155    $pgnText = preg_replace("/\[/", "\n\n[", $pgnText);
156    $pgnText = preg_replace("/\]/", "]\n\n", $pgnText);
157    $pgnText = preg_replace("/([012\*])(\s*)(\[)/", "$1\n\n$3", $pgnText);
158    $pgnText = preg_replace("/\]\s*\[/", "]\n[", $pgnText);
159    $pgnText = preg_replace("/^\s*\[/", "[", $pgnText);
160    $pgnText = preg_replace("/\n[\s*\n]+/", "\n\n", $pgnText);
161
162    $pgnTextbox = $pgnText;
163
164    return TRUE;
165  } else if ($pgnUrl) {
166    $pgnStatus = "info: games from $pgnUrl";
167    $isPgn = preg_match("/\.(pgn|txt)$/i", preg_replace("/[?#].*$/", "", $pgnUrl));
168    $isZip = preg_match("/\.zip$/i", preg_replace("/[?#].*$/", "", $pgnUrl));
169    if ($isZip) {
170      if (!$zipSupported) {
171        $pgnStatus = "error: zipfile support unavailable, unable to open $pgnUrl";
172        return FALSE;
173      } else {
174        $tempZipName = tempnam($tmpDir, "pgn4webViewer_");
175        // $pgnUrlOpts tries forcing following location redirects
176        // depending on server configuration, the script might still fail if the ZIP URL is redirected
177        $pgnUrlOpts = array("http" => array("follow_location" => TRUE, "max_redirects" => 20));
178        $pgnUrlHandle = @fopen($pgnUrl, "rb", false, stream_context_create($pgnUrlOpts));
179        if (!$pgnUrlHandle) {
180          $pgnStatus = "error: failed to get $pgnUrl: file not found or server error";
181          if ((isset($tempZipName)) && ($tempZipName) && (file_exists($tempZipName))) { unlink($tempZipName); }
182          return FALSE;
183        } else {
184          $tempZipHandle = fopen($tempZipName, "wb");
185          $copiedBytes = stream_copy_to_stream($pgnUrlHandle, $tempZipHandle, $fileUploadLimitBytes + 1, 0);
186          fclose($pgnUrlHandle);
187          fclose($tempZipHandle);
188          if (isset($http_response_header)) { http_parse_headers($http_response_header); }
189          if ((($copiedBytes > 0) && ($copiedBytes <= $fileUploadLimitBytes)) && (!http_response_header_isInvalid())) {
190            $pgnSource = $tempZipName;
191          } else {
192            $pgnStatus = "error: failed to get $pgnUrl: " . (http_response_header_isInvalid() ? "server error: $http_response_header_status" : "file not found, file size exceeds $fileUploadLimitText form limit, $fileUploadLimitIniText server limit or server error");
193            if ((isset($tempZipName)) && ($tempZipName) && (file_exists($tempZipName))) { unlink($tempZipName); }
194            return FALSE;
195          }
196        }
197      }
198    } else {
199      $pgnSource = $pgnUrl;
200    }
201  } elseif (count($_FILES) == 0) {
202    $pgnStatus = "info: no games supplied";
203    return FALSE;
204  } elseif ($_FILES['pgnFile']['error'] === UPLOAD_ERR_OK) {
205    $pgnFileName = $_FILES['pgnFile']['name'];
206    $pgnStatus = "info: games from file $pgnFileName";
207    $pgnFileSize = $_FILES['pgnFile']['size'];
208    if ($pgnFileSize == 0) {
209      $pgnStatus = "info: failed uploading games: file not found, file empty or upload error";
210      return FALSE;
211    } elseif ($pgnFileSize > $fileUploadLimitBytes) {
212      $pgnStatus = "error: failed uploading games: file size exceeds $fileUploadLimitText limit";
213      return FALSE;
214    } else {
215      $isPgn = preg_match("/\.(pgn|txt)$/i",$pgnFileName);
216      $isZip = preg_match("/\.zip$/i",$pgnFileName);
217      $pgnSource = $_FILES['pgnFile']['tmp_name'];
218    }
219  } else {
220    $pgnStatus = "error: failed uploading games: ";
221    switch ($_FILES['pgnFile']['error']) {
222      case UPLOAD_ERR_INI_SIZE:
223      case UPLOAD_ERR_FORM_SIZE:
224        $pgnStatus = $pgnStatus . "file size exceeds $fileUploadLimitText form limit or $fileUploadLimitIniText server limit";
225        break;
226      case UPLOAD_ERR_PARTIAL:
227      case UPLOAD_ERR_NO_FILE:
228        $pgnStatus = $pgnStatus . "file missing or truncated";
229        break;
230      case UPLOAD_ERR_NO_TMP_DIR:
231      case UPLOAD_ERR_CANT_WRITE:
232      case UPLOAD_ERR_EXTENSION:
233        $pgnStatus = $pgnStatus . "server error";
234        break;
235      default:
236        $pgnStatus = $pgnStatus . "unknown upload error";
237        break;
238    }
239    return FALSE;
240  }
241
242  if ($isZip) {
243    if ($zipSupported) {
244      if ($pgnUrl) { $zipFileString = $pgnUrl; }
245      else { $zipFileString = "zip file"; }
246      $pgnZip = zip_open($pgnSource);
247      if (is_resource($pgnZip)) {
248        while (is_resource($zipEntry = zip_read($pgnZip))) {
249          if (zip_entry_open($pgnZip, $zipEntry)) {
250            if (preg_match("/\.pgn$/i",zip_entry_name($zipEntry))) {
251              $pgnText = $pgnText . zip_entry_read($zipEntry, zip_entry_filesize($zipEntry)) . "\n\n\n";
252            }
253            zip_entry_close($zipEntry);
254          } else {
255            $pgnStatus = "error: failed reading $zipFileString content";
256            zip_close($pgnZip);
257            if ((isset($tempZipName)) && ($tempZipName) && (file_exists($tempZipName))) { unlink($tempZipName); }
258            return FALSE;
259          }
260        }
261        zip_close($pgnZip);
262        if ((isset($tempZipName)) && ($tempZipName) && (file_exists($tempZipName))) { unlink($tempZipName); }
263        if (!$pgnText) {
264          $pgnStatus = "error: games not found in $zipFileString";
265          return FALSE;
266        }
267      } else {
268        if ((isset($tempZipName)) && ($tempZipName) && (file_exists($tempZipName))) { unlink($tempZipName); }
269        $pgnStatus = "error: failed opening $zipFileString";
270        return FALSE;
271      }
272    } else {
273      $pgnStatus = "error: ZIP support unavailable from this server, only PGN files are supported";
274      return FALSE;
275    }
276  } elseif ($isPgn) {
277    if ($pgnUrl) { $pgnFileString = $pgnUrl; }
278    else { $pgnFileString = "pgn file"; }
279    $pgnText = @file_get_contents($pgnSource, NULL, NULL, 0, $fileUploadLimitBytes + 1);
280    if (isset($http_response_header)) { http_parse_headers($http_response_header); }
281    if ((!$pgnText) || (($pgnUrl) && (http_response_header_isInvalid()))) {
282      $pgnStatus = "error: failed reading $pgnFileString: " . (http_response_header_isInvalid() ? "server error: $http_response_header_status" : "file not found or server error");
283      return FALSE;
284    }
285    if ((strlen($pgnText) == 0) || (strlen($pgnText) > $fileUploadLimitBytes)) {
286      $pgnStatus = "error: failed reading $pgnFileString: file size exceeds $fileUploadLimitText form limit, $fileUploadLimitIniText server limit or server error";
287      return FALSE;
288    }
289  } elseif ($pgnSource) {
290    if ($zipSupported) {
291      $pgnStatus = "error: only PGN and ZIP (zipped pgn) files are supported";
292    } else {
293      $pgnStatus = "error: only PGN files are supported, ZIP support unavailable from this server";
294    }
295    return FALSE;
296  }
297
298  $assumedEncoding = $forceEncodingFrom;
299  if ($assumedEncoding == "") {
300
301
302// DeploymentCheck: conversion for given URLs
303
304// end DeploymentCheck
305
306
307  }
308  if (($assumedEncoding != "") && (strtoupper($assumedEncoding) != "NONE")) {
309    // convert text encoding to UNICODE, for example from windows WINDOWS-1252 files
310    $pgnText = html_entity_decode(htmlentities($pgnText, ENT_QUOTES, $assumedEncoding), ENT_QUOTES , "UNICODE");
311  }
312
313  return TRUE;
314}
315
316function check_tmpDir() {
317
318  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
319  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
320
321  if (preg_match("/^[a-zA-Z]+:\/\/.+/", $tmpDir)) { return; }
322
323  $unexpectedFiles = "";
324  if ($tmpDirHandle = opendir($tmpDir)) {
325    while($entryName = readdir($tmpDirHandle)) {
326      if (($entryName !== ".") && ($entryName !== "..") && ($entryName !== "index.html")) {
327        if ((time() - filemtime($tmpDir . "/" . $entryName)) > 3600) {
328          $unexpectedFiles = $unexpectedFiles . " " . $entryName;
329        }
330      }
331    }
332    closedir($tmpDirHandle);
333    if ($unexpectedFiles) {
334      $pgnDebugInfo = $pgnDebugInfo . "\\n" . "clean temporary directory " . $tmpDir . ":" . $unexpectedFiles;
335    }
336  } else {
337      $pgnDebugInfo = $pgnDebugInfo . "\\n" . "failed opening temporary directory " . $tmpDir;
338  }
339
340}
341
342function print_menu($item) {
343
344  print <<<END
345
346<div style="height:0.2em; overflow:hidden;"><a name="$item">&nbsp;</a></div>
347<div style="width:100%; text-align:right; font-size:66%; padding-bottom:0.5em;">
348&nbsp;&nbsp;&nbsp;&nbsp;<a href="#bottom" style="color: #B0B0B0;" onclick="this.blur();">bottom</a>
349&nbsp;&nbsp;&nbsp;&nbsp;<a href="#moves" style="color: #B0B0B0;" onclick="this.blur();">moves</a>
350&nbsp;&nbsp;&nbsp;&nbsp;<a href="#board" style="color: #B0B0B0;" onclick="this.blur();">board</a>
351&nbsp;&nbsp;&nbsp;&nbsp;<a href="#top" style="color: #B0B0B0;" onclick="this.blur();">top</a>
352</div>
353
354END;
355}
356
357function print_header() {
358
359  global $headlessPage;
360
361  if (($headlessPage == "true") || ($headlessPage == "t")) {
362     $headClass = "  display:none;";
363  } else {
364     $headClass = "";
365  }
366
367  print <<<END
368<!DOCTYPE HTML>
369<html>
370
371<head>
372
373<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
374
375<meta name="viewport" content="width=800">
376<link rel="icon" sizes="16x16" href="pawn.ico" />
377<title>pgn4web games viewer</title>
378
379<style type="text/css">
380
381html,
382body {
383  margin: 0px;
384  padding: 0px;
385}
386
387body {
388  color: black;
389  background: white;
390  font-family: 'pgn4web Liberation Sans', sans-serif;
391  font-size: 16px;
392  padding: 1.75em;
393  overflow-x: hidden;
394  overflow-y: scroll;
395}
396
397div, span, table, tr, td {
398  font-family: 'pgn4web Liberation Sans', sans-serif; /* fixes IE9 body css issue */
399  font-size: 16px; /* fixes Opera table css issue */
400  line-height: 1.4em;
401}
402
403a {
404  color: black;
405  text-decoration: none;
406}
407
408.formControl {
409  font-size: smaller;
410  margin: 0px;
411}
412
413.verticalMiddle {
414  display: block;
415  vertical-align: middle;
416}
417
418.borderBox {
419  box-sizing: border-box;
420  -moz-box-sizing: border-box;
421  -webkit-box-sizing: border-box;
422}
423
424.textboxAppearance {
425  appearance: field;
426  -moz-appearance: textfield;
427  -webkit-appearance: textfield;
428}
429
430.headClass {
431$headClass
432}
433
434</style>
435
436</head>
437
438<body onResize="if (typeof(updateAnnotationGraph) != 'undefined') { updateAnnotationGraph(); }">
439
440<h1 class="headClass" style="margin-top:0px; padding-top:0px; text-align:right;">
441<a style="float:left; color:red;">
442pgn4web games viewer
443</a>
444<a href="." onfocus="this.blur();" style="width:49px; height:29px; background:url(pawns.png) -47px -15px; vertical-align:baseline; display:inline-block;"></a>
445</h1>
446
447<div style="height:1em;" class="headClass">&nbsp;</div>
448
449END;
450}
451
452
453function print_form() {
454
455  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
456  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
457  global $headlessPage, $hideFormCss, $presetURLsArray;
458
459  $thisScript = $_SERVER['SCRIPT_NAME'];
460  if (($headlessPage == "true") || ($headlessPage == "t")) { $thisScript .= "?hp=t"; }
461
462  print <<<END
463
464<script type="text/javascript">
465  "use strict";
466
467  function setPgnUrl(newPgnUrl) {
468    if (!newPgnUrl) { newPgnUrl = ""; }
469    document.getElementById("urlFormText").value = newPgnUrl;
470    return false;
471  }
472
473  function checkPgnUrl() {
474    var theObj = document.getElementById("urlFormText");
475    if (!theObj) { return false; }
476    if (!checkPgnExtension(theObj.value)) { return false; }
477    else { return (theObj.value !== ""); }
478  }
479
480  function checkPgnFile() {
481    var theObj = document.getElementById("uploadFormFile");
482    if (!theObj) { return false; }
483    if (!checkPgnExtension(theObj.value)) { return false; }
484    else { return (theObj.value !== ""); }
485  }
486
487END;
488
489  if ($zipSupported) { print <<<END
490
491  function checkPgnExtension(uri) {
492    if (uri.replace(/[?#].*$/, "").match(/\\.(zip|pgn|txt)\$/i)) {
493      return true;
494    } else if (uri !== "") {
495      alert("only PGN and ZIP (zipped pgn) files are supported");
496    }
497    return false;
498  }
499
500END;
501
502  } else { print <<<END
503
504  function checkPgnExtension(uri) {
505    if (uri.match(/\\.(pgn|txt)\$/i)) {
506      return true;
507    } else if (uri.match(/\\.zip\$/i)) {
508      alert("ZIP support unavailable from this server, only PGN files are supported\\n\\nplease submit locally extracted PGN");
509    } else if (uri !== "") {
510      alert("only PGN files are supported (ZIP support unavailable from this server)");
511    }
512    return false;
513  }
514
515END;
516
517  }
518
519  print <<<END
520
521  function checkPgnFormTextSize() {
522    document.getElementById("pgnFormButton").title = "view games from textbox: PGN textbox size is " + document.getElementById("pgnFormText").value.length;
523    if (document.getElementById("pgnFormText").value.length == 1) {
524      document.getElementById("pgnFormButton").title += " char;";
525    } else {
526      document.getElementById("pgnFormButton").title += " chars;";
527    }
528    document.getElementById("pgnFormButton").title += " $debugHelpText";
529    document.getElementById("pgnFormText").title = document.getElementById("pgnFormButton").title;
530  }
531
532
533  function loadPgnFromForm() {
534
535    var theObjPgnFormText = document.getElementById('pgnFormText');
536    if (!theObjPgnFormText) { return; }
537    if (theObjPgnFormText.value === "") { return; }
538
539    var theObjPgnText = document.getElementById('pgnText');
540    if (!theObjPgnText) { return; }
541
542    theObjPgnText.value = theObjPgnFormText.value;
543
544    theObjPgnText.value = theObjPgnText.value.replace(/\\[/g,'\\n\\n[');
545    theObjPgnText.value = theObjPgnText.value.replace(/\\]/g,']\\n\\n');
546    theObjPgnText.value = theObjPgnText.value.replace(/([012\\*])(\\s*)(\\[)/g,'\$1\\n\\n\$3');
547    theObjPgnText.value = theObjPgnText.value.replace(/\\]\\s*\\[/g,']\\n[');
548    theObjPgnText.value = theObjPgnText.value.replace(/^\\s*\\[/g,'[');
549    theObjPgnText.value = theObjPgnText.value.replace(/\\n[\\s*\\n]+/g,'\\n\\n');
550
551    document.getElementById('uploadFormFile').value = "";
552    document.getElementById('urlFormText').value = "";
553
554    if (analysisStarted) { stopAnalysis(); }
555    firstStart = true;
556    start_pgn4web();
557    resetAlert();
558    myAlert("info: games from textbox input", false, true);
559
560    goToHash("board");
561    return;
562  }
563
564  function urlFormSelectChange() {
565    var theObj = document.getElementById("urlFormSelect");
566    if (!theObj) { return; }
567
568    var targetPgnUrl = "";
569    switch (theObj.value) {
570
571END;
572
573  foreach($presetURLsArray as $value) {
574    print("\n" . '      case "' . $value['label'] . '":' . "\n" . '        targetPgnUrl = (function(){ ' . $value['javascriptCode'] . '})();' . "\n" . '      break;' . "\n");
575  }
576
577  $formVariableColspan = $presetURLsArray ? 2: 1;
578  print <<<END
579
580      default:
581      break;
582    }
583    setPgnUrl(targetPgnUrl);
584    theObj.value = "header";
585  }
586
587var textFormMinHeight = "";
588function getTextFormMinHeight() {
589  var theObj;
590  if ((theObj = document.getElementById("pgnFormText")) &&  (theObj.offsetHeight)) {
591    return (theObj.offsetHeight + "px");
592  } else {
593    return "5em";
594  }
595}
596
597function reset_viewer() {
598
599  document.getElementById("uploadFormFile").value = "";
600  document.getElementById("urlFormText").value = "";
601  document.getElementById("pgnFormText").value = "";
602  document.getElementById("pgnFormText").style.height = textFormMinHeight;
603  checkPgnFormTextSize();
604  document.getElementById("pgnText").value = '$startPosition';
605
606  if (typeof(start_pgn4web) == "function") {
607    if (analysisStarted) { stopAnalysis(); }
608    firstStart = true;
609    SetAutoplayNextGame(false);
610    if (IsRotated) { FlipBoard(); }
611    start_pgn4web();
612    resetAlert();
613    resetLastCommentArea();
614  }
615
616  goToHash("top");
617}
618
619// fake functions to avoid warnings before pgn4web.js is loaded
620function disableShortcutKeysAndStoreStatus() {}
621function restoreShortcutKeysStatus() {}
622
623</script>
624
625<table style="margin-bottom:1.5em; $hideFormCss" width="100%" cellspacing="0" cellpadding="3" border="0"><tbody>
626
627  <form id="uploadForm" action="$thisScript" enctype="multipart/form-data" method="POST" style="display:inline;">
628  <tr>
629    <td align="left" valign="middle">
630      <input id="uploadFormSubmitButton" type="submit" class="formControl" value=" view games from local file " style="width:100%;" title="view games from local file: PGN and ZIP files must be smaller than $fileUploadLimitText (form limit) and $fileUploadLimitIniText (server limit); $debugHelpText" onClick="this.blur(); return checkPgnFile();">
631    </td>
632    <td colspan="$formVariableColspan" width="100%" align="left" valign="middle">
633      <input type="hidden" name="MAX_FILE_SIZE" value="$fileUploadLimitBytes">
634      <input id="uploadFormFile" name="pgnFile" type="file" class="formControl borderBox" style="width:100%;" title="view games from local file: PGN and ZIP files must be smaller than $fileUploadLimitText (form limit) and $fileUploadLimitIniText (server limit); $debugHelpText" onClick="this.blur();">
635      <input type="hidden" name="forceEncodingFrom" value="$forceEncodingFrom">
636    </td>
637  </tr>
638  </form>
639
640  <form id="urlForm" action="$thisScript" method="POST" style="display:inline;">
641  <tr>
642    <td align="left" valign="middle">
643      <input id="urlFormSubmitButton" type="submit" class="formControl" value=" view games from remote URL " title="view games from remote URL: PGN and ZIP files must be smaller than $fileUploadLimitText (form limit) and $fileUploadLimitIniText (server limit); $debugHelpText" onClick="this.blur(); return checkPgnUrl();">
644    </td>
645    <td width="100%" align="left" valign="middle">
646      <input id="urlFormText" name="pgnUrl" type="text" class="formControl verticalMiddle borderBox" value="" style="width:100%;" onFocus="disableShortcutKeysAndStoreStatus();" onBlur="restoreShortcutKeysStatus();" title="view games from remote URL: PGN and ZIP files must be smaller than $fileUploadLimitText (form limit) and $fileUploadLimitIniText (server limit); $debugHelpText">
647      <input type="hidden" name="forceEncodingFrom" value="$forceEncodingFrom">
648    </td>
649END;
650
651  if ($presetURLsArray) {
652    print('  <td align="right" valign="middle">' . "\n" . '      <select id="urlFormSelect" class="formControl verticalMiddle" style="font-family:monospace; display:block; vertical-align:middle; max-width:20ex;" title="view games from remote URL: select the download URL from the preset options; please support the sites providing the PGN games downloads" onChange="this.blur(); urlFormSelectChange();">' . "\n" . '        <option value="header"> </option>' . "\n");
653    foreach($presetURLsArray as $value) {
654      print('        <option value="' . $value['label'] . '">' . $value['label'] . '</option>' . "\n");
655    }
656    print('        <option value="clear">clear URL</option>' . "\n" . '      </select>' . "\n" . '    </td>' . "\n");
657  }
658
659  print <<<END
660  </tr>
661  </form>
662
663  <form id="textForm" style="display:inline;">
664  <tr>
665    <td align="left" valign="top">
666      <input id="pgnFormButton" type="button" class="formControl" value=" view games from textbox " style="width:100%;" onClick="this.blur(); loadPgnFromForm();">
667    </td>
668    <td colspan="$formVariableColspan" rowspan="2" width="100%" align="right" valign="middle">
669      <textarea id="pgnFormText" class="formControl verticalMiddle borderBox textboxAppearance" name="pgnTextbox" rows=4 style="width:100%; resize:vertical;" onFocus="disableShortcutKeysAndStoreStatus();" onBlur="restoreShortcutKeysStatus();" onChange="checkPgnFormTextSize();">$pgnTextbox</textarea>
670    </td>
671  </tr>
672  </form>
673
674  <tr>
675    <td align="left" valign="bottom">
676      <input id="clearButton" type="button" class="formControl" value=" reset viewer " onClick="this.blur(); if (confirm('reset viewer: current PGN games and inputs will be lost')) { reset_viewer(); }" title="reset viewer: current PGN games and inputs will be lost">
677    </td>
678  </tr>
679
680</tbody></table>
681
682<script type="text/javascript">
683"use strict";
684
685var textFormMinHeight = getTextFormMinHeight();
686var theObj = document.getElementById("pgnFormText");
687if (theObj) {
688  theObj.style.height = textFormMinHeight;
689  theObj.style.minHeight = textFormMinHeight;
690}
691
692</script>
693
694END;
695}
696
697function print_chessboard_one() {
698
699  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
700  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
701  global $hideFormCss;
702
703  print <<<END
704
705<style type="text/css">
706
707@import url("fonts/pgn4web-font-LiberationSans.css");
708@import url("fonts/pgn4web-font-ChessSansUsual.css");
709
710.gameBoard, .boardTable {
711  width: 392px !important;
712  height: 392px !important;
713}
714
715.boardTable {
716  border-style: solid;
717  border-color: #663300;
718  border-width: 4px;
719  box-shadow: 0px 0px 20px #663300;
720}
721
722.pieceImage {
723  width: 36px;
724  height: 36px;
725}
726
727.whiteSquare,
728.blackSquare,
729.highlightWhiteSquare,
730.highlightBlackSquare {
731  width: 44px;
732  height: 44px;
733  border-style: solid;
734  border-width: 2px;
735}
736
737.whiteSquare,
738.highlightWhiteSquare {
739  border-color: #FFCC99;
740  background: #FFCC99;
741}
742
743.blackSquare,
744.highlightBlackSquare {
745  border-color: #CC9966;
746  background: #CC9966;
747}
748
749.highlightWhiteSquare,
750.highlightBlackSquare {
751  border-color: #663300;
752}
753
754.selectControl {
755/* a "width" attribute here must use the !important flag to override default settings */
756  width: 100% !important;
757  margin-top: 1em;
758}
759
760.optionSelectControl {
761}
762
763.gameButtons {
764  width: 392px;
765}
766
767.buttonControlPlay,
768.buttonControlStop,
769.buttonControl {
770/* a "width" attribute here must use the !important flag to override default settings */
771  width: 75.2px !important;
772  font-family: 'pgn4web ChessSansUsual', 'pgn4web Liberation Sans', sans-serif;
773  font-size: 1em;
774  color: #B0B0B0;
775  -moz-appearance: none;
776  -webkit-appearance: none;
777  border: none;
778  background: transparent;
779  margin-top: 25px;
780  margin-bottom: 10px;
781}
782
783.buttonControlSpace {
784/* a "width" attribute here must use the !important flag to override default settings */
785  width: 4px !important;
786}
787
788.searchPgnButton {
789/* a "width" attribute here must use the !important flag to override default settings */
790  width: 10% !important;
791}
792
793.searchPgnExpression {
794/* a "width" attribute here must use the !important flag to override default settings */
795  width: 90% !important;
796}
797
798.move,
799.variation,
800.comment {
801  line-height: 1.4em;
802  font-weight: normal;
803}
804
805.move,
806.variation,
807.commentMove {
808  font-family: 'pgn4web ChessSansUsual', 'pgn4web Liberation Sans', sans-serif;
809}
810
811a.move,
812a.variation,
813.commentMove {
814  white-space: nowrap;
815}
816
817.move,
818.variation {
819  text-decoration: none;
820}
821
822.move {
823  color: black;
824}
825
826.moveText {
827  clear: both;
828  text-align: justify;
829}
830
831.comment,
832.variation {
833  color: #808080;
834}
835
836a.variation {
837  color: #808080;
838}
839
840.moveOn,
841.variationOn {
842  background-color: #FFCC99;
843}
844
845.selectSearchContainer {
846  text-align: center;
847}
848
849.emMeasure {
850  height: 1em; /* required */
851  padding-top: 1em;
852}
853
854.mainContainer {
855  padding-top: 0.5em;
856  padding-bottom: 1em;
857}
858
859.columnsContainer {
860  float: left;
861  width: 100%;
862}
863
864.boardColumn {
865  float: left;
866  width: 60%;
867}
868
869.headerColumn {
870  margin-left: 60%;
871}
872
873.headerItem {
874  width: 100%;
875  height: 1.4em;
876  white-space: nowrap;
877  overflow: hidden;
878}
879
880.innerHeaderItem,
881.innerHeaderItemNoMargin {
882  color: black;
883  text-decoration: none;
884}
885
886.innerHeaderItem {
887  margin-right: 1.25em;
888}
889
890.innerHeaderItemNoMargin {
891  margin-right: 0px;
892}
893
894.headerSpacer {
895  height: 0.66em;
896}
897
898.gameAnnotationContainer {
899  height: 6em;
900  width: 100%;
901}
902
903.toggleComments, .toggleAnalysis {
904  white-space: nowrap;
905  text-align: right;
906}
907
908.toggleCommentsLink, .toggleAnalysisLink, .backButton {
909  display: inline-block;
910  width: 1em;
911  padding-left: 1em;
912  text-decoration: none;
913  text-align: right;
914  color: #B0B0B0;
915}
916
917.gameAnnotationMessage {
918  display: inline-block;
919  white-space: nowrap;
920  color: #B0B0B0;
921  margin-top: 25px;
922  margin-bottom: 10px;
923}
924
925.lastMoveAndVariations {
926  float: left;
927}
928
929.lastMove {
930}
931
932.lastVariations {
933  padding-left: 1em;
934}
935
936.nextMoveAndVariations {
937  float: right;
938}
939
940.nextMove {
941}
942
943.nextVariations {
944  padding-right: 1em;
945}
946
947.backButton {
948}
949
950.lastMoveAndComment {
951  clear: both;
952  line-height: 1.4em;
953  display: none;
954}
955
956.lastComment {
957  clear: both;
958  resize: vertical;
959  overflow-y: auto;
960  height: 4.2em;
961  min-height: 1.4em;
962  max-height: 21em;
963  padding-right: 1em;
964  margin-bottom: 1em;
965  text-align: justify;
966}
967
968.analysisEval {
969  display: inline-block;
970  min-width: 3em;
971}
972
973.analysisMove {
974}
975
976.tablebase {
977  display: none;
978}
979
980.analysisPv {
981  margin-left: 0.5em;
982}
983
984</style>
985
986<script src="pgn4web.js" type="text/javascript"></script>
987<script src="engine.js" type="text/javascript"></script>
988<script src="fonts/chess-informant-NAG-symbols.js" type="text/javascript"></script>
989<script src="fide-lookup.js" type="text/javascript"></script>
990
991<style type="text/css">
992
993.NAGs {
994  font-size: 19px;
995  line-height: 0.9em;
996}
997
998</style>
999
1000<!-- paste your PGN below and make sure you dont specify an external source with SetPgnUrl() -->
1001<form style="display: none;"><textarea style="display: none;" id="pgnText">
1002
1003$pgnText
1004
1005</textarea></form>
1006<!-- paste your PGN above and make sure you dont specify an external source with SetPgnUrl() -->
1007
1008<script type="text/javascript">
1009   "use strict";
1010
1011   var pgn4web_engineWindowUrlParameters = "pf=m";
1012
1013   var highlightOption_default = true;
1014   var commentsOnSeparateLines_default = false;
1015   var commentsIntoMoveText_default = true;
1016   var initialHalfmove_default = "start";
1017
1018   SetImagePath("images/merida/36");
1019   SetImageType("png");
1020   SetHighlightOption(getHighlightOptionFromLocalStorage());
1021   SetCommentsIntoMoveText(getCommentsIntoMoveTextFromLocalStorage());
1022   SetCommentsOnSeparateLines(getCommentsOnSeparateLinesFromLocalStorage());
1023   SetInitialGame(1);
1024   SetInitialVariation(0);
1025   SetInitialHalfmove(initialHalfmove_default, true);
1026   SetGameSelectorOptions(null, true, 12, 12, 2, 15, 15, 3, 10);
1027   SetAutostartAutoplay(false);
1028   SetAutoplayNextGame(false);
1029   SetAutoplayDelay(getDelayFromLocalStorage());
1030   SetShortcutKeysEnabled(true);
1031
1032   function getHighlightOptionFromLocalStorage() {
1033      var ho;
1034      try { ho = (localStorage.getItem("pgn4web_chess_viewer_highlightOption") != "false"); }
1035      catch(e) { return highlightOption_default; }
1036      return ho === null ? highlightOption_default : ho;
1037   }
1038   function setHighlightOptionToLocalStorage(ho) {
1039      try { localStorage.setItem("pgn4web_chess_viewer_highlightOption", ho ? "true" : "false"); }
1040      catch(e) { return false; }
1041      return true;
1042   }
1043
1044   function getCommentsIntoMoveTextFromLocalStorage() {
1045      var cimt;
1046      try { cimt = !(localStorage.getItem("pgn4web_chess_viewer_commentsIntoMoveText") == "false"); }
1047      catch(e) { return commentsIntoMoveText_default; }
1048      return cimt === null ? commentsIntoMoveText_default : cimt;
1049   }
1050   function setCommentsIntoMoveTextToLocalStorage(cimt) {
1051      try { localStorage.setItem("pgn4web_chess_viewer_commentsIntoMoveText", cimt ? "true" : "false"); }
1052      catch(e) { return false; }
1053      return true;
1054   }
1055
1056   function getCommentsOnSeparateLinesFromLocalStorage() {
1057      var cosl;
1058      try { cosl = (localStorage.getItem("pgn4web_chess_viewer_commentsOnSeparateLines") == "true"); }
1059      catch(e) { return commentsOnSeparateLines_default; }
1060      return cosl === null ? commentsOnSeparateLines_default : cosl;
1061   }
1062   function setCommentsOnSeparateLinesToLocalStorage(cosl) {
1063      try { localStorage.setItem("pgn4web_chess_viewer_commentsOnSeparateLines", cosl ? "true" : "false"); }
1064      catch(e) { return false; }
1065      return true;
1066   }
1067   var Delay_default = 2000;
1068   function getDelayFromLocalStorage() {
1069      var d;
1070      try { d = parseInt(localStorage.getItem("pgn4web_chess_viewer_Delay"), 10); }
1071      catch(e) { return Delay_default; }
1072      return ((d === null) || (isNaN(d))) ? Delay_default : d;
1073   }
1074   function setDelayToLocalStorage(d) {
1075      try { localStorage.setItem("pgn4web_chess_viewer_Delay", d); }
1076      catch(e) { return false; }
1077      return true;
1078   }
1079
1080   function searchTag(tag, key, event) {
1081      searchPgnGame('\\\\[\\\\s*' + tag + '\\\\s*"' + fixRegExp(key) + '"\\\\s*\\\\]', event.shiftKey);
1082   }
1083   function searchTagDifferent(tag, key, event) {
1084      searchPgnGame('\\\\[\\\\s*' + tag + '\\\\s*"(?!' + fixRegExp(key) + '"\\\\s*\\\\])', event.shiftKey);
1085   }
1086
1087   function fixHeaderTag(elementId) {
1088      var headerId = ["GameEvent", "GameSite", "GameDate", "GameRound", "GameWhite", "GameBlack", "GameResult", "GameMode", "GameSection", "GameStage", "GameBoardNum", "Timecontrol", "GameWhiteTeam", "GameBlackTeam", "GameWhiteTitle", "GameBlackTitle", "GameWhiteElo", "GameBlackElo", "GameECO", "GameOpening", "GameVariation", "GameSubVariation", "GameTermination", "GameAnnotator", "GameWhiteClock", "GameBlackClock", "GameTimeControl"];
1089      var headerLabel = ["event", "site", "date", "round", "white player", "black player", "result", "mode", "section", "stage", "board", "time control", "white team", "black team", "white title", "black title", "white elo", "black elo", "eco", "opening", "variation", "subvariation", "termination", "annotator", "white clock", "black clock", "time control"];
1090      var theObj = document.getElementById(elementId);
1091      if (theObj) {
1092        theObj.className = (theObj.innerHTML === "") ? "innerHeaderItemNoMargin" : "innerHeaderItem";
1093        for (var ii = 0; ii < headerId.length; ii++) {
1094            if (headerId[ii] === elementId) { break; }
1095        }
1096        theObj.title = simpleHtmlentitiesDecode((ii < headerId.length ? headerLabel[ii] : elementId) + ": " + theObj.innerHTML);
1097      }
1098   }
1099
1100   function customPgnHeaderTagWithFix(tag, elementId, fixForDisplay) {
1101      var theObj;
1102      customPgnHeaderTag(tag, elementId);
1103      fixHeaderTag(elementId);
1104      if (fixForDisplay && (theObj = document.getElementById(elementId)) && theObj.innerHTML) {
1105         theObj.innerHTML = fixCommentForDisplay(theObj.innerHTML);
1106      }
1107   }
1108
1109   var previousCurrentVar = -1;
1110   function customFunctionOnMove() {
1111
1112      if (analysisStarted) {
1113         if (engineUnderstandsGame(currentGame)) {
1114            if (previousCurrentVar !== CurrentVar) { scanGameForFen(); }
1115            restartAnalysis();
1116         }
1117         else { stopAnalysis(); }
1118      } else {
1119         clearAnalysisHeader();
1120         clearAnnotationGraph();
1121      }
1122      previousCurrentVar = CurrentVar;
1123
1124      fixHeaderTag('GameWhiteClock');
1125      fixHeaderTag('GameBlackClock');
1126
1127      if ((annotateInProgress) && (!analysisStarted)) { stopAnnotateGame(false); }
1128      else if (theObj = document.getElementById("GameAnnotationMessage")) {
1129         if ((!annotateInProgress) && (theObj.innerHTML.indexOf("completed") > -1)) {
1130            theObj.style.display = "none";
1131            theObj.innerHTML = "";
1132            theObj.title = "";
1133            if (theObj = document.getElementById("GameButtons")) {
1134               theObj.style.display = "";
1135            }
1136         }
1137      }
1138   }
1139
1140   var PlyNumberMax;
1141   function customFunctionOnPgnGameLoad() {
1142      var theObj;
1143      fixHeaderTag('GameDate');
1144      customPgnHeaderTagWithFix('Mode', 'GameMode');
1145      fixHeaderTag('GameSite');
1146      fixHeaderTag('GameEvent');
1147      customPgnHeaderTagWithFix('Section', 'GameSection');
1148      customPgnHeaderTagWithFix('Stage', 'GameStage');
1149      fixHeaderTag('GameRound');
1150      if (theObj = document.getElementById("GameRound")) {
1151         if (theObj.innerHTML) {
1152            theObj.innerHTML = "round " + theObj.innerHTML;
1153         }
1154      }
1155      customPgnHeaderTagWithFix('Board', 'GameBoardNum');
1156      if (theObj = document.getElementById("GameBoardNum")) {
1157         if (theObj.innerHTML) {
1158            theObj.innerHTML = "board " + theObj.innerHTML;
1159         }
1160      }
1161      customPgnHeaderTagWithFix('TimeControl', 'GameTimeControl');
1162      fixHeaderTag('GameWhite');
1163      fixHeaderTag('GameBlack');
1164      customPgnHeaderTagWithFix('WhiteTeam', 'GameWhiteTeam');
1165      customPgnHeaderTagWithFix('BlackTeam', 'GameBlackTeam');
1166      customPgnHeaderTagWithFix('WhiteTitle', 'GameWhiteTitle');
1167      customPgnHeaderTagWithFix('BlackTitle', 'GameBlackTitle');
1168      customPgnHeaderTagWithFix('WhiteElo', 'GameWhiteElo');
1169      customPgnHeaderTagWithFix('BlackElo', 'GameBlackElo');
1170      customPgnHeaderTagWithFix('ECO', 'GameECO');
1171      customPgnHeaderTagWithFix('Opening', 'GameOpening', true);
1172      customPgnHeaderTagWithFix('Variation', 'GameVariation', true);
1173      customPgnHeaderTagWithFix('SubVariation', 'GameSubVariation', true);
1174      fixHeaderTag('GameResult');
1175      customPgnHeaderTagWithFix('Termination', 'GameTermination');
1176      customPgnHeaderTagWithFix('Annotator', 'GameAnnotator');
1177      if (PlyNumber > 0) { customPgnHeaderTag('Result', 'ResultAtGametextEnd'); }
1178      else { if (theObj = document.getElementById('ResultAtGametextEnd')) { theObj.innerHTML = ""; } }
1179
1180      if (theObj = document.getElementById("GameNumCurrent")) {
1181         theObj.innerHTML = currentGame + 1;
1182         theObj.title = "current game: " + (currentGame + 1);
1183      }
1184
1185      if (theObj = document.getElementById('lastMoveAndComment')) {
1186         var lastDisplayStyle;
1187         if ((PlyNumber === 0) && (gameFEN[currentGame])) {
1188            lastDisplayStyle = "block";
1189         } else if (commentsIntoMoveText && ((PlyNumber > 0) || (gameFEN[currentGame]))) {
1190            lastDisplayStyle = GameHasComments ? "block" : "none";
1191         } else {
1192            lastDisplayStyle = "none";
1193         }
1194         theObj.style.display = lastDisplayStyle;
1195      }
1196      if (theObj = document.getElementById("toggleCommentsLink")) {
1197         if (GameHasComments) {
1198            theObj.innerHTML = commentsIntoMoveText ? "&times;" : "+";
1199         } else {
1200            theObj.innerHTML = "";
1201         }
1202      }
1203
1204      PlyNumberMax = 0;
1205      for (ii = 0; ii < numberOfVars; ii++) {
1206         PlyNumberMax = Math.max(PlyNumberMax, StartPlyVar[ii] + PlyNumberVar[ii] - StartPly);
1207      }
1208
1209      if (analysisStarted) {
1210         if (engineUnderstandsGame(currentGame)) { scanGameForFen(); }
1211         else { stopAnalysis(); }
1212      }
1213      if (theObj = document.getElementById("toggleAnalysisLink")) {
1214         theObj.style.visibility = (annotationSupported && engineUnderstandsGame(currentGame)) ? "visible" : "hidden";
1215      }
1216      if (theObj = document.getElementById("GameAnalysisEval")) {
1217         theObj.style.visibility = (annotationSupported && engineUnderstandsGame(currentGame)) ? "visible" : "hidden";
1218      }
1219
1220      stopAnnotateGame(false);
1221   }
1222
1223   function customFunctionOnPgnTextLoad() {
1224      var theObj;
1225      var gameLoadStatus = "$pgnStatus";
1226      if (gameLoadStatus) {  myAlert(gameLoadStatus, gameLoadStatus.match(/^error:/), !gameLoadStatus.match(/^error:/)); }
1227      if (theObj = document.getElementById("GameNumInfo")) {
1228         theObj.style.display = numberOfGames > 1 ? "block" : "none";
1229      }
1230      if (theObj = document.getElementById("GameNumTotal")) {
1231         theObj.innerHTML = numberOfGames;
1232         theObj.title = "number of games: " + numberOfGames;
1233      }
1234   }
1235
1236   function searchPlayer(name, FideId, event) {
1237      if (name) {
1238         if (event.shiftKey) {
1239            if (typeof(openFidePlayerUrl) == "function") { openFidePlayerUrl(name, FideId); }
1240         } else {
1241            searchPgnGame('\\\\[\\\\s*(White|Black)\\\\s*"' + fixRegExp(name) + '"\\\\s*\\\\]', false);
1242         }
1243      }
1244   }
1245
1246   function searchTeam(name) {
1247      searchPgnGame('\\\\[\\\\s*(White|Black)Team\\\\s*"' + fixRegExp(name) + '"\\\\s*\\\\]', false);
1248   }
1249
1250   function cycleHash() {
1251      switch (location.hash) {
1252         case "#top": goToHash("board"); break;
1253         case "#board": goToHash("moves"); break;
1254         case "#zoom": goToHash("moves"); break;
1255         case "#moves": goToHash("bottom"); break;
1256         case "#bottom": goToHash("top"); break;
1257         default: goToHash("board"); break;
1258      }
1259   }
1260
1261   function goToHash(hash) {
1262      if (hash) { location.hash = ""; }
1263      else { location.hash = "#board"; }
1264      location.hash = "#" + hash;
1265   }
1266
1267   var shortcutKeyTimeout = null;
1268
1269   // customShortcutKey_Shift_1 defined by fide-lookup.js
1270   // customShortcutKey_Shift_2 defined by fide-lookup.js
1271
1272   function customShortcutKey_Shift_3() { if (shortcutKeyTimeout) { SetInitialHalfmove(initialHalfmove_default, true); } else { shortcutKeyTimeout = setTimeout("shortcutKeyTimeout = null;", 333); SetInitialHalfmove(initialHalfmove == "end" ? "start" : "end", true); } }
1273
1274   function customShortcutKey_Shift_4() { if (shortcutKeyTimeout) { goToHash("zoom"); } else { shortcutKeyTimeout = setTimeout("shortcutKeyTimeout = null;", 333); cycleHash(); } }
1275
1276   function customShortcutKey_Shift_5() { cycleLastCommentArea(); }
1277
1278   function customShortcutKey_Shift_6() { if (annotationSupported) { userToggleAnalysis(); } }
1279   function customShortcutKey_Shift_7() { if (annotationSupported) { goToMissingAnalysis(true); } }
1280
1281   // customShortcutKey_Shift_8 defined by engine.js
1282   // customShortcutKey_Shift_9 defined by engine.js
1283   // customShortcutKey_Shift_0 defined by engine.js
1284
1285
1286   function gameIsNormalChess(gameNum) {
1287      return ((typeof(gameVariant[gameNum]) == "undefined") || (gameVariant[gameNum].match(/^(chess|normal|standard|)$/i) !== null));
1288   }
1289
1290
1291   function emPixels(em) { return em * document.getElementById("emMeasure").offsetHeight; }
1292
1293   var cycleLCA = 0;
1294   function cycleLastCommentArea() {
1295      var theObj = document.getElementById("GameLastComment");
1296      if (theObj) {
1297         switch (cycleLCA++ % 3) {
1298            case 0:
1299               if (theObj.scrollHeight === theObj.clientHeight) { cycleLastCommentArea(); }
1300               else { fitLastCommentArea(); }
1301               break;
1302            case 1:
1303               if (theObj.offsetHeight == emPixels(21)) { cycleLastCommentArea(); }
1304               else { maximizeLastCommentArea(); }
1305               break;
1306            case 2:
1307               if (theObj.offsetHeight == emPixels(4.2)) { cycleLastCommentArea(); }
1308               else { resetLastCommentArea(); }
1309               break;
1310            default:
1311               break;
1312         }
1313      }
1314   }
1315
1316   function resetLastCommentArea() {
1317      var theObj = document.getElementById("GameLastComment");
1318      if (theObj) { theObj.style.height = ""; }
1319   }
1320
1321   function fitLastCommentArea() {
1322      var theObj = document.getElementById("GameLastComment");
1323      if (theObj) {
1324         theObj.style.height = "";
1325         theObj.style.height = theObj.scrollHeight + "px";
1326      }
1327   }
1328
1329   function maximizeLastCommentArea() {
1330      var theObj = document.getElementById("GameLastComment");
1331      if (theObj) { theObj.style.height = "21em"; }
1332   }
1333
1334   function clickedGameAnalysisEval() {
1335      displayHelp('informant_symbols');
1336   }
1337
1338</script>
1339
1340<div class="selectSearchContainer">
1341<table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr>
1342<td colspan="2" align="left" valign="bottom">
1343<div id="GameSelector" class="gameSelector"></div>
1344</td>
1345</tr><tr>
1346<td width="100%" align="left" valign="top">
1347<div id="GameSearch" style="white-space:nowrap;"></div>
1348</td><td align="right" valign="bottom">
1349<div id="GameNumInfo" style="width:15ex; margin-right:0.5ex; display:none; color: #808080; font-size: 66%;"><span id="GameNumCurrent" style="font-size: 100%;" title="current game"></span>&nbsp;/&nbsp;<span id="GameNumTotal" style="font-size: 100%;" title="number of games"></span></div>
1350</td>
1351</tr></tbody></table>
1352<div id="emMeasure" class="emMeasure"><a href="#zoom" onclick="this.blur();" id="zoom" class="NAGs" style="width:392px; font-size:14px; display:inline-block;">&nbsp;</a></div>
1353<div><a name="zoom">&nbsp;</a></div>
1354</div>
1355
1356<div class="mainContainer">
1357
1358<div class="columnsContainer">
1359
1360<div class="boardColumn">
1361<center>
1362<div id="GameBoard" class="gameBoard"></div>
1363<div id="GameButtons" class="gameButtons"></div>
1364<a href="javascript:void(0);" onclick="stopAnnotateGame(false); this.blur();" class="gameAnnotationMessage" style="display:none;" id="GameAnnotationMessage"></a>
1365</center>
1366</div>
1367
1368<div class="headerColumn">
1369<div class="headerItem"><a class="innerHeaderItem" id="GameDate" href="javascript:void(0);" onclick="searchTagDifferent('Date', this.innerHTML, event); this.blur();"></a><span class="innerHeaderItem" id="GameMode"></span><b>&nbsp;</b></div>
1370<div class="headerItem"><a class="innerHeaderItem" id="GameSite" href="javascript:void(0);" onclick="searchTagDifferent('Site', this.innerHTML, event); this.blur();"></a><b>&nbsp;</b></div>
1371<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1372<div class="headerItem"><a class="innerHeaderItem" id="GameEvent" href="javascript:void(0);" onclick="searchTagDifferent('Event', this.innerHTML, event); this.blur();"></a><a class="innerHeaderItem" id="GameSection" href="javascript:void(0);" onclick="searchTagDifferent('Section', this.innerHTML, event); this.blur();"></a><a class="innerHeaderItem" id="GameStage" href="javascript:void(0);" onclick="searchTagDifferent('Stage', this.innerHTML, event); this.blur();"></a><b>&nbsp;</b></div>
1373<div class="headerItem"><a class="innerHeaderItem" id="GameRound" href="javascript:void(0);" onclick="searchTagDifferent('Round', this.innerHTML.replace('round ', ''), event); this.blur();"></a><a class="innerHeaderItem" id="GameBoardNum" href="javascript:void(0);" onclick="searchTagDifferent('Board', this.innerHTML, event); this.blur();"></a><a class="innerHeaderItem" id="GameTimeControl"  href="javascript:void(0);" onclick="searchTagDifferent('TimeControl', this.innerHTML, event); this.blur();"></a><b>&nbsp;</b></div>
1374<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1375<div class="headerItem"><a class="innerHeaderItem" id="GameECO" href="javascript:void(0);" onclick="searchTag('ECO', this.innerHTML, event); this.blur();"></a><a class="innerHeaderItem" id="GameOpening" href="javascript:void(0);" onclick="searchTag('Opening', customPgnHeaderTag('Opening'), event); this.blur();"></a><a class="innerHeaderItem" id="GameVariation" href="javascript:void(0);" onclick="searchTag('Variation', customPgnHeaderTag('Variation'), event); this.blur();"></a><a class="innerHeaderItem" id="GameSubVariation" href="javascript:void(0);" onclick="searchTag('SubVariation', customPgnHeaderTag('SubVariation'), event); this.blur();"></a><b>&nbsp;</b></div>
1376<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1377<div class="headerItem"><span class="innerHeaderItem" id="GameWhiteClock"></span><b>&nbsp;</b></div>
1378<div class="headerItem"><b><a href="javascript:void(0);" onclick="searchPlayer(this.innerHTML, customPgnHeaderTag('WhiteFideId'), event); this.blur();" class="innerHeaderItem" id="GameWhite"></a></b><span class="innerHeaderItem" id="GameWhiteTitle"></span><span class="innerHeaderItem" id="GameWhiteElo"></span><a class="innerHeaderItem" id="GameWhiteTeam" href="javascript:void(0);" onclick="searchTeam(this.innerHTML); this.blur();"></a><b>&nbsp;</b></div>
1379<div class="headerItem"><b><a href="javascript:void(0);" onclick="searchPlayer(this.innerHTML, customPgnHeaderTag('BlackFideId'), event); this.blur();" class="innerHeaderItem" id="GameBlack"></a></b><span class="innerHeaderItem" id="GameBlackTitle"></span><span class="innerHeaderItem" id="GameBlackElo"></span><a class="innerHeaderItem" id="GameBlackTeam" href="javascript:void(0);" onclick="searchTeam(this.innerHTML); this.blur();"></a><b>&nbsp;</b></div>
1380<div class="headerItem"><span class="innerHeaderItem" id="GameBlackClock"></span><b>&nbsp;</b></div>
1381<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1382<div class="headerItem"><b><a href="javascript:void(0);" onclick="SetInitialHalfmove(event.shiftKey ? initialHalfmove_default : (initialHalfmove == 'end' ? 'start' : 'end'), true); GoToMove(initialHalfmove == 'end' ? StartPlyVar[0] + PlyNumberVar[0] : StartPlyVar[0], 0); this.blur();" class="innerHeaderItem" id="GameResult"></a></b><span class="innerHeaderItem" id="GameTermination"></span><span class="innerHeaderItem" id="GameAnnotator"></span><b>&nbsp;</b></div>
1383<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1384<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1385<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1386<div class="headerItem"><a href="javascript:void(0);" onclick="if (event.shiftKey) { clickedGameAnalysisEval(); } else { userToggleAnalysis(); } this.blur(); return false;" class="innerHeaderItem analysisEval" id="GameAnalysisEval" title="start annotation">&middot;&nbsp;</a><a href="javascript:void(0);" onclick="if (event.shiftKey) { MoveBackward(1); } else { goToMissingAnalysis(false); } this.blur();" class="innerHeaderItem move analysisMove notranslate" id="GameAnalysisMove" title="annotated move"></a><a href="javascript:void(0);" onclick="clickedGameTablebase();" class="innerHeaderItem tablebase" id="GameTablebase" title="probe endgame tablebase">&nbsp;</a><a href="javascript:void(0);" onclick="if (event.shiftKey) { MoveForward(1); } else { goToMissingAnalysis(true); } this.blur();" class="innerHeaderItemNoMargin move analysisPv notranslate" id="GameAnalysisPv"></a><b>&nbsp;</b></div>
1387<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1388<div class="gameAnnotationContainer" id="GameAnnotationContainer">
1389<canvas class="gameAnnotationGraph" id="GameAnnotationGraph" height="1" width="1" onclick="annotationGraphClick(event); this.blur();" onmousemove="annotationGraphMousemove(event);" onmouseover="annotationGraphMouseover(event);" onmouseout="annotationGraphMouseout(event);"></canvas>
1390</div>
1391<div class="headerItem headerSpacer"><b>&nbsp;</b></div>
1392<div class="toggleAnalysis" id="toggleAnalysis">&nbsp;<a class="toggleAnalysisLink" style="visibility:hidden;" id="toggleAnalysisLink" href="javascript:void(0);" onclick="if (event.shiftKey) { annotateGame(false); } else { userToggleAnalysis(); } this.blur();" title="toggle annotation">+</a></div>
1393<div class="toggleComments" id="toggleComments">&nbsp;<a class="toggleCommentsLink" id="toggleCommentsLink" href="javascript:void(0);" onClick="if (event.shiftKey && commentsIntoMoveText) { cycleLastCommentArea(); } else { SetCommentsIntoMoveText(!commentsIntoMoveText); var oldPly = CurrentPly; var oldVar = CurrentVar; Init(); GoToMove(oldPly, oldVar); } this.blur();" title="toggle show comments in game text"></a></div>
1394</div>
1395
1396</div>
1397
1398<div class="lastMoveAndComment" id="lastMoveAndComment">
1399<div class="lastMoveAndVariations">
1400<span class="lastMove" id="GameLastMove" title="last move"></span>
1401<span class="lastVariations" id="GameLastVariations" title="last move alternatives"></span>&nbsp;
1402</div>
1403<div class="nextMoveAndVariations">
1404<span class="nextVariations" id="GameNextVariations" title="next move alternatives"></span>&nbsp;
1405<span class="nextMove" id="GameNextMove" title="next move"></span><a class="backButton" href="javascript:void(0);" onclick="backButton(event); this.blur();" title="move backward">&lt;</a>
1406</div>
1407<div>&nbsp;</div>
1408<div class="lastComment" title="current position comment" id="GameLastComment"></div>
1409</div>
1410</div>
1411
1412END;
1413}
1414
1415function print_chessboard_two() {
1416
1417  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
1418  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
1419
1420  print <<<END
1421
1422<div class="mainContainer">
1423<div id="moveText" class="moveText"><span id="GameText"></span> <span class="move" style="white-space:nowrap;" id="ResultAtGametextEnd"></span></div>
1424</div>
1425
1426
1427<script type="text/javascript">
1428   "use strict";
1429
1430   var theObj;
1431
1432   var maxMenInTablebase = 0;
1433   var minMenInTablebase = 3;
1434   function probeTablebase() {}
1435
1436
1437// DeploymentCheck: tablebase glue code
1438
1439// end DeploymentCheck
1440
1441
1442   function clickedGameTablebase() {
1443      var menPosition = CurrentFEN().replace(/\s.*$/, "").replace(/[0-9\/]/g, "").length;
1444      if ((menPosition >= minMenInTablebase) && (menPosition <= maxMenInTablebase)) {
1445         probeTablebase();
1446      } else {
1447         myAlert("warning: endgame tablebase only supports positions with " + minMenInTablebase + " to " + maxMenInTablebase + " men");
1448      }
1449   }
1450   theObj = document.getElementById("GameTablebase");
1451   if (theObj) { theObj.innerHTML = translateNAGs("$148"); }
1452
1453   function updateTablebaseFlag(thisFen) {
1454      if (typeof(thisFen) == "undefined") { thisFen = CurrentFEN(); }
1455      var menPosition = thisFen.replace(/\s.*$/, "").replace(/[0-9\/]/g, "").length;
1456      var theObj = document.getElementById("GameTablebase");
1457      if (theObj) {
1458         theObj.style.display = (menPosition >= minMenInTablebase) && (menPosition <= maxMenInTablebase) && (!g_initErrors) && (analysisStarted) ? "inline" : "none";
1459      }
1460   }
1461
1462   var annotationSupported = !!window.Worker;
1463   try {
1464      document.getElementById("GameAnnotationGraph").getContext("2d");
1465   } catch(e) { annotationSupported = false; }
1466
1467   var analysisStarted = false;
1468   function toggleAnalysis() {
1469      if (analysisStarted) { stopAnalysis(); }
1470      else { restartAnalysis(); }
1471   }
1472
1473   function restartAnalysis() {
1474      analysisStarted = StartEngineAnalysis();
1475      if (theObj = document.getElementById("toggleAnalysisLink")) { theObj.innerHTML = "&times;"; }
1476      updateAnnotationGraph();
1477      updateAnalysisHeader();
1478   }
1479
1480   function stopAnalysis() {
1481      stopAnnotateGame(false);
1482      StopBackgroundEngine();
1483      analysisStarted = false;
1484      var theObj = document.getElementById("toggleAnalysisLink");
1485      if (theObj) { theObj.innerHTML = "+"; }
1486      clearAnnotationGraph();
1487      clearAnalysisHeader();
1488      save_cache_to_localStorage();
1489   }
1490
1491   var fenPositions;
1492   var fenPositionsEval;
1493   var fenPositionsPv;
1494   var fenPositionsDepth;
1495   resetFenPositions();
1496
1497   function resetFenPositions() {
1498      fenPositions = new Array();
1499      fenPositionsEval = new Array();
1500      fenPositionsPv = new Array();
1501      fenPositionsDepth = new Array();
1502   }
1503
1504   var annotationBarWidth;
1505   function updateAnnotationGraph() {
1506      if (!annotationSupported) { return; }
1507      var index, theObj;
1508      if (!analysisStarted) { clearAnnotationGraph(); }
1509      else if (theObj = document.getElementById("GameAnnotationGraph")) {
1510
1511         var canvasWidth = graphCanvasWidth();
1512         theObj.width = canvasWidth;
1513         var canvasHeight = graphCanvasHeight();
1514         theObj.height = canvasHeight;
1515
1516         var annotationPlyBlock = 40;
1517         annotationBarWidth = canvasWidth / (Math.max(Math.ceil(PlyNumberMax / annotationPlyBlock) * annotationPlyBlock, 2 * annotationPlyBlock) + 2);
1518         var barOverlap = Math.ceil(annotationBarWidth / 20);
1519         var lineHeight = Math.ceil(canvasHeight / 100);
1520         var lineTop = Math.floor((canvasHeight - lineHeight) / 2);
1521         var lineBottom = lineTop + lineHeight;
1522         var maxBarHeight = lineTop + barOverlap;
1523
1524         var context = theObj.getContext("2d");
1525         context.beginPath();
1526         var thisBarTopLeftX = 0;
1527         var thisBarHeight = lineHeight;
1528         var thisBarTopLeftY = lineTop;
1529         context.rect(thisBarTopLeftX, thisBarTopLeftY, (PlyNumber + 1) * annotationBarWidth + barOverlap, thisBarHeight);
1530         context.fillStyle = "#D9D9D9";
1531         context.fill();
1532         context.fillStyle = "#666666";
1533         var highlightTopLeftX = null;
1534         var highlightTopLeftY = null;
1535         var highlightBarHeight = null;
1536         for (var annPly = StartPly; annPly <= StartPly + PlyNumber; annPly++) {
1537            var annGraphEval = typeof(fenPositionsEval[annPly]) != "undefined" ? fenPositionsEval[annPly] : (annPly === CurrentPly ? 0 : null);
1538            if (annGraphEval !== null) {
1539               thisBarTopLeftX = (annPly - StartPly) * annotationBarWidth;
1540               if (annGraphEval >= 0) {
1541                  thisBarHeight = Math.max((1 - Math.pow(2, -annGraphEval)) * maxBarHeight, lineHeight);
1542                  thisBarTopLeftY = lineBottom - thisBarHeight;
1543               } else {
1544                  thisBarHeight = Math.max((1 - Math.pow(2,  annGraphEval)) * maxBarHeight, lineHeight);
1545                  thisBarTopLeftY = lineTop;
1546               }
1547               if (annPly !== CurrentPly) {
1548                  context.beginPath();
1549                  context.rect(thisBarTopLeftX, thisBarTopLeftY, annotationBarWidth + barOverlap, thisBarHeight);
1550                  context.fill();
1551               } else {
1552                  highlightTopLeftX = thisBarTopLeftX;
1553                  highlightTopLeftY = thisBarTopLeftY;
1554                  highlightBarHeight = thisBarHeight;
1555               }
1556            }
1557         }
1558         if (highlightBarHeight !== null) {
1559            context.beginPath();
1560            context.rect(highlightTopLeftX, highlightTopLeftY, annotationBarWidth + barOverlap, highlightBarHeight);
1561            context.fillStyle = "#FF6633";
1562            context.fill();
1563         }
1564      }
1565   }
1566
1567   function clearAnnotationGraph() {
1568      if (!annotationSupported) { return; }
1569      var theObj = document.getElementById("GameAnnotationGraph");
1570      if (theObj) {
1571         var context = theObj.getContext("2d");
1572         theObj.width = graphCanvasWidth();
1573         theObj.height = graphCanvasHeight();
1574         context.clearRect(0, 0, theObj.width, theObj.height);
1575      }
1576   }
1577
1578   function graphCanvasWidth() {
1579      var theObj = document.getElementById("GameAnnotationContainer");
1580      if (theObj) { return theObj.offsetWidth; }
1581      else { return 320; }
1582   }
1583   function graphCanvasHeight() {
1584      var theObj = document.getElementById("GameAnnotationContainer");
1585      if (theObj) { return theObj.offsetHeight; }
1586      else { return 96; }
1587   }
1588
1589   function updateAnalysisHeader() {
1590      if (!analysisStarted) { clearAnalysisHeader(); return; }
1591
1592      var theObj;
1593      var annPly = (lastMousemoveAnnPly == -1) ? CurrentPly : lastMousemoveAnnPly;
1594      var annMove = "&middot;&nbsp;";
1595      if (theObj = document.getElementById("GameAnalysisMove")) {
1596         if ((annPly > StartPly) && (annPly <= StartPly + PlyNumber)) {
1597            annMove = (Math.floor(annPly / 2) + (annPly % 2)) + (annPly % 2 ? ". " : "... ") + Moves[annPly - 1];
1598            if (isBlunder(annPly, blunderThreshold)) { annMove += translateNAGs("$4"); }
1599            else if (isBlunder(annPly, mistakeThreshold)) { annMove += translateNAGs("$2"); }
1600            annMove += "&nbsp;";
1601         }
1602         theObj.innerHTML = annMove;
1603      }
1604
1605      var annEval = fenPositionsEval[annPly];
1606      var annPv = fenPositionsPv[annPly];
1607      var annDepth = fenPositionsDepth[annPly];
1608
1609      if (theObj = document.getElementById("GameAnalysisEval")) {
1610         theObj.innerHTML = (annEval || annEval === 0) ? ev2NAG(annEval) : "";
1611         theObj.title = (annEval || annEval === 0) ? "engine evaluation: " + (annEval > 0 ? "+" : "") + annEval + (annEval == Math.floor(annEval) ? ".0" : "") + (annDepth ? "  depth: " + annDepth : "") : "";
1612      }
1613      if (theObj = document.getElementById("GameAnalysisPv")) {
1614         theObj.innerHTML = annPv ? annPv : "";
1615         theObj.title = annPv ? "engine principal variation: " + annPv : "";
1616      }
1617
1618      updateTablebaseFlag(fenPositions[annPly]);
1619   }
1620
1621
1622   var moderateDefiniteThreshold = 1.85;
1623   var slightModerateThreshold = 0.85;
1624   var equalSlightThreshold = 0.25;
1625
1626   var useNAGeval = (NAGstyle != 'default');
1627   function ev2NAG(ev) {
1628      if ((ev === null) || (ev === "") || (isNaN(ev = parseFloat(ev)))) { return ""; }
1629      if (!useNAGeval) { return (ev > 0 ? "+" : "") + ev + (ev == Math.floor(ev) ? ".0" : ""); }
1630      if (ev < -moderateDefiniteThreshold) { return NAG[19]; } // -+
1631      if (ev >  moderateDefiniteThreshold) { return NAG[18]; } // +-
1632      if (ev < -slightModerateThreshold)   { return NAG[17]; } // -/+
1633      if (ev >  slightModerateThreshold)   { return NAG[16]; } // +/-
1634      if (ev < -equalSlightThreshold)      { return NAG[15]; } // =/+
1635      if (ev >  equalSlightThreshold)      { return NAG[14]; } // +/=
1636      return NAG[11];                                          // =
1637   }
1638
1639   function clearAnalysisHeader() {
1640      var theObj;
1641      if (theObj = document.getElementById("GameAnalysisMove")) { theObj.innerHTML = ""; }
1642      if (theObj = document.getElementById("GameAnalysisEval")) { theObj.innerHTML = "&middot;&nbsp;"; theObj.title = "start annotation"; }
1643      if (theObj = document.getElementById("GameTablebase")) { theObj.style.display = "none"; }
1644      if (theObj = document.getElementById("GameAnalysisPv")) { theObj.innerHTML = ""; }
1645   }
1646
1647
1648   var lastMousemoveAnnPly = -1;
1649   var lastMousemoveAnnGame = -1;
1650
1651   function annotationGraphMouseover(e) {
1652   }
1653
1654   function annotationGraphMouseout(e) {
1655      lastMousemoveAnnPly = -1;
1656      lastMousemoveAnnGame = -1;
1657      if (analysisStarted) { updateAnalysisHeader(); }
1658   }
1659
1660   function annotationGraphMousemove(e) {
1661      var newMousemoveAnnPly = StartPly + Math.floor((e.pageX - document.getElementById("GameAnnotationGraph").offsetLeft) / annotationBarWidth);
1662      if ((newMousemoveAnnPly !== lastMousemoveAnnPly) || (currentGame !== lastMousemoveAnnGame)) {
1663         lastMousemoveAnnPly = newMousemoveAnnPly <= StartPly + PlyNumber ? newMousemoveAnnPly : -1;
1664         lastMousemoveAnnGame = currentGame;
1665         if (analysisStarted) { updateAnalysisHeader(); }
1666      }
1667   }
1668
1669   function annotationGraphClick(e) {
1670      if ((analysisStarted) && (typeof(annotationBarWidth) != "undefined")) {
1671         var annPly = StartPly + Math.floor((e.pageX - document.getElementById("GameAnnotationGraph").offsetLeft) / annotationBarWidth);
1672         if ((annPly >= StartPly) && (annPly <= StartPly + PlyNumber)) {
1673            if (e.shiftKey) { save_cache_to_localStorage(); }
1674            else { GoToMove(annPly); }
1675         }
1676      }
1677   }
1678
1679   function num2string(num) {
1680      var unit = "";
1681      if (num >= Math.pow(10, 12)) { num = Math.round(num / Math.pow(10, 11)) / 10;  unit = "T"; }
1682      else if (num >= Math.pow(10, 9)) { num = Math.round(num / Math.pow(10, 8)) / 10;  unit = "G"; }
1683      else if (num >= Math.pow(10, 6)) { num = Math.round(num / Math.pow(10, 5)) / 10; unit = "M"; }
1684      else if (num >= Math.pow(10, 3)) { num = Math.round(num / Math.pow(10, 2)) / 10; unit = "K"; }
1685      if ((unit !== "") && (num === Math.floor(num))) { num += ".0"; }
1686      return num + unit;
1687   }
1688
1689
1690   var annotateInProgress = null;
1691   var minAnnotationSeconds = Math.max(1, Math.floor(minAutoplayDelay/1000));
1692   var maxAnnotationSeconds = Math.max(100, Math.floor(maxAutoplayDelay/1000));
1693   var annotationSeconds_default = 15;
1694   var annotationSeconds = annotationSeconds_default;
1695
1696   function getAnnotationSecondsFromLocalStorage() {
1697      var as;
1698      try { as = parseFloat(localStorage.getItem("pgn4web_chess_viewer_annotationSeconds")); }
1699      catch(e) { return annotationSeconds_default; }
1700      return ((as === null) || (isNaN(as))) ? annotationSeconds_default : as;
1701   }
1702   function setAnnotationSecondsToLocalStorage(as) {
1703      try { localStorage.setItem("pgn4web_chess_viewer_annotationSeconds", as); }
1704      catch(e) { return false; }
1705      return true;
1706   }
1707
1708
1709   function annotateGame(promptUser) {
1710      if ((checkEngineUnderstandsGameAndWarn()) && (annotationSeconds = promptUser ? prompt("Automated game" + (annotateGameMulti ? "s" : "") + " annotation from the current position; please do not interact with the chessboard until the annotation has completed.\\n\\nEnter annotation time per move, in seconds, between " + minAnnotationSeconds + " and " + maxAnnotationSeconds + ":", getAnnotationSecondsFromLocalStorage()) : getAnnotationSecondsFromLocalStorage())) {
1711         if (isNaN(annotationSeconds = parseFloat(annotationSeconds))) { annotationSeconds = getAnnotationSecondsFromLocalStorage(); }
1712         annotationSeconds = Math.min(maxAnnotationSeconds, Math.max(minAnnotationSeconds, annotationSeconds));
1713         setAnnotationSecondsToLocalStorage(annotationSeconds);
1714         SetAutoPlay(false);
1715         if (!analysisStarted) {
1716           scanGameForFen();
1717           toggleAnalysis();
1718         }
1719         if (annotateInProgress) {
1720            clearTimeout(annotateInProgress);
1721            annotateInProgress = null;
1722         }
1723         var theObj = document.getElementById("GameAnnotationMessage");
1724         if (theObj) {
1725            theObj.innerHTML = "automated game" + (annotateGameMulti ? "s" : "") + " annotation in progress";
1726            theObj.title = theObj.innerHTML + " at " + annotationSeconds + " second" + (annotationSeconds == 1 ? "" : "s") + " per move; please do not interact with the chessboard until the annotation has completed; click here to stop the automated annotation";
1727            theObj.style.display = "";
1728            if (theObj = document.getElementById("GameButtons")) {
1729               theObj.style.display = "none";
1730            }
1731         }
1732         annotateGameStep(CurrentPly, CurrentVar, annotationSeconds * 1000);
1733      }
1734   }
1735
1736   var annotateGameMulti = false;
1737   function annotateGameStep(thisPly, thisVar, thisDelay) {
1738      if (analysisStarted) {
1739         GoToMove(thisPly, thisVar);
1740         var thisCmd = "stopAnnotateGame(true);";
1741         if (thisPly < StartPlyVar[thisVar] + PlyNumberVar[thisVar]) {
1742            thisCmd = "annotateGameStep(" + (thisPly + 1) + ", " + thisVar + ", " + thisDelay + ");";
1743         } else if (thisVar + 1 < numberOfVars) {
1744            thisCmd = "annotateGameStep(" + (StartPlyVar[thisVar + 1] + 1) + ", " + (thisVar + 1) + ", " + thisDelay + ");";
1745         } else if ((annotateGameMulti) && (currentGame + 1 < numberOfGames)) {
1746            thisCmd = "Init(" + (currentGame + 1) + "); GoToMove(StartPly, 0); annotateGame(false);";
1747         }
1748         annotateInProgress = setTimeout(thisCmd, thisDelay);
1749      } else {
1750         stopAnnotateGame(false);
1751         return;
1752      }
1753   }
1754
1755   function stopAnnotateGame(annotationCompleted) {
1756      var theObj = document.getElementById("GameAnnotationMessage");
1757      if (theObj) {
1758         theObj.style.display = ((annotateInProgress) && (annotationCompleted)) ? "" : "none";
1759         theObj.innerHTML = ((annotateInProgress) && (annotationCompleted)) ? "automated game" + (annotateGameMulti ? "s" : "") + " annotation completed" : "";
1760         theObj.title = "";
1761         if (theObj = document.getElementById("GameButtons")) {
1762            theObj.style.display = ((annotateInProgress) && (annotationCompleted)) ? "none" : "";
1763         }
1764      }
1765      if (annotateInProgress) {
1766         clearTimeout(annotateInProgress);
1767         annotateInProgress = null;
1768      }
1769   }
1770
1771   function engineUnderstandsGame(gameNum) {
1772      return gameIsNormalChess(gameNum);
1773   }
1774
1775   function checkEngineUnderstandsGameAndWarn() {
1776      var retVal = engineUnderstandsGame(currentGame);
1777      if (!retVal) { myAlert("warning: engine annotation unavailable for the " + gameVariant[currentGame] + " variant", true); }
1778      return retVal;
1779   }
1780
1781   function userToggleAnalysis() {
1782      if (checkEngineUnderstandsGameAndWarn()) {
1783         if (!analysisStarted) { scanGameForFen(); }
1784         toggleAnalysis();
1785      }
1786   }
1787
1788   function scanGameForFen() {
1789      var index;
1790      var savedCurrentPly = CurrentPly;
1791      var savedCurrentVar = CurrentVar;
1792      var wasAutoPlayOn = isAutoPlayOn;
1793      if (wasAutoPlayOn) { SetAutoPlay(false); }
1794      MoveForward(StartPly + PlyNumber - savedCurrentPly, CurrentVar, true);
1795      resetFenPositions();
1796      while (true) {
1797         fenPositions[CurrentPly] = CurrentFEN();
1798         if ((index = cache_fen_lastIndexOf(fenPositions[CurrentPly])) != -1) {
1799            fenPositionsEval[CurrentPly] = cache_ev[index];
1800            fenPositionsPv[CurrentPly] = cache_pv[index];
1801            fenPositionsDepth[CurrentPly] = cache_depth[index];
1802         }
1803         if (CurrentPly === StartPly) { break; }
1804         MoveBackward(1, true);
1805      }
1806      MoveForward(savedCurrentPly - StartPly, savedCurrentVar, true);
1807      updateAnnotationGraph();
1808      updateAnalysisHeader();
1809      if (wasAutoPlayOn) { SetAutoPlay(true); }
1810   }
1811
1812   function goToMissingAnalysis(forward) {
1813      if (!analysisStarted) { return; }
1814      if (typeof(fenPositions[CurrentPly]) == "undefined") { return; }
1815      if (typeof(fenPositionsEval[CurrentPly]) == "undefined") { return; }
1816
1817      if (typeof(forward) == "undefined") {
1818         forward = ((typeof(event) != "undefined") && (typeof(event.shiftKey) != "undefined")) ? !event.shiftKey : true;
1819      }
1820      var wasAutoPlayOn = isAutoPlayOn;
1821      if (wasAutoPlayOn) { SetAutoPlay(false); }
1822      for (var thisPly = CurrentPly + (forward ? 1 : -1); ; thisPly = thisPly + (forward ? 1 : -1)) {
1823         if (forward) { if (thisPly > StartPly + PlyNumber) { thisPly = StartPly; } }
1824         else { if (thisPly < StartPly) { thisPly = StartPly + PlyNumber; } }
1825         if (thisPly === CurrentPly) { break; }
1826         if (typeof(fenPositions[thisPly]) == "undefined") { break; }
1827         if (typeof(fenPositionsEval[thisPly]) == "undefined") { GoToMove(thisPly); break; }
1828      }
1829      if (wasAutoPlayOn) { SetAutoPlay(true); }
1830   }
1831
1832
1833   var mistakeThreshold = 0.55;
1834   var blunderThreshold = 1.15;
1835   var ignoreThreshold = 4.95;
1836
1837   function isBlunder(thisPly, threshold) {
1838      if (typeof(fenPositionsEval[thisPly]) == "undefined") { return false; }
1839      if (typeof(fenPositionsEval[thisPly - 1]) == "undefined") { return false; }
1840      if ((fenPositionsEval[thisPly] > ignoreThreshold) && (fenPositionsEval[thisPly - 1] > ignoreThreshold)) { return false; }
1841      if ((fenPositionsEval[thisPly] < -ignoreThreshold) && (fenPositionsEval[thisPly - 1] < -ignoreThreshold)) { return false; }
1842      return (((thisPly % 2 ? -1 : 1) * (fenPositionsEval[thisPly] - fenPositionsEval[thisPly - 1])) > threshold);
1843   }
1844
1845   function blunderCheck(threshold, backwards) {
1846      var thisPly = StartPly + ((CurrentPly - StartPly + (backwards ? -1 : 1) + (PlyNumber + 1)) % (PlyNumber + 1));
1847      while (thisPly !== CurrentPly) {
1848         if (isBlunder(thisPly, threshold)) {
1849            GoToMove(thisPly);
1850            break;
1851         }
1852         thisPly = StartPly + ((thisPly - StartPly + (backwards ? -1 : 1) + (PlyNumber + 1)) % (PlyNumber + 1));
1853      }
1854   }
1855
1856   function annotationSupportedCheckAndWarnUser() {
1857      if (!annotationSupported) { myAlert("warning: engine annotation unavailable", true); }
1858      return annotationSupported;
1859   }
1860
1861
1862   // F5
1863   boardShortcut("F5", "adjust last move and current comment text area, if present", function(t,e){ if (e.shiftKey) { resetLastCommentArea(); } else { cycleLastCommentArea(); } });
1864
1865   // A6
1866   boardShortcut("A6", "go to previous annotated blunder", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { if (e.shiftKey) { GoToMove(CurrentPly - 1); } else { if (!analysisStarted) { userToggleAnalysis(); } blunderCheck(blunderThreshold, true); } } });
1867   // B6
1868   boardShortcut("B6", "go to previous annotated mistake", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { if (e.shiftKey) { GoToMove(CurrentPly - 1); } else { if (!analysisStarted) { userToggleAnalysis(); } blunderCheck(mistakeThreshold, true); } } });
1869   // G6
1870   boardShortcut("G6", "go to next annotated mistake", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { if (e.shiftKey) { GoToMove(CurrentPly - 1); } else { if (!analysisStarted) { userToggleAnalysis(); } blunderCheck(mistakeThreshold, false); } } });
1871   // H6
1872   boardShortcut("H6", "go to next annotated blunder", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { if (e.shiftKey) { GoToMove(CurrentPly - 1); } else { if (!analysisStarted) { userToggleAnalysis(); } blunderCheck(blunderThreshold, false); } } });
1873
1874   // G5
1875   boardShortcut("G5", "start/stop automated game annotation", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { annotateGameMulti = e.shiftKey; if (annotateInProgress) { stopAnnotateGame(false); } else { annotateGame(true); } } });
1876   // H5
1877   boardShortcut("H5", "start/stop annotation", function(t,e){ if (annotationSupportedCheckAndWarnUser()) { if (e.shiftKey) { if (confirm("clear annotation cache, all current and stored annotation data will be lost")) { clear_cache_from_localStorage(); cache_clear(); if (analysisStarted) { updateAnnotationGraph(); updateAnalysisHeader(); } } } else { userToggleAnalysis(); } } });
1878
1879
1880   var pgn4web_chess_engine_id = "garbochess-pgn4web-" + pgn4web_version;
1881
1882   var engineWorker = "libs/garbochess/garbochess.js";
1883
1884   var g_backgroundEngine;
1885   var g_topNodesPerSecond = 0;
1886   var g_ev = "";
1887   var g_maxEv = 99.9;
1888   var g_pv = "";
1889   var g_depth = "";
1890   var g_nodes = "";
1891   var g_initErrors = 0;
1892   var g_lastFenError = "";
1893
1894   function InitializeBackgroundEngine() {
1895
1896      if (!g_backgroundEngine) {
1897         try {
1898            g_backgroundEngine = new Worker(engineWorker);
1899            g_backgroundEngine.addEventListener("message", function (e) {
1900               if ((e.data.match("^pv")) && (fenString == CurrentFEN())) {
1901                  var matches = e.data.substr(3, e.data.length - 3).match(/Ply:(\d+) Score:(-*\d+) Nodes:(\d+) NPS:(\d+) (.*)/);
1902                  if (matches) {
1903                     g_depth = parseInt(matches[1], 10);
1904                     if (isNaN(g_ev = parseInt(matches[2], 10))) {
1905                        g_ev = "";
1906                     } else {
1907                        g_ev = Math.round(g_ev / 100) / 10;
1908                        if (g_ev < -g_maxEv) { g_ev = -g_maxEv; } else if (g_ev > g_maxEv) { g_ev = g_maxEv; }
1909                        if (fenString.indexOf(" b ") !== -1) { g_ev = -g_ev; }
1910                     }
1911                     g_nodes = parseInt(matches[3], 10);
1912                     var nodesPerSecond = parseInt(matches[4], 10);
1913                     g_topNodesPerSecond = Math.max(nodesPerSecond, g_topNodesPerSecond);
1914                     g_pv = matches[5].replace(/(^\s+|\s*[x+=]|\s+$)/g, "").replace(/\s*stalemate/, "=").replace(/\s*checkmate/, "#"); // patch: pgn notation: remove/add '+' 'x' '=' chars for full chess informant style or pgn style for the game text
1915                     if (searchMeaningful()) {
1916                        validateSearchWithCache();
1917                        if ((typeof(fenPositionsDepth[CurrentPly]) == "undefined") || (g_depth > fenPositionsDepth[CurrentPly])) {
1918                           fenPositionsEval[CurrentPly] = g_ev;
1919                           fenPositionsPv[CurrentPly] = g_pv;
1920                           fenPositionsDepth[CurrentPly] = g_depth;
1921                           updateAnnotationGraph();
1922                           updateAnalysisHeader();
1923                        }
1924                     }
1925                     if (detectGameEnd(g_pv, "")) { StopBackgroundEngine(); }
1926                  }
1927               } else if (e.data.match("^message Invalid FEN")) {
1928                  stopAnalysis();
1929                  if (fenString != g_lastFenError) {
1930                     g_lastFenError = fenString;
1931                     myAlert("error: engine: " + e.data.replace(/^message /, "") + "\\n" + fenString);
1932                  }
1933               }
1934            }, false);
1935            g_initErrors = 0;
1936            return true;
1937         } catch(e) {
1938            stopAnalysis();
1939            if (!g_initErrors++) { myAlert("error: engine exception " + e); }
1940            return false;
1941         }
1942      }
1943   }
1944
1945   var cache_local_storage_prefix = "pgn4web_chess_viewer_engine_cache_"; // default "pgn4web_chess_engine_cache_"
1946
1947   var localStorage_supported;
1948   try { localStorage_supported = (("localStorage" in window) && (window["localStorage"] !== null)); }
1949   catch(e) { localStorage_supported = false; }
1950
1951   function load_cache_from_localStorage() {
1952      if (!localStorage_supported) { return; }
1953      if (pgn4web_chess_engine_id != localStorage[cache_local_storage_prefix + "id"]) {
1954         clear_cache_from_localStorage();
1955         localStorage[cache_local_storage_prefix + "id"] = pgn4web_chess_engine_id;
1956         return;
1957      }
1958      if (cache_pointer = localStorage[cache_local_storage_prefix + "pointer"]) {
1959         cache_pointer = parseInt(cache_pointer, 10) % cache_max;
1960      } else { cache_pointer = -1; }
1961      if (cache_fen = localStorage[cache_local_storage_prefix + "fen"]) {
1962         cache_fen = cache_fen.split(",");
1963      } else { cache_fen = new Array(); }
1964      if (cache_ev = localStorage[cache_local_storage_prefix + "ev"]) {
1965         cache_ev = cache_ev.split(",");
1966         if (typeof(cache_ev.map == "function")) { cache_ev = cache_ev.map(parseFloat); }
1967      } else { cache_ev = new Array(); }
1968      if (cache_pv = localStorage[cache_local_storage_prefix + "pv"]) {
1969         cache_pv = cache_pv.split(",");
1970      } else { cache_pv = new Array(); }
1971      if (cache_depth = localStorage[cache_local_storage_prefix + "depth"]) {
1972         cache_depth = cache_depth.split(",");
1973         if (typeof(cache_depth.map == "function")) { cache_depth = cache_depth.map(parseFloat); }
1974      } else { cache_depth = new Array(); }
1975      cache_needs_sync = 0;
1976      if ((cache_fen.length !== cache_ev.length) || (cache_fen.length !== cache_pv.length) || (cache_fen.length !== cache_depth.length)) {
1977         clear_cache_from_localStorage();
1978         cache_clear();
1979      }
1980   }
1981
1982   function save_cache_to_localStorage() {
1983      if (!localStorage_supported) { return; }
1984      if (!cache_needs_sync) { return; }
1985      localStorage[cache_local_storage_prefix + "pointer"] = cache_pointer;
1986      localStorage[cache_local_storage_prefix + "fen"] = cache_fen.toString();
1987      localStorage[cache_local_storage_prefix + "ev"] = cache_ev.toString();
1988      localStorage[cache_local_storage_prefix + "pv"] = cache_pv.toString();
1989      localStorage[cache_local_storage_prefix + "depth"] = cache_depth.toString();
1990      cache_needs_sync = 0;
1991   }
1992
1993   function clear_cache_from_localStorage() {
1994      if (!localStorage_supported) { return; }
1995      localStorage.removeItem(cache_local_storage_prefix + "pointer");
1996      localStorage.removeItem(cache_local_storage_prefix + "fen");
1997      localStorage.removeItem(cache_local_storage_prefix + "ev");
1998      localStorage.removeItem(cache_local_storage_prefix + "pv");
1999      localStorage.removeItem(cache_local_storage_prefix + "depth");
2000      localStorage.removeItem(cache_local_storage_prefix + "nodes"); // backward compatibility
2001      cache_needs_sync++;
2002   }
2003
2004   function cacheDebugInfo() {
2005      var dbg = "";
2006      if (localStorage_supported) {
2007         dbg += " cache=";
2008         try {
2009            dbg += num2string(localStorage[cache_local_storage_prefix + "pointer"].length + localStorage[cache_local_storage_prefix + "fen"].length + localStorage[cache_local_storage_prefix + "ev"].length + localStorage[cache_local_storage_prefix + "pv"].length + localStorage[cache_local_storage_prefix + "depth"].length);
2010         } catch(e) {
2011            dbg += "0";
2012         }
2013      }
2014      return dbg;
2015   }
2016
2017   var cache_pointer = -1;
2018   var cache_max = 8000; // ~ 64 games of 60 moves ~ 1MB of local storage
2019   var cache_fen = new Array();
2020   var cache_ev = new Array();
2021   var cache_pv = new Array();
2022   var cache_depth = new Array();
2023
2024   var cache_needs_sync = 0;
2025
2026   load_cache_from_localStorage();
2027
2028   function searchMeaningful() {
2029      var minNodesForAnnotation = 12345;
2030      return ((g_nodes > minNodesForAnnotation) || (g_ev === g_maxEv) || (g_ev === -g_maxEv) || (g_ev === 0));
2031   }
2032
2033   function validateSearchWithCache() {
2034      var id = cache_fen_lastIndexOf(fenString);
2035      if (id == -1) {
2036         cache_last = cache_pointer = (cache_pointer + 1) % cache_max;
2037         cache_fen[cache_pointer] = fenString.replace(/\s+\d+\s+\d+\s*$/, "");
2038         cache_ev[cache_pointer] = g_ev;
2039         cache_pv[cache_pointer] = g_pv;
2040         cache_depth[cache_pointer] = g_depth;
2041         cache_needs_sync++;
2042      } else {
2043         if (g_depth > cache_depth[id]) {
2044            cache_ev[id] = g_ev;
2045            cache_pv[id] = g_pv;
2046            cache_depth[id] = g_depth;
2047            cache_needs_sync++;
2048         } else {
2049            g_ev = parseFloat(cache_ev[id]);
2050            g_pv = cache_pv[id];
2051            g_depth = parseInt(cache_depth[id], 10);
2052         }
2053      }
2054      if (cache_needs_sync > 3) { save_cache_to_localStorage(); }
2055   }
2056
2057   var cache_last = 0;
2058   function cache_fen_lastIndexOf(fenString) {
2059      fenString = fenString.replace(/\s+\d+\s+\d+\s*$/, "");
2060      if (fenString === cache_fen[cache_last]) { return cache_last; }
2061      if (typeof(cache_fen.lastIndexOf) == "function") { return (cache_last = cache_fen.lastIndexOf(fenString)); }
2062      for (var n = cache_fen.length - 1; n >= 0; n--) {
2063         if (fenString === cache_fen[n]) { return (cache_last = n); }
2064      }
2065      return -1;
2066   }
2067
2068   function cache_clear() {
2069      cache_pointer = -1;
2070      cache_fen = new Array();
2071      cache_ev = new Array();
2072      cache_pv = new Array();
2073      cache_depth = new Array();
2074   }
2075
2076
2077   function StopBackgroundEngine() {
2078      if (analysisTimeout) { clearTimeout(analysisTimeout); }
2079      if (g_backgroundEngine) {
2080         g_backgroundEngine.terminate();
2081         g_backgroundEngine = null;
2082      }
2083   }
2084
2085   var analysisTimeout;
2086   function setAnalysisTimeout(seconds) {
2087      if (analysisTimeout) { clearTimeout(analysisTimeout); }
2088      analysisTimeout = setTimeout("analysisTimeout = null; save_cache_to_localStorage(); StopBackgroundEngine();", seconds * 1000);
2089   }
2090
2091   var fenString;
2092   function StartEngineAnalysis() {
2093      StopBackgroundEngine();
2094      if (InitializeBackgroundEngine()) {
2095         fenString = CurrentFEN();
2096         g_backgroundEngine.postMessage("position " + fenString);
2097         g_backgroundEngine.postMessage("analyze");
2098         setAnalysisTimeout(analysisSeconds);
2099         return true;
2100      } else {
2101         stopAnnotateGame(false);
2102         return false;
2103      }
2104   }
2105
2106   var analysisSeconds = 300;
2107
2108   function detectGameEnd(pv, FEN) {
2109      if ((pv !== "") && (pv.match(/^[#=]/))) { return true; }
2110      var matches = FEN.match(/\s*\S+\s+\S+\s+\S+\s+\S+\s+(\d+)\s+\S+\s*/);
2111      if ((matches) && (parseInt(matches[1], 10) > 100)) { return true; }
2112      return false;
2113   }
2114
2115   function customDebugInfo() {
2116      var dbg = "initialHalfmove=" + initialHalfmove;
2117      dbg += " annotation=";
2118      if (!annotationSupported) { dbg += "unavailable"; }
2119      else if (!analysisStarted) { dbg += "disabled"; }
2120      else { dbg += (g_backgroundEngine ? ( annotateInProgress ? ("automatedGame" + (annotateGameMulti ? "s" : "") + " annotationSeconds=" + getAnnotationSecondsFromLocalStorage()) : "pondering") : "idle") + " analysisSeconds=" + analysisSeconds + " topNodesPerSecond=" + num2string(g_topNodesPerSecond) + cacheDebugInfo(); }
2121      if ("$forceEncodingFrom") { dbg += " forceEncodingFrom=$forceEncodingFrom"; }
2122      return dbg;
2123   }
2124
2125   window.onunload = function() {
2126      setDelayToLocalStorage(Delay);
2127      setHighlightOptionToLocalStorage(highlightOption);
2128      setCommentsIntoMoveTextToLocalStorage(commentsIntoMoveText);
2129      setCommentsOnSeparateLinesToLocalStorage(commentsOnSeparateLines);
2130      if (analysisStarted) { stopAnalysis(); }
2131   };
2132
2133</script>
2134
2135
2136END;
2137}
2138
2139function print_footer() {
2140
2141  global $pgnText, $pgnTextbox, $pgnUrl, $pgnFileName, $pgnFileSize, $pgnStatus, $forceEncodingFrom, $tmpDir, $debugHelpText, $pgnDebugInfo;
2142  global $fileUploadLimitIniText, $fileUploadLimitText, $fileUploadLimitBytes, $startPosition, $goToView, $zipSupported;
2143
2144  if ($goToView) { $hashStatement = "  goToHash('board');"; }
2145  else { $hashStatement = ""; }
2146
2147  if (($pgnDebugInfo) != "") { $pgnDebugMessage = "warning: system: " . $pgnDebugInfo; }
2148  else {$pgnDebugMessage = ""; }
2149
2150  print <<<END
2151
2152<script type="text/javascript">
2153"use strict";
2154
2155function pgn4web_onload(e) {
2156  setPgnUrl("$pgnUrl");
2157  checkPgnFormTextSize();
2158  start_pgn4web();
2159  if ("$pgnDebugMessage".length > 0) { myAlert("$pgnDebugMessage", false, true); }
2160$hashStatement
2161}
2162
2163</script>
2164
2165END;
2166}
2167
2168function print_html_close() {
2169
2170  print <<<END
2171
2172</body>
2173
2174</html>
2175END;
2176}
2177
2178?>
2179