dce5f7a43ace9c973c726432f70d651112cbea69
[sheet.git] / word / quiz.js
1 Array.prototype.shuffle = function () {
2         for (let i = this.length - 1; i > 0; i--) {
3                 const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
4                 [this[i], this[j]] = [this[j], this[i]]; // swap elements
5         }
6         return this;
7 };
8
9 function hashparams() {
10         // location.hash is not encoded in firefox
11         const encodedhash = (window.location.href.split('#'))[1] || '';
12         return decodeURIComponent(encodedhash).split('#');
13 }
14
15 class Words {
16         constructor(data, root = undefined) {
17                 this.data = data;
18                 this.selection = root || this.data[''][3];
19                 this.visible = new Set(root || Object.keys(data).flatMap(id => id && parseInt(id)));
20                 if (root) {
21                         let children = root;
22                         for (let loop = 0; children.length && loop < 20; loop++) {
23                                 for (let child of children) this.visible.add(child);
24                                 children = children.map(cat => data[cat][3]).filter(is => is).flat();
25                         }
26                 }
27         }
28
29         filter(f) {
30                 // keep only matching entries, and root selection regardless
31                 this.visible = new Set([...this.visible].filter(f).concat(this.selection));
32         }
33
34         *root() {
35                 for (let i of this.selection) {
36                         if (!this.has(i)) {
37                                 continue;
38                         }
39                         yield this.get(i);
40                 }
41         }
42
43         *random() {
44                 let order = [...this.visible.keys()].shuffle();
45                 for (let i of order) {
46                         if (!this.has(i)) {
47                                 continue;
48                         }
49                         yield this.get(i);
50                 }
51         }
52
53         has(id) {
54                 return this.visible.has(id);
55         }
56
57         subs(id) {
58                 let refs = this.data[id][3];
59                 if (!refs) {
60                         return [];
61                 }
62                 for (let ref of refs) {
63                         // retain orphaned references in grandparent categories
64                         if (!this.has(ref)) {
65                                 refs = refs.concat(this.subs(ref));
66                         }
67                 }
68                 return refs;
69         }
70
71         get(id) {
72                 if (!this.has(id)) {
73                         return;
74                 }
75                 const p = this;
76                 const row = this.data[id];
77                 return row && {
78                         id: id,
79                         title: row[0],
80                         get label() {
81                                 return row[0].replace(/\/.*/, ''); // primary form
82                         },
83                         level: row[1],
84                         imgid: row[2],
85                         thumb(size = 32) {
86                                 return `/data/word/${size}/${row[2]}.jpg`;
87                         },
88                         get subs() {
89                                 return p.subs(id).map(e => p.get(e));
90                         },
91                 };
92         }
93 }
94
95 class WordQuiz {
96         dataselect(json) {
97                 this.data = this.datafilter(json);
98                 return [...this.data.random()];
99         }
100
101         datafilter(json) {
102                 // find viable rows from json data
103                 const selection = new Words(json, this.preset.cat);
104
105                 if (this.preset.images) {
106                         selection.filter(id => json[id][2]);
107                 }
108                 if (this.preset.level !== undefined) {
109                         selection.filter(id => json[id][1] <= this.preset.level);
110                 }
111
112                 if (this.preset.distinct) {
113                         // remove referenced categories
114                         selection.filter(id => !selection.get(id).subs.length);
115                 }
116
117                 return selection;
118         }
119
120         configure(params = hashparams()) {
121                 const opts = new Map(params.map(arg => arg.split(/[:=](.*)/)));
122                 for (let [query, val] of opts) {
123                         if (query.match(/^\d+$/)) {
124                                 this.preset.cat = [parseInt(query)];
125                         }
126                         else if (query === 'level') {
127                                 this.preset.level = parseInt(val);
128                         }
129                         else {
130                                 this.preset[query] = val;
131                         }
132                 }
133                 this.preset.dataurl = `/data/wordlist.${this.preset.lang}.json`
134         }
135
136         setup() {
137                 this.form = document.getElementById('quiz');
138         }
139
140         load() {
141                 this.configure();
142                 fetch(this.preset.dataurl).then(res => res.json()).then(json => {
143                         this.words = this.dataselect(json)
144                         this.setup();
145                 });
146         }
147
148         log(...args) {
149                 this.history.push([new Date().toISOString(), ...args]);
150         }
151
152         stop(...args) {
153                 this.log(...args);
154                 window.onbeforeunload = null;
155                 fetch('/word/report', {method: 'POST', body: JSON.stringify(this.history)});
156         }
157
158         constructor() {
159                 this.preset = {images: true, lang: 'en'};
160                 this.load();
161                 this.history = [];
162                 window.onbeforeunload = e => {
163                         this.stop('abort');
164                 };
165                 window.onhashchange = e => {
166                         this.load();
167                 };
168         }
169 }