@noe-teritorio/opening_hours 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,735 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2014 Robin Schneider <ypid@riseup.net>
3
+ *
4
+ * SPDX-License-Identifier: LGPL-3.0-only
5
+ */
6
+ // Import dependencies
7
+ import i18next from '../../node_modules/i18next/dist/esm/i18next.js';
8
+ import { OpeningHoursTable } from './opening_hours_table.js';
9
+ import { mapCountryToLanguage } from './countryToLanguageMapping.js';
10
+ import { updateTimeButtonLabels } from './main.js';
11
+ import { YoHoursChecker } from './yohours_model.js';
12
+
13
+ // Access global variables set by main.js or UMD scripts
14
+ const { opening_hours, default_lat, default_lon } = window;
15
+
16
+ // Export date/time state
17
+ export let currentDateTime = {
18
+ year: 2013,
19
+ month: 0, // January (0-indexed)
20
+ day: 2,
21
+ hour: 22,
22
+ minute: 21
23
+ };
24
+
25
+ /* Constants {{{ */
26
+ const nominatim_api_url = 'https://nominatim.openstreetmap.org/reverse';
27
+ // let nominatim_api_url = 'https://open.mapquestapi.com/nominatim/v1/reverse.php';
28
+
29
+ const evaluation_tool_colors = {
30
+ 'ok': '#ADFF2F',
31
+ 'warn': '#FFA500',
32
+ 'error': '#DEB887',
33
+ };
34
+
35
+ const OSM_MAX_VALUE_LENGTH = 255;
36
+ /* }}} */
37
+
38
+ /**
39
+ * Load nominatim_data in JOSM using the JOSM remote control API. {{{
40
+ * @param {string} url_param - Query parameter sent to the JOSM remote control API.
41
+ * @returns {void}
42
+ */
43
+ export function josm(url_param) {
44
+ fetch(`http://localhost:8111/${url_param}`)
45
+ .then(response => {
46
+ if (!response.ok) {
47
+ alert(i18next.t('texts.JOSM remote conn error'));
48
+ }
49
+ })
50
+ .catch(() => {
51
+ alert(i18next.t('texts.JOSM remote conn error'));
52
+ });
53
+ }
54
+ // }}}
55
+
56
+ /**
57
+ * ISO 8601 calendar week number. {{{
58
+ * @param {Date} date - Date for which to calculate the week number.
59
+ * @returns {number} ISO calendar week number.
60
+ */
61
+ export function getISOWeekNumber(date) {
62
+ const millisecondsPerDay = 24 * 60 * 60 * 1000;
63
+ const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
64
+
65
+ // ISO week uses Monday=1..Sunday=7 and anchors weeks on Thursday.
66
+ const isoDay = utcDate.getUTCDay() || 7;
67
+ utcDate.setUTCDate(utcDate.getUTCDate() + 4 - isoDay);
68
+
69
+ const isoYearStart = new Date(Date.UTC(utcDate.getUTCFullYear(), 0, 1));
70
+ const dayOfYear = Math.floor((utcDate - isoYearStart) / millisecondsPerDay) + 1;
71
+ return Math.ceil(dayOfYear / 7);
72
+ }
73
+ // }}}
74
+
75
+ /**
76
+ * Reverse geocode coordinates to get localized place names from Nominatim.
77
+ *
78
+ * The names of countries and states are localized in OSM and opening_hours.js
79
+ * (holidays) so we need to get the localized names from Nominatim as well.
80
+ * @param {number} lat - Latitude
81
+ * @param {number} lon - Longitude
82
+ * @param {string} preferredLanguage - Preferred language code (e.g., 'de', 'en')
83
+ * @returns {Promise<object>} Nominatim response with address data
84
+ */
85
+ async function reverseGeocodeLocation(lat, lon, preferredLanguage) {
86
+ // Cached response for default coordinates to avoid queries on initial load
87
+ if (lat === 48.7769 && lon === 9.1844) {
88
+ return { place_id: '159221147', licence: 'Data © OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright', osm_type: 'relation', osm_id: '62611', lat: '48.6296972', lon: '9.1949534', display_name: 'Baden-Württemberg, Deutschland', address: { state: 'Baden-Württemberg', country: 'Deutschland', country_code: 'de' }, boundingbox: ['47.5324787', '49.7912941', '7.5117461', '10.4955731'] };
89
+ }
90
+
91
+ const params = new URLSearchParams({
92
+ format: 'json',
93
+ lat: String(lat),
94
+ lon: String(lon),
95
+ zoom: '5',
96
+ addressdetails: '1',
97
+ email: 'ypid23@aol.de',
98
+ 'accept-language': preferredLanguage
99
+ });
100
+
101
+ async function fetchNominatim() {
102
+ const response = await fetch(`${nominatim_api_url}?${params}`);
103
+ if (!response.ok) {
104
+ throw new Error(`Nominatim request failed: ${response.status}`);
105
+ }
106
+ return response.json();
107
+ }
108
+
109
+ let data = await fetchNominatim();
110
+
111
+ // Refetch with localized language if country differs from preferred language
112
+ const countryCode = data.address?.country_code;
113
+ if (countryCode && countryCode !== preferredLanguage) {
114
+ params.set('accept-language', mapCountryToLanguage(countryCode));
115
+ data = await fetchNominatim();
116
+ }
117
+
118
+ return data;
119
+ }
120
+
121
+ /**
122
+ * Toggle examples on and off. {{{
123
+ * @param {string} control - ID of the element to toggle.
124
+ * @returns {void}
125
+ */
126
+ export function toggle(control){
127
+ const elem = document.getElementById(control);
128
+
129
+ if (elem.style.display === 'none') {
130
+ elem.style.display = 'block';
131
+ } else {
132
+ elem.style.display = 'none';
133
+ }
134
+ }
135
+ /* }}} */
136
+
137
+ /**
138
+ * Open the browser prompt used to copy text to the clipboard.
139
+ * @param {string} text - Text to display for copying.
140
+ * @returns {void}
141
+ */
142
+ export function copyToClipboard(text) {
143
+ window.prompt('Copy to clipboard: Ctrl+C, Enter', text);
144
+ }
145
+
146
+ // Internal state for geocoding and date
147
+ let lat, lon, string_lat, string_lon, nominatim;
148
+ let date;
149
+
150
+ /* Helper functions for Evaluate {{{ */
151
+
152
+ function getFragmentIdentifier(selectorType) {
153
+ switch(selectorType) {
154
+ case '24/7':
155
+ return 'selector_sequence';
156
+ case 'state':
157
+ return 'section:rule_modifier';
158
+ case 'comment':
159
+ return 'comment';
160
+ default:
161
+ return `selector:${selectorType}`;
162
+ }
163
+ }
164
+
165
+ function generateRuleSeparatorElement(ruleSeparator) {
166
+ const separator = createElement('span', 'rule_separator');
167
+ separator.title = i18next.t('texts.rule separator ' + ruleSeparator);
168
+
169
+ const link = createElement('a', 'specification', ruleSeparator);
170
+ link.target = '_blank';
171
+ link.href = `${window.specification_url}#section:rule_separators`;
172
+ separator.append(link);
173
+ return separator;
174
+ }
175
+
176
+ function generateSelectorElement(selectorType, selectorValue) {
177
+ const fragmentIdentifier = getFragmentIdentifier(selectorType);
178
+ const translationKey = selectorType.match(/(?:state|comment)/) ? 'modifier' : 'selector';
179
+
180
+ const selector = createElement('span', selectorType);
181
+ selector.title = i18next.t(`words.${translationKey}`, { name: selectorType });
182
+
183
+ const link = createElement('a', 'specification', selectorValue);
184
+ link.target = '_blank';
185
+ link.href = `${window.specification_url}#${fragmentIdentifier}`;
186
+ selector.append(link);
187
+ return selector;
188
+ }
189
+
190
+ /**
191
+ * Generate HTML explanation for prettified opening hours value.
192
+ *
193
+ * Converts the internal rule structure into human-readable HTML with links
194
+ * to the specification for each selector type and rule separator.
195
+ * @param {Array} prettifiedValueArray - Array containing [rules, ruleSeparators]
196
+ * @returns {DocumentFragment} DOM fragment with formatted value explanation
197
+ */
198
+ function generateValueExplanationFragment(prettifiedValueArray) {
199
+ const [rules, ruleSeparators] = prettifiedValueArray;
200
+ const fragment = document.createDocumentFragment();
201
+ fragment.append(i18next.t('texts.prettified value for displaying'), ':');
202
+ fragment.append(document.createElement('br'));
203
+
204
+ const explanation = createElement('p', 'value_explanation');
205
+
206
+ for (const [ruleIndex, selectors] of rules.entries()) {
207
+ if (ruleIndex !== 0) {
208
+ const separatorData = ruleSeparators[ruleIndex];
209
+ const ruleSeparator = separatorData[1]
210
+ ? ' ||'
211
+ : (separatorData[0][0][1] === 'rule separator' ? ',' : ';');
212
+
213
+ explanation.append(generateRuleSeparatorElement(ruleSeparator));
214
+ explanation.append(document.createElement('br'));
215
+ }
216
+
217
+ const rule = createElement('span', 'one_rule');
218
+
219
+ for (const [selectorIndex, selector] of selectors.entries()) {
220
+ const [typeArray, selectorValue] = selector;
221
+ const selectorType = typeArray[2];
222
+
223
+ rule.append(generateSelectorElement(selectorType, selectorValue));
224
+
225
+ const isLastSelector = selectorIndex === selectors.length - 1;
226
+ if (!isLastSelector) {
227
+ rule.append(' ');
228
+ }
229
+ }
230
+
231
+ explanation.append(rule);
232
+ }
233
+
234
+ fragment.append(explanation);
235
+ return fragment;
236
+ }
237
+
238
+ function generateResultsElement(matchingRule) {
239
+ return createElement('div', 'matching-rule-card',
240
+ createElement('div', 'status-label', i18next.t('texts.MatchingRule')),
241
+ createElement('div', 'matching-rule-value', matchingRule)
242
+ );
243
+ }
244
+
245
+ /**
246
+ * Generate HTML display for deviation information between two opening hours values.
247
+ * @param {object} oh1 - The first opening_hours instance
248
+ * @param {object} oh2 - The second opening_hours instance
249
+ * @param {object} deviationInfo - Deviation data from isEqualTo comparison
250
+ * @returns {string} HTML string with formatted deviation information
251
+ */
252
+ function generateDeviationHTML(oh1, oh2, deviationInfo) {
253
+ const parts = ['<div class="diff-deviation">'];
254
+
255
+ // Show which rules are matching
256
+ if (typeof deviationInfo.matching_rule !== 'undefined' || typeof deviationInfo.matching_rule_other !== 'undefined') {
257
+ parts.push('<div class="diff-rules">');
258
+ parts.push(`<strong>${i18next.t('texts.Affected rules')}:</strong> `);
259
+ if (typeof deviationInfo.matching_rule !== 'undefined') {
260
+ parts.push(`${i18next.t('texts.Original')}: ${i18next.t('texts.Rule')} ${deviationInfo.matching_rule + 1}`);
261
+ }
262
+ if (typeof deviationInfo.matching_rule_other !== 'undefined') {
263
+ parts.push(` / ${i18next.t('texts.Comparison')}: ${i18next.t('texts.Rule')} ${deviationInfo.matching_rule_other + 1}`);
264
+ }
265
+ parts.push('</div>');
266
+ }
267
+
268
+ // Show time-based deviations with actual values
269
+ if (deviationInfo.deviation_for_time && typeof deviationInfo.deviation_for_time === 'object') {
270
+ for (const [timeCode, deviations] of Object.entries(deviationInfo.deviation_for_time)) {
271
+ const deviationDate = new Date(parseInt(timeCode));
272
+ const timeString = deviationDate.toLocaleString(i18next.language, {
273
+ year: 'numeric',
274
+ month: '2-digit',
275
+ day: '2-digit',
276
+ hour: '2-digit',
277
+ minute: '2-digit',
278
+ hour12: false
279
+ });
280
+
281
+ // Build readable comparison lines
282
+ const line1Parts = [];
283
+ const line2Parts = [];
284
+
285
+ if (deviations.includes('getState') || deviations.includes('getDate')) {
286
+ const state1 = oh1.getState(deviationDate);
287
+ const state2 = oh2.getState(deviationDate);
288
+ const unknown1 = oh1.getUnknown(deviationDate);
289
+ const unknown2 = oh2.getUnknown(deviationDate);
290
+
291
+ const stateText1 = unknown1 ? i18next.t('words.unknown') : i18next.t(`words.${state1 ? 'open' : 'closed'}`);
292
+ const stateText2 = unknown2 ? i18next.t('words.unknown') : i18next.t(`words.${state2 ? 'open' : 'closed'}`);
293
+
294
+ line1Parts.push(stateText1);
295
+ line2Parts.push(stateText2);
296
+ }
297
+
298
+ if (deviations.includes('getComment')) {
299
+ const comment1 = oh1.getComment(deviationDate);
300
+ const comment2 = oh2.getComment(deviationDate);
301
+
302
+ if (comment1) line1Parts.push(`"${comment1}"`);
303
+ if (comment2) line2Parts.push(`"${comment2}"`);
304
+ }
305
+
306
+ const comparisonHTML = `
307
+ <div class="diff-times">
308
+ <strong>${i18next.t('texts.Deviation at')} ${timeString}</strong><br>
309
+ ${i18next.t('texts.Original')}: ${line1Parts.join(', ')}<br>
310
+ ${i18next.t('texts.Comparison')}: ${line2Parts.join(', ')}
311
+ </div>
312
+ `;
313
+
314
+ parts.push(comparisonHTML);
315
+ }
316
+ }
317
+
318
+ // Show raw JSON for developers
319
+ const deviationJson = JSON.stringify(deviationInfo);
320
+ parts.push(`<div class="diff-raw"><code>${deviationJson}</code></div>`);
321
+
322
+ parts.push('</div>');
323
+ return parts.join('');
324
+ }
325
+
326
+ /**
327
+ * Compare opening hours value with a diff value and update UI accordingly.
328
+ *
329
+ * Compares the current opening hours object with another value and sets
330
+ * the background color of the diff input field to indicate the result:
331
+ * - Green (ok): Values are equivalent
332
+ * - Orange (warn): Values differ, shows deviation details in #compare-result
333
+ * - Brown (error): Diff value failed to parse
334
+ * @param {object} oh - The opening_hours instance to compare
335
+ * @param {string} diffValue - The opening hours value to compare against
336
+ * @param {number} mode - The parsing mode for opening hours
337
+ * @param {Date} startDate - The date to start comparison from
338
+ */
339
+ function handleDiffComparison(oh, diffValue, mode, startDate) {
340
+ const diffValueElement = document.getElementById('diff_value');
341
+ const compareResult = document.getElementById('compare-result');
342
+
343
+ if (diffValue.length === 0) {
344
+ diffValueElement.style.backgroundColor = '';
345
+ compareResult.innerHTML = '';
346
+ return;
347
+ }
348
+
349
+ let comparisonOh;
350
+ let comparisonResult;
351
+ try {
352
+ comparisonOh = new opening_hours(diffValue, nominatim, {
353
+ 'mode': mode,
354
+ 'warnings_severity': 7,
355
+ 'locale': i18next.language
356
+ });
357
+ comparisonResult = oh.isEqualTo(comparisonOh, startDate);
358
+ } catch {
359
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.error;
360
+ compareResult.innerHTML = '';
361
+ return;
362
+ }
363
+
364
+ if (!Array.isArray(comparisonResult)) {
365
+ compareResult.innerHTML = '';
366
+ return;
367
+ }
368
+
369
+ const [isEqual, deviationInfo] = comparisonResult;
370
+
371
+ if (isEqual) {
372
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.ok;
373
+ compareResult.innerHTML = '';
374
+ } else {
375
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.warn;
376
+ compareResult.innerHTML = generateDeviationHTML(oh, comparisonOh, deviationInfo);
377
+ }
378
+ }
379
+
380
+ function generateJosmHTML(value) {
381
+ const josmUrl = 'import?url=' + encodeURIComponent(
382
+ `https://overpass-api.de/api/xapi_meta?*[opening_hours=${value}]`
383
+ );
384
+
385
+ return `<div class="action-description">${i18next.t('texts.load osm objects')}</div>` +
386
+ `<div><a href="#" class="josm-link" data-url="${josmUrl}">JOSM</a></div>`;
387
+ }
388
+
389
+ function generateYoHoursHTML(value, crashed) {
390
+ const yoHoursChecker = new YoHoursChecker();
391
+ if (!crashed && yoHoursChecker.canRead(value)) {
392
+ const yohoursUrl = `https://projets.pavie.info/yohours/?oh=${value}`;
393
+ return `<div class="action-description">${i18next.t('texts.yohours description')}</div>` +
394
+ `<div><a href="${yohoursUrl}" target="_blank">YoHours</a></div>`;
395
+ }
396
+
397
+ return `<div class="action-description">${i18next.t('texts.yohours description')}</div>` +
398
+ `<div class="yohours-warning">${i18next.t('texts.yohours incompatible')}</div>`;
399
+ }
400
+
401
+ function generatePrettifiedValueFragment(prettified) {
402
+ // Build translation with placeholder for the link
403
+ const translatedText = i18next.t('texts.prettified value', { copyFunc: '__COPY_LINK__' });
404
+
405
+ const section = createElement('div', 'prettified-value-section');
406
+ const description = createElement('p');
407
+ const linkMatch = translatedText.match(/^(.*?)<a\b[^>]*>(.*?)<\/a>(.*)$/s);
408
+ if (linkMatch) {
409
+ const [, beforeLink, linkText, afterLink] = linkMatch;
410
+ const link = createElement('a', 'copy-prettified-value', linkText);
411
+ link.href = '#';
412
+ link.dataset.value = prettified;
413
+ description.append(beforeLink, link, afterLink, ':');
414
+ } else {
415
+ description.append(translatedText, ':');
416
+ }
417
+ section.append(description);
418
+
419
+ const container = createElement('div', 'prettified-value-container');
420
+ const valueDisplay = createElement('code', 'prettified-value-display', prettified);
421
+ valueDisplay.dataset.value = prettified;
422
+ container.append(valueDisplay);
423
+
424
+ const copyButton = createElement('button', 'copy-btn copy-prettified-btn', '📋');
425
+ copyButton.type = 'button';
426
+ copyButton.dataset.value = prettified;
427
+ copyButton.title = i18next.t('texts.copy');
428
+ container.append(copyButton);
429
+ section.append(container);
430
+ return section;
431
+ }
432
+
433
+ function createElement(tagName, className, ...children) {
434
+ const element = document.createElement(tagName);
435
+ if (className) element.className = className;
436
+ if (children.length) element.append(...children);
437
+ return element;
438
+ }
439
+
440
+ function createMessageBox(className, messageContent) {
441
+ return createElement('div', className,
442
+ i18next.t('texts.filter.error'),
443
+ createElement('div', 'warning_error_message', messageContent)
444
+ );
445
+ }
446
+
447
+ function generateWarningsFragment(warnings) {
448
+ if (warnings.length === 0) return document.createDocumentFragment();
449
+
450
+ const entries = document.createDocumentFragment();
451
+
452
+ for (const { message, value, position } of warnings) {
453
+ const entry = createElement('div', 'warning-entry');
454
+
455
+ if (position !== null && typeof value === 'string') {
456
+ const context = createElement('div', 'warning-context');
457
+ const valueCode = createElement('code');
458
+ valueCode.append(
459
+ value.substring(0, position),
460
+ createElement('span', 'warning-marker'),
461
+ value.substring(position)
462
+ );
463
+ context.append(valueCode);
464
+ entry.append(context);
465
+ }
466
+
467
+ entry.append(createElement('div', 'warning-message', message));
468
+ entries.append(entry);
469
+ }
470
+
471
+ const fragment = document.createDocumentFragment();
472
+ fragment.append(createMessageBox('warning', entries));
473
+ return fragment;
474
+ }
475
+
476
+ function generateValueTooLongFragment(prettified, value) {
477
+ const fragment = document.createDocumentFragment();
478
+ if (prettified.length <= OSM_MAX_VALUE_LENGTH) return fragment;
479
+
480
+ fragment.append(createMessageBox('warning', i18next.t('texts.value to long for osm', {
481
+ pretLength: prettified.length,
482
+ valLength: value.length,
483
+ maxLength: OSM_MAX_VALUE_LENGTH
484
+ })));
485
+ return fragment;
486
+ }
487
+
488
+ /* }}} */
489
+
490
+ /**
491
+ * Evaluate the current opening-hours expression and update the page.
492
+ * @param {number} [offset] - Offset used when evaluating the expression; defaults to 0.
493
+ * @param {boolean} [reset] - Whether to reset the current evaluation state.
494
+ * @returns {Promise<void>}
495
+ */
496
+ export async function Evaluate (offset = 0, reset) {
497
+ if (document.forms.check.elements['lat'].value !== string_lat || document.forms.check.elements['lon'].value !== string_lon) {
498
+ string_lat = document.forms.check.elements['lat'].value;
499
+ string_lon = document.forms.check.elements['lon'].value;
500
+ lat = parseFloat(string_lat);
501
+ lon = parseFloat(string_lon);
502
+ if (typeof lat !== 'number' || typeof lon !== 'number') {
503
+ if (typeof lat !== 'number') {
504
+ document.forms.check.elements['lat'].value = default_lat;
505
+ }
506
+ if (typeof lon !== 'number') {
507
+ document.forms.check.elements['lon'].value = default_lon;
508
+ }
509
+ console.log('Please enter numbers for latitude and longitude.');
510
+ return;
511
+ }
512
+ try {
513
+ nominatim = await reverseGeocodeLocation(
514
+ lat,
515
+ lon,
516
+ mapCountryToLanguage(i18next.language)
517
+ );
518
+ document.forms.check.elements['cc'].value = nominatim.address.country_code;
519
+ document.forms.check.elements['state'].value = nominatim.address.state;
520
+ Evaluate();
521
+ } catch (error) {
522
+ /* Set fallback Nominatim answer to allow using the evaluation tool even without Nominatim. */
523
+ console.error('Reverse geocoding failed:', error);
524
+ alert('Reverse geocoding of the coordinates using Nominatim was not successful. The evaluation of features of the opening_hours specification which depend this information will be unreliable. Otherwise, this tool will work as expected using a fallback answer. You might want to check your browser settings to fix this.');
525
+ nominatim = {'place_id':'44651229','licence':'Data \u00a9 OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright','osm_type':'way','osm_id':'36248375','lat':'49.5400039','lon':'9.7937133','display_name':'K 2847, Lauda-K\u00f6nigshofen, Main-Tauber-Kreis, Regierungsbezirk Stuttgart, Baden-W\u00fcrttemberg, Germany, European Union','address':{'road':'K 2847','city':'Lauda-K\u00f6nigshofen','county':'Main-Tauber-Kreis','state_district':'Regierungsbezirk Stuttgart','state':'Baden-W\u00fcrttemberg','country':'Germany','country_code':'de','continent':'European Union'}};
526
+ document.forms.check.elements['cc'].value = nominatim.address.country_code;
527
+ document.forms.check.elements['state'].value = nominatim.address.state;
528
+ Evaluate();
529
+ }
530
+ return;
531
+ }
532
+
533
+ date = reset
534
+ ? new Date()
535
+ : new Date(
536
+ currentDateTime.year,
537
+ currentDateTime.month,
538
+ currentDateTime.day,
539
+ currentDateTime.hour,
540
+ currentDateTime.minute,
541
+ offset
542
+ );
543
+
544
+ // Update module state
545
+ currentDateTime = {
546
+ year: date.getFullYear(),
547
+ month: date.getMonth(),
548
+ day: date.getDate(),
549
+ hour: date.getHours(),
550
+ minute: date.getMinutes()
551
+ };
552
+
553
+ // Update time button labels with current values
554
+ updateTimeButtonLabels(date);
555
+
556
+ // Cache DOM elements
557
+ const showTimeTable = document.getElementById('show_time_table');
558
+ const showWarningsOrErrors = document.getElementById('show_warnings_or_errors');
559
+ const showPrettifiedValue = document.getElementById('show_prettified_value');
560
+ const showResults = document.getElementById('show_results');
561
+ const actionJosm = document.getElementById('action-josm');
562
+ const actionYoHours = document.getElementById('action-yohours');
563
+
564
+ // Parse opening hours value
565
+ let crashed = false;
566
+ const value = document.forms.check.elements['expression'].value;
567
+ const diffValue = document.forms.check.elements['diff_value'].value;
568
+ const mode = parseInt(document.getElementById('mode').selectedIndex);
569
+ let oh;
570
+ let it;
571
+
572
+ try {
573
+ oh = new opening_hours(value, nominatim, {
574
+ 'mode': mode,
575
+ 'warnings_severity': 7,
576
+ 'locale': i18next.language
577
+ });
578
+ it = oh.getIterator(date);
579
+ } catch (err) {
580
+ crashed = err;
581
+ showWarningsOrErrors.replaceChildren(createMessageBox('error', crashed));
582
+ showPrettifiedValue.innerHTML = '';
583
+ showTimeTable.innerHTML = '';
584
+ showResults.innerHTML = '';
585
+ }
586
+
587
+ // Populate action links
588
+ actionJosm.innerHTML = generateJosmHTML(value);
589
+ actionYoHours.innerHTML = generateYoHoursHTML(value, crashed);
590
+
591
+ if (!crashed) {
592
+ const prettified = oh.prettifyValue({});
593
+ const prettifiedValueArray = oh.prettifyValue({
594
+ get_internals: true,
595
+ });
596
+
597
+ // Handle diff comparison
598
+ handleDiffComparison(oh, diffValue, mode, date);
599
+
600
+ // Display value explanation
601
+ showWarningsOrErrors.replaceChildren(generateValueExplanationFragment(prettifiedValueArray));
602
+ showPrettifiedValue.innerHTML = '';
603
+
604
+ // Display matching rule
605
+ const ruleIndex = it.getMatchingRule();
606
+ const matchingRule = typeof ruleIndex === 'undefined'
607
+ ? i18next.t('words.none')
608
+ : oh.prettifyValue({ 'rule_index': ruleIndex });
609
+ showResults.replaceChildren(generateResultsElement(matchingRule));
610
+
611
+ // Show prettified value if different from input
612
+ if (prettified !== value) {
613
+ showPrettifiedValue.append(generatePrettifiedValueFragment(prettified));
614
+ }
615
+
616
+ // Append warnings if any
617
+ const warnings = oh.getStructuredWarnings();
618
+ showWarningsOrErrors.append(generateWarningsFragment(warnings));
619
+
620
+ // Check value length
621
+ showWarningsOrErrors.append(generateValueTooLongFragment(prettified, value));
622
+
623
+ // Generate time table
624
+ const publicHolidayContext = oh.getPublicHolidayContext(date);
625
+ showTimeTable.innerHTML = OpeningHoursTable.drawTableAndComments(oh, it, date, warnings, publicHolidayContext);
626
+ }
627
+
628
+ updatePermalinkHref();
629
+ }
630
+
631
+ /**
632
+ * Evaluate an example expression from a page element.
633
+ * @param {HTMLElement} element - Element containing the expression.
634
+ * @returns {boolean} False to prevent the default element action.
635
+ */
636
+ export function EX (element) {
637
+ newValue(element.innerHTML);
638
+ return false;
639
+ }
640
+
641
+ /**
642
+ * Set and evaluate a new opening-hours expression.
643
+ * @param {string} value - Opening-hours expression to evaluate.
644
+ * @returns {void}
645
+ */
646
+ export function newValue(value) {
647
+ document.forms.check.elements['expression'].value = value;
648
+ Evaluate();
649
+ }
650
+
651
+ function updatePermalinkHref() {
652
+ const params = new URLSearchParams({
653
+ EXP: document.getElementById('expression').value,
654
+ lat: document.getElementById('lat').value,
655
+ lon: document.getElementById('lon').value,
656
+ mode: document.getElementById('mode').selectedIndex
657
+ });
658
+
659
+ const diffValue = document.getElementById('diff_value').value;
660
+ if (diffValue !== '') {
661
+ params.set('diff_value', diffValue);
662
+ }
663
+
664
+ const baseUrl = `${location.origin}${location.pathname}`;
665
+
666
+ // Permalink with timestamp
667
+ const paramsWithTimestamp = new URLSearchParams(params);
668
+ paramsWithTimestamp.set('DATE', date.getTime());
669
+ document.getElementById('permalink-link-with-timestamp').href = `${baseUrl}?${paramsWithTimestamp}`;
670
+
671
+ // Permalink without timestamp
672
+ document.getElementById('permalink-link-without-timestamp').href = `${baseUrl}?${params}`;
673
+ }
674
+
675
+ /**
676
+ * Request the user's current geolocation and update the form.
677
+ * @returns {void}
678
+ */
679
+ export function setCurrentPosition() {
680
+ if(navigator.geolocation) {
681
+ navigator.geolocation.getCurrentPosition(onPositionUpdate);
682
+ }
683
+ }
684
+
685
+ function onPositionUpdate(position) {
686
+ const lat = position.coords.latitude;
687
+ const lng = position.coords.longitude;
688
+ document.getElementById('lat').value = lat;
689
+ document.getElementById('lon').value = lng;
690
+ Evaluate();
691
+ console.log('Current position: ' + lat + ' ' + lng);
692
+ }
693
+
694
+ window.onload = function () {
695
+ const params = new URLSearchParams(location.search);
696
+ const customCoords = params.has('lat') || params.has('lon');
697
+
698
+ if (params.has('EXP')) {
699
+ document.forms.check.elements['expression'].value = params.get('EXP');
700
+ }
701
+ if (params.has('diff_value')) {
702
+ document.forms.check.elements['diff_value'].value = params.get('diff_value');
703
+ }
704
+ if (params.has('lat')) {
705
+ document.forms.check.elements['lat'].value = params.get('lat');
706
+ }
707
+ if (params.has('lon')) {
708
+ document.forms.check.elements['lon'].value = params.get('lon');
709
+ }
710
+ if (params.has('mode')) {
711
+ document.forms.check.elements['mode'].value = params.get('mode');
712
+ }
713
+ if (params.has('DATE')) {
714
+ try {
715
+ const loadedDate = new Date(parseInt(params.get('DATE')));
716
+ currentDateTime = {
717
+ year: loadedDate.getFullYear(),
718
+ month: loadedDate.getMonth(),
719
+ day: loadedDate.getDate(),
720
+ hour: loadedDate.getHours(),
721
+ minute: loadedDate.getMinutes()
722
+ };
723
+ Evaluate(0, false);
724
+ } catch (err) {
725
+ console.error(err);
726
+ Evaluate(0, true);
727
+ }
728
+ } else {
729
+ Evaluate(0, true);
730
+ }
731
+ if (navigator.geolocation && !customCoords) {
732
+ navigator.geolocation.getCurrentPosition(onPositionUpdate);
733
+ }
734
+ };
735
+ /* }}} */