blob: 25e1d97659fd22b98e6d9f7cb997bcb1add78444 [file] [log] [blame]
Nils Diewald86dad5b2015-01-28 15:09:07 +00001/**
Akroncd42a142019-07-12 18:55:37 +02002 * Create virtual corpora with a visual user interface. This resembles the
3 * corpus/collection type objects of a KoralQuery "collection"/"corpus" object.
hebastaa79d69d2018-07-24 12:13:02 +02004 *
Nils Diewald4c221252015-04-21 20:19:25 +00005 * KoralQuery v0.3 is expected.
hebastaa79d69d2018-07-24 12:13:02 +02006 *
Nils Diewald86dad5b2015-01-28 15:09:07 +00007 * @author Nils Diewald
8 */
Nils Diewald2fe12e12015-03-06 16:47:06 +00009/*
Nils Diewald1fcb2ad2015-04-20 19:19:18 +000010 * This replaces a previous version written by Mengfei Zhou
Nils Diewald2fe12e12015-03-06 16:47:06 +000011 */
Nils Diewald2fe12e12015-03-06 16:47:06 +000012
Nils Diewaldd0770492014-12-19 03:55:00 +000013/*
hebasta86759392018-07-25 15:44:37 +020014 TODO: Disable "and" or "or" in case it's followed
15 by an unspecified document
16 TODO: Add "and"-method to root to add further constraints
17 based on match-input (like clicking on a pubDate timestamp in a match)
18 TODO: Implement "persistence"-Option, injecting the current creation
19 date stamp
20 TODO: Implement vec-Type for document-id vectors like docID in [1,2,3,4 ...]
21
22 Error codes:
23 701: "JSON-LD group has no @type attribute"
24 704: "Operation needs operand list"
25 802: "Match type is not supported by value type"
26 804: "Unknown value type"
27 805: "Value is invalid"
28 806: "Value is not a valid date string"
29 807: "Value is not a valid regular expression"
30 810: "Unknown document group operation" (like 711)
31 811: "Document group expects operation" (like 703)
32 812: "Operand not supported in document group" (like 744)
33 813: "Collection type is not supported" (like 713)
34 814: "Unknown rewrite operation"
35 815: "Rewrite expects source"
36
37 Localization strings:
38 KorAP.Locale = {
39 EMPTY : '...',
40 AND : 'and',
41 OR : 'or',
42 DELETE : 'x' }
43
44 and various field names with the prefix 'VC_'
hebastaa79d69d2018-07-24 12:13:02 +020045 */
Akron88d237e2020-10-21 08:05:18 +020046"use strict";
Nils Diewald86dad5b2015-01-28 15:09:07 +000047
Akronb19803c2018-08-16 16:39:42 +020048define([
49 'vc/unspecified',
50 'vc/doc',
51 'vc/docgroup',
52 'vc/docgroupref',
53 'vc/menu',
54 'vc/statistic',
55 'datepicker',
56 'buttongroup',
Akronaa613222019-11-19 13:57:12 +010057 'panel/vc',
58 'view/vc/corpstatv',
Akronec6bb8e2018-08-29 13:07:56 +020059 'buttongroup',
Akronb19803c2018-08-16 16:39:42 +020060 'util'
61], function(
62 unspecDocClass,
63 docClass,
64 docGroupClass,
65 docGroupRefClass,
66 menuClass,
67 statClass,
68 dpClass,
69 buttonGrClass,
hebasta2535c762018-11-21 16:27:33 +010070 vcPanelClass,
Akronec6bb8e2018-08-29 13:07:56 +020071 corpStatVClass,
72 buttonGroupClass) {
Nils Diewald1fcb2ad2015-04-20 19:19:18 +000073
Akronb19803c2018-08-16 16:39:42 +020074 KorAP._validUnspecMatchRE = new RegExp(
75 "^(?:eq|ne|contains(?:not)?|excludes)$");
76 KorAP._validStringMatchRE = new RegExp("^(?:eq|ne)$");
Marc Kupietza5f79e62024-12-17 07:15:46 +010077 KorAP._validIntegerMatchRE = new RegExp("^(?:[gl]?eq|ne|[gl]t)$");
Akronb19803c2018-08-16 16:39:42 +020078 KorAP._validTextMatchRE = KorAP._validUnspecMatchRE;
79 KorAP._validTextOnlyMatchRE = new RegExp(
80 "^(?:contains(?:not)?|excludes)$");
81 KorAP._overrideStyles = false;
82 // KorAP._validDateMatchRE is defined in datepicker.js!
Nils Diewaldd0770492014-12-19 03:55:00 +000083
Akronb19803c2018-08-16 16:39:42 +020084 const loc = KorAP.Locale;
hebastaa0282be2018-12-05 16:58:00 +010085 loc.SHOW_STAT = loc.SHOW_STAT || 'Statistics';
86 loc.VERB_SHOWSTAT = loc.VERB_SHOWSTAT || 'Corpus Statistics';
Akron8a670162018-08-28 10:09:13 +020087 loc.VC_allCorpora = loc.VC_allCorpora || 'all corpora';
88 loc.VC_oneCollection = loc.VC_oneCollection || 'a virtual corpus';
Akronec6bb8e2018-08-29 13:07:56 +020089 loc.MINIMIZE = loc.MINIMIZE || 'Minimize';
Nils Diewald3a2d8022014-12-16 02:45:41 +000090
Akronb19803c2018-08-16 16:39:42 +020091 KorAP._vcKeyMenu = undefined;
92 KorAP._vcDatePicker = dpClass.create();
Nils Diewald3a2d8022014-12-16 02:45:41 +000093
Akronb19803c2018-08-16 16:39:42 +020094 // Create match menus ....
95 KorAP._vcMatchopMenu = {
96 'string' : menuClass.create([
97 [ 'eq', null ],
98 [ 'ne', null ]
99 ]),
100 'text' : menuClass.create([
101 [ 'eq', null ], // Requires exact match
102 [ 'ne', null ],
103 [ 'contains', null ], // Requires token sequence match
104 [ 'containsnot', null ]
105 ]),
106 'date' : menuClass.create([
107 [ 'eq', null ],
108 [ 'ne', null ],
109 [ 'geq', null ],
110 [ 'leq', null ]
111 ]),
112 'regex' : menuClass.create([
113 [ 'eq', null ],
114 [ 'ne', null ]
Akron5d4f2e42024-12-16 09:10:27 +0100115 ]),
116 'integer' : menuClass.create([
117 [ 'eq', null ],
118 [ 'ne', null ],
119 [ 'geq', null ],
Marc Kupietza5f79e62024-12-17 07:15:46 +0100120 [ 'leq', null ],
121 [ 'gt', null ],
122 [ 'lt', null ]
Akronb19803c2018-08-16 16:39:42 +0200123 ])
124 };
125
Akron88d237e2020-10-21 08:05:18 +0200126
Akronb19803c2018-08-16 16:39:42 +0200127 /**
Akroncd42a142019-07-12 18:55:37 +0200128 * Virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200129 */
130 return {
131
132 /**
Akroncd42a142019-07-12 18:55:37 +0200133 * The JSON-LD type of the virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200134 */
135 ldType : function() {
136 return null;
137 },
138
Akron88d237e2020-10-21 08:05:18 +0200139
Akroncd42a142019-07-12 18:55:37 +0200140 // Initialize virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200141 _init : function(keyList) {
142
143 // Inject localized css styles
144 if (!KorAP._overrideStyles) {
Akron88d237e2020-10-21 08:05:18 +0200145
146 const sheet = KorAP.newStyleSheet();
Akronb19803c2018-08-16 16:39:42 +0200147
148 // Add css rule for OR operations
149 sheet.insertRule('.vc .docGroup[data-operation=or] > .doc::before,'
150 + '.vc .docGroup[data-operation=or] > .docGroup::before '
151 + '{ content: "' + loc.OR + '" }', 0);
152
153 // Add css rule for AND operations
154 sheet.insertRule(
155 '.vc .docGroup[data-operation=and] > .doc::before,'
156 + '.vc .docGroup[data-operation=and] > .docGroup::before '
157 + '{ content: "' + loc.AND + '" }', 1);
158
159 KorAP._overrideStyles = true;
Nils Diewald359a72c2015-04-20 17:40:29 +0000160 };
161
Akron88d237e2020-10-21 08:05:18 +0200162 let l;
Akron3ad46942018-08-22 16:47:14 +0200163 if (keyList) {
Akronadab5e52018-08-20 13:50:53 +0200164 l = keyList.slice();
Akron3ad46942018-08-22 16:47:14 +0200165 l.unshift(['referTo', 'ref']);
166 }
167 else {
168 l = [['referTo', 'ref']];
169 }
Akronadab5e52018-08-20 13:50:53 +0200170
Akronb19803c2018-08-16 16:39:42 +0200171 // Create key menu
Akron3ad46942018-08-22 16:47:14 +0200172 KorAP._vcKeyMenu = menuClass.create(l);
Akronb19803c2018-08-16 16:39:42 +0200173 KorAP._vcKeyMenu.limit(6);
Akron712733a2018-04-05 18:17:47 +0200174
Akronb19803c2018-08-16 16:39:42 +0200175 return this;
176 },
Nils Diewald359a72c2015-04-20 17:40:29 +0000177
Akron88d237e2020-10-21 08:05:18 +0200178
Akronb19803c2018-08-16 16:39:42 +0200179 /**
Akroncd42a142019-07-12 18:55:37 +0200180 * Create a new virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200181 */
Akron88d237e2020-10-21 08:05:18 +0200182 create : function (keyList) {
183 const obj = Object.create(this)._init(keyList);
Akronb19803c2018-08-16 16:39:42 +0200184 obj._root = unspecDocClass.create(obj);
185 return obj;
186 },
Nils Diewaldd599d542015-01-08 20:41:34 +0000187
Akron8a670162018-08-28 10:09:13 +0200188
Akronb19803c2018-08-16 16:39:42 +0200189 /**
Akroncd42a142019-07-12 18:55:37 +0200190 * Create and render a new virtual corpus based on a KoralQuery
191 * corpus document
Akronb19803c2018-08-16 16:39:42 +0200192 */
193 fromJson : function(json) {
Akron13af2f42019-07-25 15:06:21 +0200194
195 let obj;
196
Akronb19803c2018-08-16 16:39:42 +0200197 if (json !== undefined) {
Akron88d237e2020-10-21 08:05:18 +0200198
Akronb19803c2018-08-16 16:39:42 +0200199 // Parse root document
200 if (json['@type'] == 'koral:doc') {
Akron13af2f42019-07-25 15:06:21 +0200201 obj = docClass.create(this, json);
hebastaa79d69d2018-07-24 12:13:02 +0200202 }
Akron88d237e2020-10-21 08:05:18 +0200203
Akronb19803c2018-08-16 16:39:42 +0200204 // parse root group
205 else if (json['@type'] == 'koral:docGroup') {
Akron13af2f42019-07-25 15:06:21 +0200206 obj = docGroupClass.create(this, json);
Akronb19803c2018-08-16 16:39:42 +0200207 }
208
209 // parse root reference
210 else if (json['@type'] == 'koral:docGroupRef') {
Akron13af2f42019-07-25 15:06:21 +0200211 obj = docGroupRefClass.create(this, json);
Akronb19803c2018-08-16 16:39:42 +0200212 }
213
214 // Unknown collection type
215 else {
216 KorAP.log(813, "Collection type is not supported");
217 return;
218 };
219 }
220
221 else {
222 // Add unspecified object
Akron13af2f42019-07-25 15:06:21 +0200223 obj = unspecDocClass.create(this);
Nils Diewald845282c2015-05-14 07:53:03 +0000224 };
Akronb19803c2018-08-16 16:39:42 +0200225
226 // Init element and update
Akron13af2f42019-07-25 15:06:21 +0200227 this.root(obj);
228
Akronb19803c2018-08-16 16:39:42 +0200229 return this;
230 },
Akrond2474aa2018-08-28 12:06:27 +0200231
232
233 // Check if the virtual corpus contains a rerite
234 wasRewritten : function (obj) {
235
Akron88d237e2020-10-21 08:05:18 +0200236 if (arguments.length !== 1) {
Akrond2474aa2018-08-28 12:06:27 +0200237 obj = this._root;
238 };
239
240 // Check for rewrite
241 if (obj.rewrites() && obj.rewrites().length() > 0) {
242 return true;
243 }
244
245 // Check recursively
246 else if (obj.ldType() === 'docGroup') {
Akron678c26f2020-10-09 08:52:50 +0200247
248 // If there was a rewritten object
249 if (obj.operands().find(op => this.wasRewritten(op)) !== undefined) {
250 return true;
Akrond2474aa2018-08-28 12:06:27 +0200251 };
252 };
Akron88d237e2020-10-21 08:05:18 +0200253
Akrond2474aa2018-08-28 12:06:27 +0200254 return false;
255 },
Akron43c5cc62018-08-28 13:10:25 +0200256
Akron8a670162018-08-28 10:09:13 +0200257
Akronb19803c2018-08-16 16:39:42 +0200258 /**
hebasta3f4be922018-12-11 10:41:46 +0100259 * Clean the virtual document to unspecified doc.
Akronb19803c2018-08-16 16:39:42 +0200260 */
261 clean : function() {
Akron88d237e2020-10-21 08:05:18 +0200262 const t = this;
263 if (t._root.ldType() !== "non") {
264 t._root.destroy();
265 t.root(unspecDocClass.create(t));
Akronb19803c2018-08-16 16:39:42 +0200266 };
Akron88d237e2020-10-21 08:05:18 +0200267
268 // update for graying corpus statistic by deleting the first line of the vc builder
269 t.update();
270 return t;
Akronb19803c2018-08-16 16:39:42 +0200271 },
272
Akron88d237e2020-10-21 08:05:18 +0200273
Akronb19803c2018-08-16 16:39:42 +0200274 /**
Akroncd42a142019-07-12 18:55:37 +0200275 * Get or set the root object of the virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200276 */
277 root : function(obj) {
278 if (arguments.length === 1) {
Akron88d237e2020-10-21 08:05:18 +0200279 const e = this.builder();
280
Akronb19803c2018-08-16 16:39:42 +0200281 if (e.firstChild !== null) {
Akronadab5e52018-08-20 13:50:53 +0200282
283 // Object not yet set
Akronb19803c2018-08-16 16:39:42 +0200284 if (e.firstChild !== obj.element()) {
285 e.replaceChild(obj.element(), e.firstChild);
286 };
287 }
288
289 // Append root element
290 else {
291 e.appendChild(obj.element());
292 };
293
294 // Update parent child relations
295 this._root = obj;
296 obj.parent(this);
297
298 this.update();
299 };
Akron88d237e2020-10-21 08:05:18 +0200300
Akronb19803c2018-08-16 16:39:42 +0200301 return this._root;
302 },
303
Akronadab5e52018-08-20 13:50:53 +0200304
305 /**
306 * Get the wrapper element associated with the vc
307 */
308 builder : function () {
Akron88d237e2020-10-21 08:05:18 +0200309 const t = this;
Akronadab5e52018-08-20 13:50:53 +0200310
311 // Initialize if necessary
Akron88d237e2020-10-21 08:05:18 +0200312 if (t._builder !== undefined)
313 return t._builder;
Akronadab5e52018-08-20 13:50:53 +0200314
Akron88d237e2020-10-21 08:05:18 +0200315 t.element();
316 return t._builder;
Akronadab5e52018-08-20 13:50:53 +0200317 },
318
Akron88d237e2020-10-21 08:05:18 +0200319
Akronb19803c2018-08-16 16:39:42 +0200320 /**
Akron68d28322018-08-27 15:02:42 +0200321 * Get the element associated with the virtual corpus
Akronb19803c2018-08-16 16:39:42 +0200322 */
323 element : function() {
Akron88d237e2020-10-21 08:05:18 +0200324 const t = this;
Akron24aa0052020-11-10 11:00:34 +0100325 let e = t._el;
Akronb19803c2018-08-16 16:39:42 +0200326
Akron88d237e2020-10-21 08:05:18 +0200327 if (e !== undefined)
328 return e;
Akronec6bb8e2018-08-29 13:07:56 +0200329
Akronb19803c2018-08-16 16:39:42 +0200330
Akron24aa0052020-11-10 11:00:34 +0100331 e = t._el = document.createElement('div');
Akron88d237e2020-10-21 08:05:18 +0200332 e.classList.add('vc');
Akronadab5e52018-08-20 13:50:53 +0200333
Akron88d237e2020-10-21 08:05:18 +0200334
335 t._builder = e.addE('div');
336 t._builder.setAttribute('class', 'builder');
337
338 const btn = buttonGroupClass.create(
Akronec6bb8e2018-08-29 13:07:56 +0200339 ['action','button-view']
340 );
Akronec6bb8e2018-08-29 13:07:56 +0200341
Akron88d237e2020-10-21 08:05:18 +0200342 btn.add(loc.MINIMIZE, {'cls':['button-icon','minimize']}, function () {
343 this.minimize();
344 }.bind(t));
345
346 e.appendChild(btn.element());
347
Akronb19803c2018-08-16 16:39:42 +0200348 // Initialize root
Akron88d237e2020-10-21 08:05:18 +0200349 t._builder.appendChild(t._root.element());
Akronb19803c2018-08-16 16:39:42 +0200350
351 // Add panel to display corpus statistic, ...
Akron88d237e2020-10-21 08:05:18 +0200352 t.addVcInfPanel();
Akronb19803c2018-08-16 16:39:42 +0200353
hebasta4dd77bc2019-02-07 12:57:57 +0100354 //Adds EventListener for corpus changes
Akron24aa0052020-11-10 11:00:34 +0100355 t._el.addEventListener('vcChange', function (e) {
Akron88d237e2020-10-21 08:05:18 +0200356 this.checkStatActive(e.detail);
357 }.bind(t), false);
hebasta4dd77bc2019-02-07 12:57:57 +0100358
Akron88d237e2020-10-21 08:05:18 +0200359 return e;
Akronb19803c2018-08-16 16:39:42 +0200360 },
361
Akronec6bb8e2018-08-29 13:07:56 +0200362
363 /**
364 * Check, if the VC is open
365 */
366 isOpen : function () {
Akron24aa0052020-11-10 11:00:34 +0100367 if (!this._el)
Akronec6bb8e2018-08-29 13:07:56 +0200368 return false;
Akron24aa0052020-11-10 11:00:34 +0100369 return this._el.classList.contains('active');
Akronec6bb8e2018-08-29 13:07:56 +0200370 },
371
Akron88d237e2020-10-21 08:05:18 +0200372
Akronec6bb8e2018-08-29 13:07:56 +0200373 /**
374 * Open the VC view
375 */
376 open : function () {
377 this.element().classList.add('active');
378 if (this.onOpen)
379 this.onOpen();
380 },
381
382
383 /**
384 * Minimize the VC view
385 */
386 minimize : function () {
387 this.element().classList.remove('active');
388 if (this.onMinimize)
389 this.onMinimize();
390 },
391
392
Akronb19803c2018-08-16 16:39:42 +0200393 /**
394 * Update the whole object based on the underlying data structure
hebastaa0282be2018-12-05 16:58:00 +0100395 */
Akronb19803c2018-08-16 16:39:42 +0200396 update : function() {
397 this._root.update();
Akron88d237e2020-10-21 08:05:18 +0200398 if (KorAP.vc) {
399 this.element().dispatchEvent(
400 new CustomEvent('vcChange', {'detail':this})
401 );
hebasta4dd77bc2019-02-07 12:57:57 +0100402 };
Akronb19803c2018-08-16 16:39:42 +0200403 return this;
404 },
Akron88d237e2020-10-21 08:05:18 +0200405
406
Akronb19803c2018-08-16 16:39:42 +0200407 /**
408 * Make the vc persistant by injecting the current timestamp as a
409 * creation date limit criterion.
410 * THIS IS CURRENTLY NOT USED
411 */
Akron88d237e2020-10-21 08:05:18 +0200412 /*
Akronb19803c2018-08-16 16:39:42 +0200413 makePersistant : function() {
414 // this.root().wrapOnRoot('and');
415 var todayStr = KorAP._vcDatePicker.today();
416 var doc = docClass.create();
417 var root = this.root();
418
419 if (root.ldType() === 'docGroup' && root.operation === 'and') {
420 root.append(cond);
421 } else {
422 root.wrapOnRoot('and');
423 root.append(doc);
424 };
425
426 doc.key("creationDate");
427 doc.type("date");
428 doc.matchop("leq");
429 doc.value(todayStr);
430
Akron88d237e2020-10-21 08:05:18 +0200431 // { "@type" : "koral:doc", "key" : "creationDate", "type" :
432 // "type:date", "match" : "match:leq", "value" : todayStr }
433 // this.root().append(cond);
Akronb19803c2018-08-16 16:39:42 +0200434 this.update();
435 },
Akron88d237e2020-10-21 08:05:18 +0200436 */
Akronb19803c2018-08-16 16:39:42 +0200437
Akron8a670162018-08-28 10:09:13 +0200438
439 // Get the reference name
440 getName : function () {
441 if (this._root.ldType() === 'non') {
442 return loc.VC_allCorpora;
443 }
444 else if (this._root.ldType() === 'docGroupRef') {
445 return this._root.ref();
446 }
447 else {
448 return loc.VC_oneCollection;
449 }
450 },
451
Akron88d237e2020-10-21 08:05:18 +0200452
Akron4feec9d2018-11-20 17:00:50 +0100453 // Add "and" constraint to VC
454 addRequired : function (doc) {
Akron88d237e2020-10-21 08:05:18 +0200455 const root = this.root();
456 const ldType = root.ldType();
457 const parent = root.parent();
Akron4feec9d2018-11-20 17:00:50 +0100458
Akron4feec9d2018-11-20 17:00:50 +0100459 if (ldType === 'non') {
460 parent.root(doc);
461 }
462
463 // root is doc
464 else if (
465 ldType === 'doc' ||
466 ldType === 'docGroupRef' ||
467 (ldType === 'docGroup' &&
468 root.operation() === 'or'
469 )) {
Akron88d237e2020-10-21 08:05:18 +0200470 const group = require('vc/docgroup').create(
Akron4feec9d2018-11-20 17:00:50 +0100471 parent
472 );
473 group.operation("and");
474 group.append(root);
475 group.append(doc);
476 group.element(); // Init (seems to be necessary)
477 parent.root(group);
478 }
479
480 // root is a docGroup
481 // and is already an 'and'-Group
482 else if (ldType === 'docGroup') {
483 root.append(doc);
484 }
485
486 // Unknown
487 else {
488 console.log("Unknown root object");
489 };
490
491 // Init element and update
492 this.update();
493 },
Akron8a670162018-08-28 10:09:13 +0200494
Akron88d237e2020-10-21 08:05:18 +0200495
Akronb19803c2018-08-16 16:39:42 +0200496 /**
497 * Get the generated json string
498 */
Akron88d237e2020-10-21 08:05:18 +0200499 toJson : function () {
Akronb19803c2018-08-16 16:39:42 +0200500 return this._root.toJson();
501 },
502
Akron88d237e2020-10-21 08:05:18 +0200503
Akronb19803c2018-08-16 16:39:42 +0200504 /**
505 * Get the generated query string
506 */
Akron88d237e2020-10-21 08:05:18 +0200507 toQuery : function () {
Akronb19803c2018-08-16 16:39:42 +0200508 return this._root.toQuery();
509 },
510
hebasta2535c762018-11-21 16:27:33 +0100511
512 /**
Akronb19803c2018-08-16 16:39:42 +0200513 * Add panel to display virtual corpus information
514 */
Akron88d237e2020-10-21 08:05:18 +0200515 addVcInfPanel : function () {
516 // Create panel
hebasta2535c762018-11-21 16:27:33 +0100517 this.panel = vcPanelClass.create(this);
Akron24aa0052020-11-10 11:00:34 +0100518 this._el.addE('div').appendChild(this.panel.element());
hebastaa0282be2018-12-05 16:58:00 +0100519
hebasta2535c762018-11-21 16:27:33 +0100520 },
521
hebastaa0282be2018-12-05 16:58:00 +0100522 /**
hebasta48842cf2018-12-11 12:57:38 +0100523 * Checks if corpus statistic has to be disabled,
hebastaa0282be2018-12-05 16:58:00 +0100524 * and to be updated after clicking at the "reload-button"
525 */
Akron88d237e2020-10-21 08:05:18 +0200526 checkStatActive : function (){
527 if (this.panel !== undefined && this.panel.statView !== undefined){
hebasta48842cf2018-12-11 12:57:38 +0100528 this.panel.statView.checkStatActive();
Akron88d237e2020-10-21 08:05:18 +0200529 };
hebastaa0282be2018-12-05 16:58:00 +0100530 }
Akronb19803c2018-08-16 16:39:42 +0200531 };
532});