@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.
- package/CHANGELOG.md +816 -0
- package/LICENSES/AGPL-3.0-only.txt +235 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC0-1.0.txt +121 -0
- package/LICENSES/LGPL-3.0-only.txt +304 -0
- package/LICENSES/ODbL-1.0.txt +540 -0
- package/Makefile +526 -0
- package/README.md +926 -0
- package/REUSE.toml +40 -0
- package/build/opening_hours.esm.mjs +45870 -0
- package/build/opening_hours.esm.mjs.map +1 -0
- package/build/opening_hours.js +45875 -0
- package/package.json +133 -0
- package/site/js/countryToLanguageMapping.js +247 -0
- package/site/js/helpers.js +735 -0
- package/site/js/i18n-resources.js +2284 -0
- package/site/js/main.js +401 -0
- package/site/js/opening_hours_table.js +414 -0
- package/site/js/theme.js +73 -0
- package/site/js/yohours_model.js +2791 -0
- package/src/locales/i18n.js +97 -0
- package/types/index.d.ts +159 -0
|
@@ -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
|
+
/* }}} */
|