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