Newer
Older
jstrr / jstrr.js
(function (){
    // var wsURL = "wss://tmp.iekei.org/jstype";
    var wsURL = "wss://" + location.host + "/jtserv";
    var ws;			// for WebSocket Object
    var textlist = [];
    var nText = nText||4;
    var nLine = nLine||3;
    var area = [];
    var team = {}, teamnames = new Set(), tmsel, nmsel, txsel;
    var mystate = {};
    var infoBox;
    var loginhead, prompt, input, tmpkey;
    var singleTextMode = true;
    //
    // https://developer.mozilla.org/ja/docs/Web/API/Element/classList
    function setElementIDText(id, text) {
	document.getElementById(id).innerText = text;
    }
    function loadTextToElement1(text) {
	console.log("elm1="+mystate.elm1);
	mystate.elm1.textContent = text[0];
	mystate.elm2.textContent = text.substr(1);
	resetState();
    }
    function loadTextToElement(hash, elm1, elm2) {
	sendJSONtoServer({
	    "gettext": hash.file,
	    "lines": nLine,
	    "fill": 60
	});
	mystate.elm1 = elm1;
	mystate.elm2 = elm2;
    }
    function updateMemberOption(e) {
	let tn = tmsel.value;
	console.log(nmsel.childNodes);
	// We should repeat remove lastChild when deleting all children
	// https://into-the-program.com/removechild/
	while (nmsel.lastChild) nmsel.removeChild(nmsel.lastChild);
	for (let mem of team[tn]) {
	    let o = document.createElement("option")
	    o.textContent = mem.name;
	    nmsel.appendChild(o);
	    o.selected = true;
	}
    }
    function sendJSONtoServer(json) {
	if (ws.readyState == WebSocket.OPEN) { // ==1
	    ws.send(JSON.stringify(json));
	}
    }
    function sendMynameToServer() {
	sendJSONtoServer({"name": nmsel.value, "team": tmsel.value});
    }
    function loadTeamList(file, elm) {
	fetch(file).then((resp) => {
	    if (resp.ok) return resp.text();
	}).then((txt) => {
	    let teamcsv = new CSV(txt, {header: true}).parse();
	    let tmpa;
	    for (let row of teamcsv) { // Collect all team members
		if (!team[row.teamname]) team[row.teamname]=[];
		team[row.teamname].push({uid: row.uid, name: row.name});
	    }
	    if (tmsel) {
		for (let tn of Object.keys(team)) {
		    let o = document.createElement("option")
		    o.textContent = tn;
		    tmsel.appendChild(o);
		}
		updateMemberOption();
		tmsel.addEventListener("change", updateMemberOption);
		nmsel.addEventListener("change", sendMynameToServer);
		console.log(Object.keys(team));
	    }
	});
    }
    // Typing engine
    var pos=0, nArea=0;
    function switchToArea(n) {
	if (singleTextMode) {
	    for (let i=0; i<area.length; i++) {
		area[i].style.display = (i==n ? "block" : "none");
	    }
	}
	area[nArea].classList.remove("current");
	nArea = n;
	if (nArea < area.length) {
	    area[nArea].classList.add("current");
	}
    }
    function startTrr(time) {
	mystate.start = time;
	sendJSONtoServer({
	    "text":	null,  // XXXXX not yet
	    "start":	time
	});
	info.classList.add("go");
	infoBox.innerText = "GO!!";
	mystate.miss = 0;
	document.body.addEventListener("click", _entryFocus);
    }
    function commentScore(score) {
	if (score < 10)
	    return "努力あるのみ";
	else if (score < 50)
	    return "まず正しい指使いを覚えなさい";
	else if (score < 100)
	    return "見ながら打っているようではダメよ";
	else if (score < 160)
	    return "ここからが勝負よ";
	else if (score < 200)
	    return "あと一息で業界標準の200点よ";
	else if (score < 220)
	    return "業界標準ね";
	else if (score < 300)
	    return "業界目標の300点までもう少し!";
	else if (score < 400)
	    return "業界一流の400点は行っておかないと";
	else
	    return "業界一流ね. ここから先は自分との戦いよ";
    }
    function updateScoreField() {
	let text = textlist[nArea]["file"],
	    data = mystate.scoreinfo[text];
	setElementIDText("name", mystate.scoreinfo.gecos);
	if (data) {
	    let st   = mystate.scoreinfo.steptrial[text][data.step];
	    setElementIDText("step", data.step);
	    setElementIDText("target", 10*(data.step+1));
	    setElementIDText("hs", data.highscore);
	    setElementIDText("trial", data.trial);
	    setElementIDText("strial", st);
	} else {
	    for (let i of ["step", "target", "hs", "trial", "strial"])
		setElementIDText(i, "");
	}
    }
    function finishTrr(data) {
	document.body.removeEventListener("click", _entryFocus);
	setElementIDText("time", data.time+"s");
	setElementIDText("score", Math.max(Math.round(data.score), 0));
	setElementIDText("types", mystate.sum);
	setElementIDText("speed", data.speed);
	setElementIDText("comment", data.message+"\n(TEXT選択ボタンで再挑戦)");
	mystate.scoreinfo = data.scoreinfo;
	updateScoreField();
	setElementIDText("info", "GOAL!!");
    }
    function sendFinishTrr() {
	let finish = Date.now()/1000;
	let elapse = finish - mystate.start, sum=0;
	mystate.sum = sum = area[nArea].textContent.length+1;
	console.log("nArea="+nArea);
	console.log("LEN="+sum);
	console.log("elapse="+elapse);
	sendJSONtoServer({
	    "text":	textlist[nArea]["file"],
	    "start":	mystate.start,
	    "finish":	finish,
	    "miss":	mystate.miss,
	    "types":	sum,
	    "trrmode":	mystate.trrmode
	});
    }
    function trr(e) {
	e.preventDefault();
	let done  = area[nArea].childNodes[0],
	    curs  = area[nArea].childNodes[1],
	    text  = area[nArea].childNodes[2],
	    inp   = e.target;
	let str, len=done.textContent.length;
	if (e.keyCode == 13) {
	    console.log("ENTER");
	    str = "\n";
	} else {
	    str = inp.value;
	}
	if (mystate.finish > 0) {
	    return;
	} else if (mystate.start == 0) {
	    if (e.keyCode == 8) { // First Hit of [BS] Start Self-training
		startTrr(Date.now()/1000);
		mystate.singleUserMode = true;
		return;
	    } else {
		info.innerText = "まだだよ(BSキーで練習スタート)";
	    }
	} else {
	    if (str.length > 10) {
		alert("あら、なにかおかしいわね。");
		alert("頭を冷やして明日またやりましょう。");
		alert("もしかして日本語入力をONにしてしまったとかかしら?");
		inp.value = ""
		resetState();
		return;
	    }
	    while (str > "") {
		if (str[0] == curs.textContent) {
		    if (text.textContent > "") {
			if (mystate.missflag) {
			    done.innerHTML +=
				'<span class="miss">'+str[0]+'</span>';
			    console.log("done="+done.innerHTML);
			} else {
			    console.log("X"+str);
			    done.innerHTML += str[0];
			    console.log("DONE="+done.innerHTML);
			}
			mystate.missflag = false;
			curs.textContent = text.textContent[0];
		    } else {
			curs.textContent = ""; // Prevent adding newline
		    }
		    pos++;
		    text.textContent = text.textContent.substr(1);
		} else {
		    console.log("MISSSSSSSS"+str);
		    mystate.miss++;
		    mystate.missflag = true;
		}
		str = str.substr(1);
	    }
	    sendJSONtoServer({"types": done.textContent.length});
	    setElementIDText("miss", mystate.miss);
	    if (len>0) {
		let ratio = "     "+Math.round(1000*mystate.miss/len)/10+"%";
		ratio = ratio.substr(-6);
		setElementIDText("ratio", ratio);
	    }
	}
	inp.value = "";
	if (curs.textContent == "") {
	    mystate.finish = Date.now()/1000;
	    if (nArea+1 < area.length && !mystate.singleUserMode) {
		switchToArea(++nArea);
	    } else {	// Finished!!
		// finishTrr();
		sendFinishTrr();
	    }
	}
    }
    function selTextAndSend(e) {
	let n = e.target.value;
	for (let j=0; j<area.length; j++) {
	    if (n==j)
		area[j].classList.add("mytext");
	    else
		area[j].classList.remove("mytext");
	}
	console.log("TL="+textlist)
	sendJSONtoServer({"settext": n});
	// updateScoreField();
    }
    function clearText() {
	let stage = area[nArea];
	stage.querySelector(".done").textContent = "";
	stage.querySelector(".cursor").textContent = "";
	stage.querySelector(".text").textContent = "";
    }
    function reloadText(n) {		// Reload text(n) to typing field
	let stage = area[n],
	    dne = stage.querySelector(".done"),
	    cur = stage.querySelector(".cursor"),
	    txt = stage.querySelector(".text");
	console.log("stage="+stage);
	console.log("cur="+cur);
	console.log("txt="+txt);
	dne.textContent = "";
	loadTextToElement(textlist[n], cur, txt);
	switchToArea(n);
	setTimeout(()=>{entry.focus();}, 100);
    }
    function reloadTextByBtn(e) {
	let n = e.target.value;
	if (n<0 || n>=textlist.length) return;
	reloadText(n);
    }
    function xxx_prepareStage() {
	fetch("TEXT").then((resp) => {
	    if (resp.ok) return resp.text();
	}).then(async (text) => { // Should be async func for serial loading
	    let tl0 = text.split("\n"), tl=[], f;
	    for (f of tl0)
		if (f!="\n" && f!="")
		    tl.push(f);	// Eliminate non-filenames
	    for (f of tl) {
		// console.log("LOADING "+f);
		let csv = f.split(","), file=csv[0], title=csv[1];
		if (false) {
		    // XXXXXXXXXXXXXXXXXXXXXXXXXXXX
		    await fetch(file).then((r)=>{ // Load sequentially
			if (r.ok) return r.text();
		    }).then((text) => {
			textlist.push({"file":file, "title":title, "text":text});
			if (textlist.length == tl.length)
			    createStage();
		    });
		} else {
		    textlist.push({"file":file, "title":title});
		}
	    }
	    createStage();
	    if ("scoreinfo" in mystate) {
		txsel.querySelector("label+label>input").click();
		//Fake update by click()
		setTimeout(()=>{txsel.querySelector("input").click()}, 100);
		// setElementIDText("hs", hs.text||"0")
	    }
	});
    }
    function prepareStage(textfiles) {
	if (textlist.length > 1) return;	// Maybe 2nd time
	textlist = textfiles;
	createStage();
	if ("scoreinfo" in mystate) {
	    txsel.querySelector("label+label>input").click();
	    //Fake update by click()
	    setTimeout(()=>{txsel.querySelector("input").click()}, 100);
	    // setElementIDText("hs", hs.text||"0")
	}
	let entry = document.getElementById("entry");
	entry.focus();
	entry.addEventListener("keyup", trr, false);
    }
    function createStage() {
	let stage = document.getElementById("typing");
	// for (let i=0; i<nText; i++)
	for (let i=0; i<textlist.length; i++) {
	    let pre  = document.createElement("pre"),
		done = document.createElement("span"),
		curs = document.createElement("span"),
		text = document.createElement("span"),
		rbtn = document.createElement("input");
	    let texthash = textlist[i];
	    done.classList.add("done");
	    curs.classList.add("cursor");
	    text.classList.add("text");
	    stage.appendChild(pre);
	    pre.appendChild(done);
	    pre.appendChild(curs);
	    pre.appendChild(text);
	    pre.setAttribute("contentEditable", false);
	    area.push(pre)
	    // console.log("area==="+area);
	    let textlabel = document.createElement("button");
	    textlabel.textContent = texthash.title;
	    //textlabel = document.createTextNode(texthash.file);
	    loadTextToElement(texthash, curs, text)
	    rbtn.setAttribute("type", "radio");
	    rbtn.setAttribute("name", "text");
	    rbtn.setAttribute("value", i);
	    rbtn.addEventListener("change", selTextAndSend);
	    rbtn.addEventListener("click", reloadTextByBtn);
	    let label = document.createElement("label");
	    // https://qiita.com/sola-msr/items/bdec752da00c5ab677b3
	    textlabel.style.pointerEvents = "none";
	    label.appendChild(rbtn);
	    label.appendChild(textlabel);
	    txsel.appendChild(label);
	}
	switchToArea(0);
    }
    function filterSameTeam(e) {
	e.stopPropagation();
	let tbl = document.getElementById("ranking-table");
	let team = e.target.textContent, body = document.body;
	mystate.collapsed = mystate.collapsed ? false : team;
	let visible = mystate.collapsed ? "collapse" : "visible",
	    rows = document.querySelectorAll("#ranking-table tr");
	for (let i=1; i<rows.length; i++) {	// i==0 is header line, skip it
	    let tr = rows[i];
	    tr.style.visibility =
		tr.classList.contains(team) ? "visible" : visible;
	}
	e.target.scrollIntoView({behavior: "smooth", block: "center"});
	if (mystate.collapsed)
	    tbl.addEventListener("click", filterSameTeam);
	else
	    tbl.removeEventListener("click", filterSameTeam);
    }
    function showRank(list) {
	mystate.ranklist = list;
	mystate.collapsed = false;
	// user, max(score), max(step), count(score), sum(time)/60, max(at)
	let tbl = document.getElementById("ranking-table"), me, teamcol,
	    rankinteam = {};
	if (false) {
	    for (let i=0; i<40; i++) {
		let tr = document.createElement("tr")
		tr.innerHTML="<tr><th>"+i+"</th></tr>";
		tbl.appendChild(tr);
	    }
	}
	for (let i in list) {
	    let tr   = document.createElement("tr"),
		rank = document.createElement("th");
	    if (i==0) {				// Header row
		if (list.mode == null) {
		    for (let c in list[i])
			if (list[i][c] == "Team") {
			    teamcol = c
			    break;
			}
		}
	    } else {
		rank.textContent = parseInt(i);	// for contents rows
	    }
	    tr.appendChild(rank);
	    for (let j=0; j<list[i].length; j++) {
		let td = document.createElement(i==0?"th":"td");
		td.textContent = list[i][j];
		tr.appendChild(td);
		if (j == teamcol) {		// Add inTeam ranking hook
		    let tm = list[i][j];
		    if (tm > "") {
			tr.classList.add(tm);
			td.addEventListener("click", filterSameTeam);
			if (!rankinteam[tm]) rankinteam[tm] = 0;
			td.title = (++rankinteam[tm]) + " in "+tm;
		    }
		}
	    }
	    tbl.appendChild(tr);
	    if (mystate.user == list[i][0]) me = tr;
	}
	if (me) {
	    me.classList.add("me");
	    setTimeout(() => {
		me.scrollIntoView({behavior: "smooth", block: "center"});
		console.log(me);
	    }, 1000);
	}
    }
    function downloadCSV(ev) {
	ev.stopPropagation();
	if (!mystate.ranklist) return;
	let list = mystate.ranklist,
	    mode = mystate.rankmode,
	    team = mystate.collapsed,
	    str="",
	    text = textlist[nArea]["file"],
	    file = text+(mode ? "-"+mode : "") + (team?"_"+team:"")+".csv"
	if (team) {
	    str = "Text" + list[0].join(",") + "\n";
	    let tbl = document.getElementById("ranking-table");
	    for (let row of tbl.querySelectorAll("tr")) {
		if (row.classList.contains(mystate.collapsed)) {
		    str += text;
		    for (let col of row.querySelectorAll("td"))
			str += ","+col.textContent;
		    str += "\n";
		}
	    }
	} else {
	    for (let i in list) {
		str += (i==0?"Text":text)+"," + list[i].join(",") + "\n";
	    }
	}
	let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
	var a = document.createElement("a");
	
	var csv = new Blob([bom, str], {type: "text/csv"});
	var uri = URL.createObjectURL(csv);
	a.download = file;
	a.href = uri;
	document.body.appendChild(a);
	a.click();
	document.body.removeChild(a);
    }
    function getServerMessage(e) {
	console.log("Get: "+e.data);
	let data = JSON.parse(e.data);
	if (data.myid) {
	    mystate.myid = data.myid;
	    //infoBox.innerText = "Ready";
	    resetState();
	} else if (data.start) {
	    startTrr(data.start);
	} else if (data.disable) {
	    for (let lbl of txsel.childNodes) {
		let inp = lbl.firstChild;
		console.log(inp.tagName);
		console.log(inp.value==data.disable);
		if (!inp.tagName.match(/input/i)) continue;
		if (inp.value==data.disable) {
		    inp.disabled = true;
		    lbl.classList.add("disabled");
		} else {
		    inp.disabled = false;
		    lbl.classList.remove("disabled");
		}
	    }
	} else if (data.tmpkey) {
	    input.disabled = false;
	    if (data.email) {
		loginhead.innerText = "Sent passcode to "+data.email;
		prompt.innerText = "Passcode"
		tmpkey = data.tmpkey;	// Don't save tmp-skey
		mystate.user = data.user;
		input.value = "";
		input.setAttribute("type", "password");
	    } else {
		loginhead.innerText = "Input Your Login ID";
	    }
	    input.focus();
	} else if ("step" in data) {
	    finishTrr(data);
	} else if (data.fail) {
	    input.disabled = false;
	    switch (data.fail) {
	    case "fail":
		loginhead.innerHTML = "Invalid passcode<br>Try again ";
		logdiv.classList.remove("login2");
		input.value = "";
		input.focus();
		break;
	    case "byebye":
		loginhead.innerHTML = "Too many login failure... Reloading ";
		setTimeout(logout, 2000);
		break;
	    case "nokey":
		// alert("Ooops - session forcibly been timeout")
		_reset();
		break;
	    case "overlimit":
		alert(data.message);
		break;
	    }
	} else if (data.user && data.skey) {
	    console.log("You are "+data.user);
	    prepareStage(data.textfiles);
	    let logdiv = document.querySelector("div.login");
	    localStorage.setItem("trrSkey", data.skey);
	    localStorage.setItem("trrUser", data.user);
	    tmpkey = null;		// Reset temporary skey
	    console.log("LSK="+localStorage.getItem("trrSkey"));
	    console.log("LSU="+localStorage.getItem("trrUser"));
	    mystate.user = data.user;
	    mystate.scoreinfo = data.scoreinfo;
	    setElementIDText("user", data.user);
	    if (logdiv) {
		logdiv.setAttribute("class", "login2");
		setTimeout(() => {
		    logdiv.remove();
		    console.log("BYEBYE");
		}, 1000);
	    }
	} else if (data.scoreinfo) {
	    mystate.scoreinfo = data.scoreinfo;
	    updateScoreField();
	} else if (data.ranking) {
	    console.log("RANKING="+JSON.stringify(data.ranking));
	    showRank(data.ranking);
	} else if (data.yourtext) {
	    console.log("YT="+data.yourtext);
	    loadTextToElement1(data.yourtext);
	} else if (data.css) {
	    let csslink = document.createElement("link");
	    csslink.setAttribute("rel", "stylesheet");
	    csslink.setAttribute("href", data.css);
	    console.log(csslink);
	    document.querySelector("head").appendChild(csslink);
	} else {
	    console.log("message: "+data.message);
	}
    }
    function ranking(ev) {
	var mode = null;
	switch (ev.target.id) {
	case "ranking": mode = null; break;
	default: mode = ev.target.id.replace("ranking:", ""); break;
	}
	mystate["rankmode"] = mode;
	var div = document.createElement("div");
	div.classList.add("login2");
	setTimeout(()=>{div.setAttribute("class", "ranking")}, 1000);
	var p1 = document.createElement("p");
	p1.className = "c t";
	p1.innerHTML = "<button>Back to stage</button><p>"+
	    (mode||"Total")+"</p>";
	var p2 = document.createElement("p");
	p2.className = "c t";
	var b2 = document.createElement("button");
	b2.innerText = "Download CSV";
	b2.addEventListener("click", downloadCSV, false);
	p2.appendChild(b2);
	var d2 = document.createElement("div");
	d2.classList.add("tbl");
	var tbl = document.createElement("table");
	tbl.id = "ranking-table";
	tbl.classList.add("ranking");
	div.appendChild(p1);
	div.appendChild(d2);
	div.appendChild(p2);
	d2.appendChild(tbl);
	document.body.appendChild(div);
	if (history.pushState) {
	    let h = location.href.replace(/#.*/, '')+"#ranking";
	    history.pushState({url: h}, null, h);
	    window.addEventListener("popstate", (e) => {
		if (div) {
		    div.remove(); div = null;
		}
		if (mystate.start && mystate.start > 0) {
		    clearText();
		    resetState();
		}
		_entryFocus();
	    }, false);
	}
	let text = textlist[nArea].file;
	mystate.ranklist = null; // Clear ranking list
	let request = {ranking: text};
	if (mode) request["mode"] = mode;
	sendJSONtoServer(request);
	div.addEventListener("click", ()=>{history.back();}, false);
	div.addEventListener("keydown", ()=>{history.back();}, false);
    }
    function resetState() {
	mystate.start = mystate.miss = mystate.finish = 0;
	for (id of ["miss", "ratio", "types", "time", "score"]) {
	    setElementIDText(id, "");
	}
	if (area[nArea] && area[nArea].textContent
	    && area[nArea].textContent > "")
	    setElementIDText("comment", "自主練は BS で開始よ。");
	else
	    setElementIDText("comment", "まずTEXTを選択するところからよ。");
	setElementIDText("info", "Ready...");
    }
    function tryLogin() {
	let skey = localStorage.getItem("trrSkey");
	// alert(skey);
	if (skey && skey > "") {
	    let user = mystate.user||localStorage.getItem("trrUser");
	    console.log("Sending"+skey);
	    sendJSONtoServer({"user": user, "skey": skey});
	}
    }
    function _reset(e) {
	localStorage.clear();
	mystate.user = "";
	tmpkey = null;
    }
    function initLogin() {
	function loginProcedure(json) {
	    
	}
	function _send(e) {
	    if (prompt && prompt.innerText > "" && input && input.value > "") {
		let user = mystate.user||localStorage.getItem("trrUser"),
		    skey = localStorage.getItem("trrSkey");
		let j = {}
		j[prompt.innerText] = input.value;
		if (user && user>"") j.user       = user;
		if (tmpkey && tmpkey>"") j.tmpkey = tmpkey;
		input.value = ""
		
		sendJSONtoServer(j);
		input.disabled = true;
	    }
	};
	tryLogin();
	input.addEventListener(
	    "keypress", (e) => {if (e.key=='Enter') _send(e);});
	let login = document.getElementById("login");
	if (login) {
	    login.addEventListener("click", _send);
	    document.getElementById("reset").addEventListener("click", _reset);
	}
    }
    function logout() {
	_reset();
	location.reload();
    }
    function trrmode(ev) {
	document.body.classList.remove(mystate["trrmode"]);
	document.body.classList.add(mystate["trrmode"] = ev.target.value);
	_entryFocus();
    }
    function setupTrrMode() {
	var modesel = document.getElementById("trrmode");
	modesel.addEventListener("change", trrmode);
	modesel.options[0].selected = true;
    }
    var wsInitRetry = 10;
    function wsInit() {
	loginhead.textContent = "Connecting Server....."+wsInitRetry;
	input.disabled = true;		// Start with input prompt disabled
	ws = new WebSocket(wsURL);
	ws.onerror = (e) => {
	    console.log("Oh...");
	    if (--wsInitRetry > 0)
		setTimeout(wsInit, 5000);
	};
	infoBox.removeEventListener("click", wsInit);
	var typing = document.getElementById("typing");
	ws.onopen = function () {
	    console.log("WS-OK");
	    input.disabled = false;
	    input.focus();
	    infoBox.classList.remove("warn");
	    loginhead.textContent = "jsTRR";
	    initLogin();
	};
	ws.onmessage = getServerMessage;
	ws.onclose = function () {
	    setElementIDText("info", "No Server Connection.  Click to reconnect");
	    infoBox.classList.add("warn");
	    infoBox.addEventListener("click", wsInit);
	};
    }
    function _entryFocus() {
	if (entry) entry.focus();
    }
    function init() {
	let btn;
	prompt = document.getElementById("prompt");
	input = document.getElementById("inputvalue");
	console.log("prom="+prompt.innerText+", val="+input.value);
	tmsel = document.querySelector("select[name=team]");
	nmsel = document.querySelector("select[name=name]");
	txsel = document.getElementById("text");
	loginhead = document.getElementById("loginhead");
	for (let btn of document.querySelectorAll("button")) {
	    if (btn.id.match(/^ranking/))
		btn.addEventListener("click", ranking);
	}
	let entry = document.createElement("input");
	entry.id = "entry"; entry.type = "text";
	document.body.appendChild(entry);
	infoBox = entry.insertAdjacentHTML(
	    "afterend", '<p class="info" id="info"><p>');
	infoBox = document.getElementById("info");
	document.getElementById("logout").addEventListener("click", logout);
	setupTrrMode()
	wsInit();
    }
    document.addEventListener("DOMContentLoaded", init, false);
    //loadTeamList("teams.csv");
})();