opening_hours 3.10.0 → 3.11.0

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.
@@ -1,4 +1,21 @@
1
- /* global $, , default_lat, default_lon, i18next, jQuery, mapCountryToLanguage, opening_hours, OpeningHoursTable, specification_url, YoHoursChecker */
1
+ // Import dependencies
2
+ import i18next from '../../node_modules/i18next/dist/esm/i18next.bundled.js';
3
+ import { OpeningHoursTable } from './opening_hours_table.js';
4
+ import { mapCountryToLanguage } from './countryToLanguageMapping.js';
5
+ import { updateTimeButtonLabels } from './main.js';
6
+ import { YoHoursChecker } from './yohours_model.js';
7
+
8
+ // Access global variables set by main.js or UMD scripts
9
+ const { opening_hours, default_lat, default_lon } = window;
10
+
11
+ // Export date/time state
12
+ export let currentDateTime = {
13
+ year: 2013,
14
+ month: 0, // January (0-indexed)
15
+ day: 2,
16
+ hour: 22,
17
+ minute: 21
18
+ };
2
19
 
3
20
  /* Constants {{{ */
4
21
  const nominatim_api_url = 'https://nominatim.openstreetmap.org/reverse';
@@ -9,27 +26,28 @@ const evaluation_tool_colors = {
9
26
  'warn': '#FFA500',
10
27
  'error': '#DEB887',
11
28
  };
29
+
30
+ const OSM_MAX_VALUE_LENGTH = 255;
12
31
  /* }}} */
13
32
 
14
33
  // load nominatim_data in JOSM {{{
15
- // Using a different way to load stuff in JOSM than https://github.com/rurseekatze/OpenLinkMap/blob/master/js/small.js
34
+ // Using a different way to load stuff in JOSM than https://github.com/vibrog/OpenLinkMap/
16
35
  // prevent josm remote plugin of showing message
17
- // FIXME: Warning in console. Encoding stuff.
18
- // eslint-disable-next-line no-unused-vars
19
- function josm(url_param) {
20
- const xhr = new XMLHttpRequest();
21
- xhr.open('GET', 'http://localhost:8111/' + url_param, true); // true makes this call asynchronous
22
- xhr.onreadystatechange = function () { // need eventhandler since our call is async
23
- if ( xhr.status !== 200 ) {
36
+ export function josm(url_param) {
37
+ fetch(`http://localhost:8111/${url_param}`)
38
+ .then(response => {
39
+ if (!response.ok) {
40
+ alert(i18next.t('texts.JOSM remote conn error'));
41
+ }
42
+ })
43
+ .catch(() => {
24
44
  alert(i18next.t('texts.JOSM remote conn error'));
25
- }
26
- };
27
- xhr.send(null);
45
+ });
28
46
  }
29
47
  // }}}
30
48
 
31
49
  // add calculation for calendar week to date {{{
32
- function dateAtWeek(date, week) {
50
+ export function dateAtWeek(date, week) {
33
51
  const minutes_in_day = 60 * 24;
34
52
  const msec_in_day = 1000 * 60 * minutes_in_day;
35
53
  const msec_in_week = msec_in_day * 7;
@@ -40,63 +58,55 @@ function dateAtWeek(date, week) {
40
58
  }
41
59
  // }}}
42
60
 
43
- /*
61
+ /**
62
+ * Reverse geocode coordinates to get localized place names from Nominatim.
63
+ *
44
64
  * The names of countries and states are localized in OSM and opening_hours.js
45
65
  * (holidays) so we need to get the localized names from Nominatim as well.
66
+ *
67
+ * @param {number} lat - Latitude
68
+ * @param {number} lon - Longitude
69
+ * @param {string} preferredLanguage - Preferred language code (e.g., 'de', 'en')
70
+ * @returns {Promise<Object>} Nominatim response with address data
46
71
  */
47
- function reverseGeocodeLocation(query, guessed_language_for_location, on_success, on_error) {
48
- if (typeof on_error === 'undefined') {
49
- on_error = function() { };
72
+ async function reverseGeocodeLocation(lat, lon, preferredLanguage) {
73
+ // Cached response for default coordinates to avoid queries on initial load
74
+ if (lat === 48.7769 && lon === 9.1844) {
75
+ 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'] };
50
76
  }
51
77
 
52
- if (query === '&lat=48.7769&lon=9.1844') {
53
- /* Cached response to avoid two queries for each usage of the tool. */
54
- return on_success({'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']});
78
+ const params = new URLSearchParams({
79
+ format: 'json',
80
+ lat: String(lat),
81
+ lon: String(lon),
82
+ zoom: '5',
83
+ addressdetails: '1',
84
+ email: 'ypid23@aol.de',
85
+ 'accept-language': preferredLanguage
86
+ });
87
+
88
+ async function fetchNominatim() {
89
+ const response = await fetch(`${nominatim_api_url}?${params}`);
90
+ if (!response.ok) {
91
+ throw new Error(`Nominatim request failed: ${response.status}`);
92
+ }
93
+ return response.json();
55
94
  }
56
95
 
57
- const nominatim_api_url_template_query = nominatim_api_url
58
- + '?format=json'
59
- + query
60
- + '&zoom=5'
61
- + '&addressdetails=1'
62
- + '&email=ypid23@aol.de';
96
+ let data = await fetchNominatim();
63
97
 
64
- let nominatim_api_url_query = nominatim_api_url_template_query;
65
- if (typeof accept_lanaguage === 'string') {
66
- nominatim_api_url_query += '&accept-language=' + guessed_language_for_location;
98
+ // Refetch with localized language if country differs from preferred language
99
+ const countryCode = data.address?.country_code;
100
+ if (countryCode && countryCode !== preferredLanguage) {
101
+ params.set('accept-language', mapCountryToLanguage(countryCode));
102
+ data = await fetchNominatim();
67
103
  }
68
104
 
69
- $.getJSON(nominatim_api_url_query, function(nominatim_data) {
70
- // console.log(JSON.stringify(nominatim_data, null, '\t'));
71
- if (nominatim_data.address.country_code === guessed_language_for_location) {
72
- on_success(nominatim_data);
73
- } else {
74
- nominatim_api_url_query += '&accept-language=' + mapCountryToLanguage(nominatim_data.address.country_code);
75
- $.getJSON(nominatim_api_url_query, function(nominatim_data) {
76
- on_success(nominatim_data);
77
- }).error(on_error);
78
- }
79
- }).error(on_error);
80
- }
81
-
82
- // eslint-disable-next-line no-unused-vars
83
- function submitenter(myfield,e) {
84
- Evaluate();
85
- // let keycode;
86
- // if (window.event) keycode = window.event.keyCode;
87
- // else if (e) keycode = e.which;
88
- // else return true;
89
-
90
- // if (keycode === 13) {
91
- // Evaluate();
92
- // return false;
93
- // } else
94
- // return true;
105
+ return data;
95
106
  }
96
107
 
97
108
  /* JS for toggling examples on and off {{{ */
98
- // eslint-disable-next-line no-unused-vars
99
- function toggle(control){
109
+ export function toggle(control){
100
110
  const elem = document.getElementById(control);
101
111
 
102
112
  if (elem.style.display === 'none') {
@@ -107,19 +117,296 @@ function toggle(control){
107
117
  }
108
118
  /* }}} */
109
119
 
110
- // eslint-disable-next-line no-unused-vars
111
- function copyToClipboard(text) {
120
+ export function copyToClipboard(text) {
112
121
  window.prompt('Copy to clipboard: Ctrl+C, Enter', text);
113
122
  }
114
123
 
124
+ // Internal state for geocoding and date
115
125
  let lat, lon, string_lat, string_lon, nominatim;
116
126
  let date;
117
127
 
118
- function Evaluate (offset, reset) {
119
- if (typeof offset === 'undefined') {
120
- offset = 0;
128
+ /* Helper functions for Evaluate {{{ */
129
+
130
+ function getFragmentIdentifier(selectorType) {
131
+ switch(selectorType) {
132
+ case '24/7':
133
+ return 'selector_sequence';
134
+ case 'state':
135
+ return 'section:rule_modifier';
136
+ case 'comment':
137
+ return 'comment';
138
+ default:
139
+ return `selector:${selectorType}`;
140
+ }
141
+ }
142
+
143
+ function generateRuleSeparatorHTML(ruleSeparator) {
144
+ return `<span title="${i18next.t('texts.rule separator ' + ruleSeparator)}" class="rule_separator">` +
145
+ `<a target="_blank" class="specification" href="${window.specification_url}#section:rule_separators">${ruleSeparator}</a></span><br>`;
146
+ }
147
+
148
+ function generateSelectorHTML(selectorType, selectorValue) {
149
+ const fragmentIdentifier = getFragmentIdentifier(selectorType);
150
+ const translationKey = selectorType.match(/(?:state|comment)/) ? 'modifier' : 'selector';
151
+
152
+ return `<span title="${i18next.t(`words.${translationKey}`, { name: selectorType })}" class="${selectorType}">` +
153
+ `<a target="_blank" class="specification" href="${window.specification_url}#${fragmentIdentifier}">${selectorValue}</a></span>`;
154
+ }
155
+
156
+ /**
157
+ * Generate HTML explanation for prettified opening hours value.
158
+ *
159
+ * Converts the internal rule structure into human-readable HTML with links
160
+ * to the specification for each selector type and rule separator.
161
+ *
162
+ * @param {Array} prettifiedValueArray - Array containing [rules, ruleSeparators]
163
+ * @returns {string} HTML string with formatted value explanation
164
+ */
165
+ function generateValueExplanation(prettifiedValueArray) {
166
+ const [rules, ruleSeparators] = prettifiedValueArray;
167
+ const parts = [
168
+ `${i18next.t('texts.prettified value for displaying')}:<br />`,
169
+ '<p class="value_explanation">'
170
+ ];
171
+
172
+ for (const [ruleIndex, selectors] of rules.entries()) {
173
+ if (ruleIndex !== 0) {
174
+ const separatorData = ruleSeparators[ruleIndex];
175
+ const ruleSeparator = separatorData[1]
176
+ ? ' ||'
177
+ : (separatorData[0][0][1] === 'rule separator' ? ',' : ';');
178
+
179
+ parts.push(generateRuleSeparatorHTML(ruleSeparator));
180
+ }
181
+
182
+ parts.push('<span class="one_rule">');
183
+
184
+ for (const [selectorIndex, selector] of selectors.entries()) {
185
+ const [typeArray, selectorValue] = selector;
186
+ const selectorType = typeArray[2];
187
+
188
+ parts.push(generateSelectorHTML(selectorType, selectorValue));
189
+
190
+ const isLastSelector = selectorIndex === selectors.length - 1;
191
+ if (!isLastSelector) {
192
+ parts.push(' ');
193
+ }
194
+ }
195
+
196
+ parts.push('</span>');
197
+ }
198
+
199
+ parts.push('</p></div>');
200
+ return parts.join('');
201
+ }
202
+
203
+ function generateResultsHTML() {
204
+ return `
205
+ <div class="matching-rule-card">
206
+ <div class="status-label">${i18next.t('texts.MatchingRule')}</div>
207
+ <div class="matching-rule-value" id="matching-rule-display"></div>
208
+ </div>
209
+ `;
210
+ }
211
+
212
+ /**
213
+ * Generate HTML display for deviation information between two opening hours values.
214
+ *
215
+ * @param {Object} oh1 - The first opening_hours instance
216
+ * @param {Object} oh2 - The second opening_hours instance
217
+ * @param {Object} deviationInfo - Deviation data from isEqualTo comparison
218
+ * @returns {string} HTML string with formatted deviation information
219
+ */
220
+ function generateDeviationHTML(oh1, oh2, deviationInfo) {
221
+ const parts = ['<div class="diff-deviation">'];
222
+
223
+ // Show which rules are matching
224
+ if (typeof deviationInfo.matching_rule !== 'undefined' || typeof deviationInfo.matching_rule_other !== 'undefined') {
225
+ parts.push('<div class="diff-rules">');
226
+ parts.push(`<strong>${i18next.t('texts.Affected rules')}:</strong> `);
227
+ if (typeof deviationInfo.matching_rule !== 'undefined') {
228
+ parts.push(`${i18next.t('texts.Original')}: ${i18next.t('texts.Rule')} ${deviationInfo.matching_rule + 1}`);
229
+ }
230
+ if (typeof deviationInfo.matching_rule_other !== 'undefined') {
231
+ parts.push(` / ${i18next.t('texts.Comparison')}: ${i18next.t('texts.Rule')} ${deviationInfo.matching_rule_other + 1}`);
232
+ }
233
+ parts.push('</div>');
234
+ }
235
+
236
+ // Show time-based deviations with actual values
237
+ if (deviationInfo.deviation_for_time && typeof deviationInfo.deviation_for_time === 'object') {
238
+ for (const [timeCode, deviations] of Object.entries(deviationInfo.deviation_for_time)) {
239
+ const deviationDate = new Date(parseInt(timeCode));
240
+ const timeString = deviationDate.toLocaleString(i18next.language, {
241
+ year: 'numeric',
242
+ month: '2-digit',
243
+ day: '2-digit',
244
+ hour: '2-digit',
245
+ minute: '2-digit',
246
+ hour12: false
247
+ });
248
+
249
+ // Build readable comparison lines
250
+ const line1Parts = [];
251
+ const line2Parts = [];
252
+
253
+ if (deviations.includes('getState') || deviations.includes('getDate')) {
254
+ const state1 = oh1.getState(deviationDate);
255
+ const state2 = oh2.getState(deviationDate);
256
+ const unknown1 = oh1.getUnknown(deviationDate);
257
+ const unknown2 = oh2.getUnknown(deviationDate);
258
+
259
+ const stateText1 = unknown1 ? i18next.t('words.unknown') : i18next.t(`words.${state1 ? 'open' : 'closed'}`);
260
+ const stateText2 = unknown2 ? i18next.t('words.unknown') : i18next.t(`words.${state2 ? 'open' : 'closed'}`);
261
+
262
+ line1Parts.push(stateText1);
263
+ line2Parts.push(stateText2);
264
+ }
265
+
266
+ if (deviations.includes('getComment')) {
267
+ const comment1 = oh1.getComment(deviationDate);
268
+ const comment2 = oh2.getComment(deviationDate);
269
+
270
+ if (comment1) line1Parts.push(`"${comment1}"`);
271
+ if (comment2) line2Parts.push(`"${comment2}"`);
272
+ }
273
+
274
+ const comparisonHTML = `
275
+ <div class="diff-times">
276
+ <strong>${i18next.t('texts.Deviation at')} ${timeString}</strong><br>
277
+ ${i18next.t('texts.Original')}: ${line1Parts.join(', ')}<br>
278
+ ${i18next.t('texts.Comparison')}: ${line2Parts.join(', ')}
279
+ </div>
280
+ `;
281
+
282
+ parts.push(comparisonHTML);
283
+ }
284
+ }
285
+
286
+ // Show raw JSON for developers
287
+ const deviationJson = JSON.stringify(deviationInfo);
288
+ parts.push(`<div class="diff-raw"><code>${deviationJson}</code></div>`);
289
+
290
+ parts.push('</div>');
291
+ return parts.join('');
292
+ }
293
+
294
+ /**
295
+ * Compare opening hours value with a diff value and update UI accordingly.
296
+ *
297
+ * Compares the current opening hours object with another value and sets
298
+ * the background color of the diff input field to indicate the result:
299
+ * - Green (ok): Values are equivalent
300
+ * - Orange (warn): Values differ, shows deviation details in #compare-result
301
+ * - Brown (error): Diff value failed to parse
302
+ *
303
+ * @param {Object} oh - The opening_hours instance to compare
304
+ * @param {string} diffValue - The opening hours value to compare against
305
+ * @param {number} mode - The parsing mode for opening hours
306
+ * @param {Date} startDate - The date to start comparison from
307
+ */
308
+ function handleDiffComparison(oh, diffValue, mode, startDate) {
309
+ const diffValueElement = document.getElementById('diff_value');
310
+ const compareResult = document.getElementById('compare-result');
311
+
312
+ if (diffValue.length === 0) {
313
+ diffValueElement.style.backgroundColor = '';
314
+ compareResult.innerHTML = '';
315
+ return;
316
+ }
317
+
318
+ let comparisonOh;
319
+ let comparisonResult;
320
+ try {
321
+ comparisonOh = new opening_hours(diffValue, nominatim, {
322
+ 'mode': mode,
323
+ 'warnings_severity': 7,
324
+ 'locale': i18next.language
325
+ });
326
+ comparisonResult = oh.isEqualTo(comparisonOh, startDate);
327
+ } catch {
328
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.error;
329
+ compareResult.innerHTML = '';
330
+ return;
331
+ }
332
+
333
+ if (!Array.isArray(comparisonResult)) {
334
+ compareResult.innerHTML = '';
335
+ return;
336
+ }
337
+
338
+ const [isEqual, deviationInfo] = comparisonResult;
339
+
340
+ if (isEqual) {
341
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.ok;
342
+ compareResult.innerHTML = '';
343
+ } else {
344
+ diffValueElement.style.backgroundColor = evaluation_tool_colors.warn;
345
+ compareResult.innerHTML = generateDeviationHTML(oh, comparisonOh, deviationInfo);
121
346
  }
347
+ }
348
+
349
+ function generateJosmHTML(value) {
350
+ const josmUrl = 'import?url=' + encodeURIComponent(
351
+ `https://overpass-api.de/api/xapi_meta?*[opening_hours=${value}]`
352
+ );
353
+
354
+ return `<div class="action-description">${i18next.t('texts.load osm objects')}</div>` +
355
+ `<div><a href="#" class="josm-link" data-url="${josmUrl}">JOSM</a></div>`;
356
+ }
357
+
358
+ function generateYoHoursHTML(value, crashed) {
359
+ const yoHoursChecker = new YoHoursChecker();
360
+ if (!crashed && yoHoursChecker.canRead(value)) {
361
+ const yohoursUrl = `https://projets.pavie.info/yohours/?oh=${value}`;
362
+ return `<div class="action-description">${i18next.t('texts.yohours description')}</div>` +
363
+ `<div><a href="${yohoursUrl}" target="_blank">YoHours</a></div>`;
364
+ }
365
+
366
+ return `<div class="action-description">${i18next.t('texts.yohours description')}</div>` +
367
+ `<div class="yohours-warning">${i18next.t('texts.yohours incompatible')}</div>`;
368
+ }
369
+
370
+ function generatePrettifiedValueHTML(prettified) {
371
+ const escapedValue = prettified.replace(/"/g, '&quot;');
372
+
373
+ // Build translation with placeholder for the link
374
+ const translatedText = i18next.t('texts.prettified value', { copyFunc: '__COPY_LINK__' });
375
+ const linkHtml = `<a href="#" class="copy-prettified-value" data-value="${escapedValue}">`;
376
+ const finalText = translatedText.replace('<a href="__COPY_LINK__">', linkHtml);
377
+
378
+ const copyTooltip = i18next.t('texts.copy');
379
+
380
+ return `<div class="prettified-value-section">
381
+ <p>${finalText}:</p>
382
+ <div class="prettified-value-container">
383
+ <code class="prettified-value-display" data-value="${escapedValue}">${prettified}</code>
384
+ <button type="button" class="copy-btn copy-prettified-btn" data-value="${escapedValue}" title="${copyTooltip}">📋</button>
385
+ </div>
386
+ </div>`;
387
+ }
388
+
389
+ function generateWarningsHTML(warnings) {
390
+ if (warnings.length === 0) return '';
391
+
392
+ return `<div class="warning">${i18next.t('texts.filter.error')}` +
393
+ `<div class="warning_error_message">${warnings.join('\n')}</div></div>`;
394
+ }
395
+
396
+ function generateValueTooLongHTML(prettified, value) {
397
+ if (prettified.length <= OSM_MAX_VALUE_LENGTH) return '';
398
+
399
+ return `<div class="warning">${i18next.t('texts.filter.error')}` +
400
+ `<div class="warning_error_message">${i18next.t('texts.value to long for osm', {
401
+ pretLength: prettified.length,
402
+ valLength: value.length,
403
+ maxLength: OSM_MAX_VALUE_LENGTH
404
+ })}</div></div>`;
405
+ }
406
+
407
+ /* }}} */
122
408
 
409
+ export async function Evaluate (offset = 0, reset) {
123
410
  if (document.forms.check.elements['lat'].value !== string_lat || document.forms.check.elements['lon'].value !== string_lon) {
124
411
  string_lat = document.forms.check.elements['lat'].value;
125
412
  string_lon = document.forms.check.elements['lon'].value;
@@ -135,59 +422,67 @@ function Evaluate (offset, reset) {
135
422
  console.log('Please enter numbers for latitude and longitude.');
136
423
  return;
137
424
  }
138
- reverseGeocodeLocation(
139
- '&lat=' + lat + '&lon=' + lon,
140
- mapCountryToLanguage(i18next.language),
141
- function(nominatim_data) {
142
- nominatim = nominatim_data;
143
- document.forms.check.elements['cc'].value = nominatim.address.country_code;
144
- document.forms.check.elements['state'].value = nominatim.address.state;
145
- Evaluate();
146
- },
147
- function() {
148
- /* Set fallback Nominatim answer to allow using the evaluation tool even without Nominatim. */
149
- 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.');
150
- 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'}};
151
- document.forms.check.elements['cc'].value = nominatim.address.country_code;
152
- document.forms.check.elements['state'].value = nominatim.address.state;
153
- Evaluate();
154
- }
155
- );
425
+ try {
426
+ nominatim = await reverseGeocodeLocation(
427
+ lat,
428
+ lon,
429
+ mapCountryToLanguage(i18next.language)
430
+ );
431
+ document.forms.check.elements['cc'].value = nominatim.address.country_code;
432
+ document.forms.check.elements['state'].value = nominatim.address.state;
433
+ Evaluate();
434
+ } catch (error) {
435
+ /* Set fallback Nominatim answer to allow using the evaluation tool even without Nominatim. */
436
+ console.error('Reverse geocoding failed:', error);
437
+ 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.');
438
+ 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'}};
439
+ document.forms.check.elements['cc'].value = nominatim.address.country_code;
440
+ document.forms.check.elements['state'].value = nominatim.address.state;
441
+ Evaluate();
442
+ }
443
+ return;
156
444
  }
157
445
 
158
446
  date = reset
159
447
  ? new Date()
160
448
  : new Date(
161
- document.forms.check.elements['yyyy'].value,
162
- document.forms.check.elements['mm'].selectedIndex,
163
- document.forms.check.elements['dd'].value,
164
- document.forms.check.elements['HH'].value,
165
- parseInt(document.forms.check.elements['MM'].value),
449
+ currentDateTime.year,
450
+ currentDateTime.month,
451
+ currentDateTime.day,
452
+ currentDateTime.hour,
453
+ currentDateTime.minute,
166
454
  offset
167
455
  );
168
456
 
169
- function u2 (v) { return v>=0 && v<10 ? '0'+v : v; }
457
+ // Update module state
458
+ currentDateTime = {
459
+ year: date.getFullYear(),
460
+ month: date.getMonth(),
461
+ day: date.getDate(),
462
+ hour: date.getHours(),
463
+ minute: date.getMinutes()
464
+ };
170
465
 
171
- document.forms.check.elements['yyyy'].value = date.getFullYear();
172
- document.forms.check.elements['mm'].selectedIndex = date.getMonth();
173
- document.forms.check.elements['dd'].value = u2(date.getDate());
174
- document.forms.check.elements['HH'].value = u2(date.getHours());
175
- document.forms.check.elements['MM'].value = u2(date.getMinutes());
176
- document.forms.check.elements['wday'].value = date.toLocaleString(i18next.language, {weekday: 'short'});
177
- document.forms.check.elements['week'].value = 'W'+u2(dateAtWeek(date, 0) + 1);
466
+ // Update time button labels with current values
467
+ updateTimeButtonLabels(date);
178
468
 
179
- const show_time_table = document.getElementById('show_time_table');
180
- const show_warnings_or_errors = document.getElementById('show_warnings_or_errors');
181
- const show_results = document.getElementById('show_results');
469
+ // Cache DOM elements
470
+ const showTimeTable = document.getElementById('show_time_table');
471
+ const showWarningsOrErrors = document.getElementById('show_warnings_or_errors');
472
+ const showResults = document.getElementById('show_results');
473
+ const actionJosm = document.getElementById('action-josm');
474
+ const actionYoHours = document.getElementById('action-yohours');
182
475
 
183
- show_warnings_or_errors.innerHTML = '';
476
+ showWarningsOrErrors.innerHTML = '';
184
477
 
478
+ // Parse opening hours value
185
479
  let crashed = false;
186
480
  const value = document.forms.check.elements['expression'].value;
187
- const diff_value = document.forms.check.elements['diff_value'].value;
481
+ const diffValue = document.forms.check.elements['diff_value'].value;
188
482
  const mode = parseInt(document.getElementById('mode').selectedIndex);
189
483
  let oh;
190
484
  let it;
485
+
191
486
  try {
192
487
  oh = new opening_hours(value, nominatim, {
193
488
  'mode': mode,
@@ -197,204 +492,96 @@ function Evaluate (offset, reset) {
197
492
  it = oh.getIterator(date);
198
493
  } catch (err) {
199
494
  crashed = err;
200
- show_warnings_or_errors.innerHTML = `
201
- <div class="error"> ${i18next.t('texts.filter.error')}
202
- <div class="warning_error_message"> ${crashed} </div>
203
- </div>`;
204
- show_time_table.innerHTML = '';
205
- show_results.innerHTML = '';
495
+ showWarningsOrErrors.innerHTML =
496
+ `<div class="error">${i18next.t('texts.filter.error')}` +
497
+ `<div class="warning_error_message">${crashed}</div></div>`;
498
+ showTimeTable.innerHTML = '';
499
+ showResults.innerHTML = '';
206
500
  }
207
501
 
208
- show_time_table.innerHTML = '<a href="javascript:josm(\'import?url=' + encodeURIComponent('https://overpass-api.de/api/xapi_meta?*[opening_hours='
209
- + document.forms.check.elements['expression'].value + ']') + '\')">' + i18next.t('texts.load all with JOSM') + '</a><br />';
502
+ // Populate action links
503
+ actionJosm.innerHTML = generateJosmHTML(value);
504
+ actionYoHours.innerHTML = generateYoHoursHTML(value, crashed);
505
+
210
506
  if (!crashed) {
211
507
  const prettified = oh.prettifyValue({});
212
- const prettified_value_array = oh.prettifyValue({
213
- // conf: { locale: i18next.language },
508
+ const prettifiedValueArray = oh.prettifyValue({
214
509
  get_internals: true,
215
510
  });
216
- // let prettified_newline_sep = oh.prettifyValue({ conf: { locale: i18next.language, rule_sep_string: '\n', print_semicolon: false } });
217
- show_results.innerHTML = '<p><span class="hd">' + i18next.t('words.status') + ':</span>'
218
- + '<input class="nostyle" size="10" name="status" readonly="readonly" />'
219
- + '<input class="nostyle" size="60" name="comment" readonly="readonly" />'
220
- + '</p>' + '<p><span class="hd">'
221
- + i18next.t('texts.MatchingRule') + ':</span>'
222
- + '<input class="nostyle w100" name="MatchingRule" readonly="readonly" />'
223
- + '</p>';
224
- const used_selectors = { };
225
- let value_explanation =
226
- i18next.t('texts.prettified value for displaying') + ':<br />'
227
- + '<p class="value_explanation">';
228
- // console.log(JSON.stringify(prettified_value_array, null, ' '));
229
- // console.log(JSON.stringify(prettified_value_array, null, ' '));
230
- for (let nrule = 0; nrule < prettified_value_array[0].length; nrule++) {
231
- if (nrule !== 0) {
232
- const rule_separator = (
233
- prettified_value_array[1][nrule][1]
234
- ? ' ||'
235
- : (
236
- prettified_value_array[1][nrule][0][0][1] === 'rule separator'
237
- ? ','
238
- : ';'
239
- )
240
- );
241
- value_explanation +=
242
- '<span title="'
243
- + i18next.t('texts.rule separator ' + rule_separator) + '"'
244
- + ' class="rule_separator"><a target="_blank" class="specification" href="'
245
- + specification_url + '#section:rule_separators'
246
- + '">' + rule_separator + '</a></span><br>';
247
- }
248
- value_explanation += '<span class="one_rule">';
249
- for (let nselector = 0, sl = prettified_value_array[0][nrule].length; nselector < sl; nselector++) {
250
- const selector_type = prettified_value_array[0][nrule][nselector][0][2];
251
- const selector_value = prettified_value_array[0][nrule][nselector][1];
252
- let fragment_identifier;
253
- switch(selector_type) {
254
- case '24/7':
255
- fragment_identifier = 'selector_sequence';
256
- break;
257
- case 'state':
258
- fragment_identifier = 'section:rule_modifier';
259
- break;
260
- case 'comment':
261
- fragment_identifier = 'comment';
262
- break;
263
- default:
264
- fragment_identifier = 'selector:' + selector_type;
265
- }
266
- value_explanation += '<span title="'
267
- + i18next.t('words.' + (selector_type.match(/(?:state|comment)/) ? 'modifier' : 'selector'), { name: selector_type }) + '"'
268
- + ' class="' + selector_type + '"><a target="_blank" class="specification" href="'
269
- + specification_url + '#' + fragment_identifier
270
- + '">' + selector_value + '</a></span>';
271
- if (nselector + 1 < sl)
272
- value_explanation += ' ';
273
- used_selectors[selector_type] = true;
274
- }
275
- // console.log(value_explanation);
276
- value_explanation += '</span>';
277
- }
278
- value_explanation += '</p></div>';
279
- if (YoHoursChecker.canRead(value)) {
280
- value_explanation = i18next.t('texts.refer to yohours', { href: 'https://projets.pavie.info/yohours/?oh=' + value })
281
- + '<br>'
282
- + value_explanation;
283
- }
284
511
 
285
- if (diff_value.length > 0) {
286
- let is_equal_to;
287
- try {
288
- is_equal_to = oh.isEqualTo(new opening_hours(diff_value, nominatim, {
289
- 'mode': mode,
290
- 'warnings_severity': 7,
291
- 'locale': i18next.language
292
- }));
293
- } catch {
294
- $('input#diff_value').css({'background-color' : evaluation_tool_colors.error})
295
- }
296
- if (typeof is_equal_to === 'object') {
297
- if (is_equal_to[0]) {
298
- $('input#diff_value').css({'background-color' : evaluation_tool_colors.ok})
299
- } else {
300
- $('input#diff_value').css({'background-color' : evaluation_tool_colors.warn})
301
- const human_readable_not_equal_output = jQuery.extend(true, {}, is_equal_to[1])
302
- if (typeof human_readable_not_equal_output.deviation_for_time === 'object') {
303
- human_readable_not_equal_output.deviation_for_time = {};
304
- for (const time_code in is_equal_to[1].deviation_for_time) {
305
- console.log(time_code);
306
- const time_string = new Date(parseInt(time_code)).toLocaleString();
307
- human_readable_not_equal_output.deviation_for_time[time_string] =
308
- is_equal_to[1].deviation_for_time[time_code];
309
- }
310
- }
311
- value_explanation = JSON.stringify(human_readable_not_equal_output, null, ' ')
312
- + '<br>'
313
- + value_explanation;
314
- }
315
- }
316
- }
512
+ // Generate and display results
513
+ showResults.innerHTML = generateResultsHTML();
317
514
 
318
- show_warnings_or_errors.innerHTML = value_explanation;
515
+ // Generate value explanation
516
+ const valueExplanation = generateValueExplanation(prettifiedValueArray);
319
517
 
320
- // if (prettified_newline_sep.split('\n').length > 1)
321
- // show_results.innerHTML += '<p>' + i18next.t('texts.prettified value for displaying') + ':<br />'
322
- // + '<textarea rows="' + prettified_newline_sep.split('\n').length
323
- // + '" style="width: 100%" name="prettifiedValueNewlineSep" readonly="readonly">'
324
- // + prettified_newline_sep + '</textarea></p>';
518
+ // Handle diff comparison
519
+ handleDiffComparison(oh, diffValue, mode, date);
325
520
 
521
+ // Display value explanation
522
+ showWarningsOrErrors.innerHTML = valueExplanation;
326
523
 
327
- document.forms.check.elements['comment'].value = typeof it.getComment() !== 'undefined'
328
- ? it.getComment() : i18next.t('words.no') + ' ' + i18next.t('words.comment');
329
- document.forms.check.elements['status'].value = (it.getState() ? i18next.t('words.open')
330
- : (it.getUnknown() ? i18next.t('words.unknown') : i18next.t('words.closed')));
331
- const rule_index = it.getMatchingRule();
332
- document.forms.check.elements['MatchingRule'].value = typeof rule_index === 'undefined'
333
- ? i18next.t('words.none') : oh.prettifyValue({ 'rule_index': rule_index });
524
+ // Update matching rule
525
+ const ruleIndex = it.getMatchingRule();
526
+ const ruleDisplay = document.getElementById('matching-rule-display');
527
+ if (ruleDisplay) {
528
+ ruleDisplay.textContent = typeof ruleIndex === 'undefined' ? i18next.t('words.none') : oh.prettifyValue({ 'rule_index': ruleIndex });
529
+ }
334
530
 
531
+ // Show prettified value if different from input
335
532
  if (prettified !== value) {
336
- show_warnings_or_errors.innerHTML = '<p>' + i18next.t('texts.prettified value',
337
- { copyFunc: 'javascript:newValue(\'' + prettified.replace(/"/g, '&quot;') + '\')' }) + ':<br />'
338
- + '<input style="width: 100%" onclick="javascript:newValue(\'' + prettified.replace(/"/g, '&quot;') + '\')" id="prettifiedValue" name="prettifiedValue" value="' + prettified.replace(/"/g, '&quot;') + '" /></p>';
533
+ showWarningsOrErrors.innerHTML = generatePrettifiedValueHTML(prettified);
339
534
  }
340
535
 
536
+ // Append warnings if any
341
537
  const warnings = oh.getWarnings();
342
- if (warnings.length > 0) {
343
- show_warnings_or_errors.innerHTML += `
344
- <div class="warning"> ${i18next.t('texts.filter.error')}
345
- <div class="warning_error_message"> ${warnings.join('\n')} </div>
346
- </div>`;
347
- }
538
+ showWarningsOrErrors.innerHTML += generateWarningsHTML(warnings);
348
539
 
349
- if (prettified.length > 255) {
350
- show_warnings_or_errors.innerHTML += `
351
- <div class="warning"> ${i18next.t('texts.filter.error')}
352
- <div class="warning_error_message"> ${i18next.t('texts.value to long for osm',
353
- { pretLength: prettified.length, valLength: value.length, maxLength: 255 })} </div>
354
- </div>`;
355
- }
540
+ // Check value length
541
+ showWarningsOrErrors.innerHTML += generateValueTooLongHTML(prettified, value);
356
542
 
357
- show_time_table.innerHTML += OpeningHoursTable.drawTableAndComments(oh, it);
543
+ // Generate time table
544
+ showTimeTable.innerHTML = OpeningHoursTable.drawTableAndComments(oh, it, date);
358
545
  }
546
+
547
+ updatePermalinkHref();
359
548
  }
360
549
 
361
- // eslint-disable-next-line no-unused-vars
362
- function EX (element) {
550
+ export function EX (element) {
363
551
  newValue(element.innerHTML);
364
552
  return false;
365
553
  }
366
554
 
367
- function newValue(value) {
368
- if (typeof document.forms.check.elements['prettifiedValue'] === 'object') {
369
- document.forms.check.elements['prettifiedValue'].focus();
370
- document.forms.check.elements['prettifiedValue'].focus();
371
- }
555
+ export function newValue(value) {
372
556
  document.forms.check.elements['expression'].value = value;
373
557
  Evaluate();
374
558
  }
375
559
 
376
- // eslint-disable-next-line no-unused-vars
377
- function permalink () {
378
- const exp = document.getElementById('expression').value;
379
- const diff_value = document.getElementById('diff_value').value;
380
- const lat = document.getElementById('lat').value;
381
- const lon = document.getElementById('lon').value;
382
- const mode = document.getElementById('mode').selectedIndex;
560
+ function updatePermalinkHref() {
561
+ const params = new URLSearchParams({
562
+ EXP: document.getElementById('expression').value,
563
+ lat: document.getElementById('lat').value,
564
+ lon: document.getElementById('lon').value,
565
+ mode: document.getElementById('mode').selectedIndex
566
+ });
567
+
568
+ const diffValue = document.getElementById('diff_value').value;
569
+ if (diffValue !== '') {
570
+ params.set('diff_value', diffValue);
571
+ }
383
572
 
384
- let permalink_url_query='?EXP='+encodeURIComponent(exp)+'&lat='+lat+'&lon='+lon+'&mode='+mode;
573
+ const baseUrl = `${location.origin}${location.pathname}`;
385
574
 
386
- if (document.getElementById('permalink-include-timestamp').checked) {
387
- permalink_url_query += '&DATE='+date.getTime();
388
- }
389
- if (diff_value !== '') {
390
- permalink_url_query += '&diff_value='+encodeURIComponent(diff_value);
391
- }
575
+ // Permalink with timestamp
576
+ const paramsWithTimestamp = new URLSearchParams(params);
577
+ paramsWithTimestamp.set('DATE', date.getTime());
578
+ document.getElementById('permalink-link-with-timestamp').href = `${baseUrl}?${paramsWithTimestamp}`;
392
579
 
393
- location = location.protocol+'//'+location.host+location.pathname+permalink_url_query;
580
+ // Permalink without timestamp
581
+ document.getElementById('permalink-link-without-timestamp').href = `${baseUrl}?${params}`;
394
582
  }
395
583
 
396
- // eslint-disable-next-line no-unused-vars
397
- function setCurrentPosition() {
584
+ export function setCurrentPosition() {
398
585
  if(navigator.geolocation) {
399
586
  navigator.geolocation.getCurrentPosition(onPositionUpdate);
400
587
  }
@@ -408,72 +595,46 @@ function onPositionUpdate(position) {
408
595
  Evaluate();
409
596
  console.log('Current position: ' + lat + ' ' + lng);
410
597
  }
598
+
411
599
  window.onload = function () {
412
- const prmarr = window.location.search.replace( '?', '' ).split('&');
413
- const params = {};
414
- let customCoords = false;
600
+ const params = new URLSearchParams(location.search);
601
+ const customCoords = params.has('lat') || params.has('lon');
415
602
 
416
- for ( let i = 0; i < prmarr.length; i++) {
417
- const tmparr = prmarr[i].split('=');
418
- params[tmparr[0]] = tmparr[1];
419
- }
420
- if (typeof params['EXP'] !== 'undefined') {
421
- document.forms.check.elements['expression'].value = decodeURIComponent(params['EXP']);
603
+ if (params.has('EXP')) {
604
+ document.forms.check.elements['expression'].value = params.get('EXP');
422
605
  }
423
- if (typeof params['diff_value'] !== 'undefined') {
424
- document.forms.check.elements['diff_value'].value = decodeURIComponent(params['diff_value']);
606
+ if (params.has('diff_value')) {
607
+ document.forms.check.elements['diff_value'].value = params.get('diff_value');
425
608
  }
426
- if (typeof params['lat'] !== 'undefined') {
427
- document.forms.check.elements['lat'].value = decodeURIComponent(params['lat']);
428
- customCoords = true;
609
+ if (params.has('lat')) {
610
+ document.forms.check.elements['lat'].value = params.get('lat');
429
611
  }
430
- if (typeof params['lon'] !== 'undefined') {
431
- document.forms.check.elements['lon'].value = decodeURIComponent(params['lon']);
432
- customCoords = true;
612
+ if (params.has('lon')) {
613
+ document.forms.check.elements['lon'].value = params.get('lon');
433
614
  }
434
- if (typeof params['mode'] !== 'undefined') {
435
- document.forms.check.elements['mode'].value = decodeURIComponent(params['mode']);
615
+ if (params.has('mode')) {
616
+ document.forms.check.elements['mode'].value = params.get('mode');
436
617
  }
437
- if (typeof params['DATE'] !== 'undefined') {
438
- let crashed = true;
618
+ if (params.has('DATE')) {
439
619
  try {
440
- date = new Date(parseInt(params['DATE']));
441
- crashed = false;
620
+ const loadedDate = new Date(parseInt(params.get('DATE')));
621
+ currentDateTime = {
622
+ year: loadedDate.getFullYear(),
623
+ month: loadedDate.getMonth(),
624
+ day: loadedDate.getDate(),
625
+ hour: loadedDate.getHours(),
626
+ minute: loadedDate.getMinutes()
627
+ };
628
+ Evaluate(0, false);
442
629
  } catch (err) {
443
630
  console.error(err);
631
+ Evaluate(0, true);
444
632
  }
445
- if (!crashed) {
446
- document.forms.check.elements['yyyy'].value = date.getFullYear();
447
- document.forms.check.elements['mm'].selectedIndex = date.getMonth();
448
- document.forms.check.elements['dd'].value = date.getDate();
449
- document.forms.check.elements['HH'].value = date.getHours();
450
- document.forms.check.elements['MM'].value = date.getMinutes();
451
- }
452
- Evaluate(0, false);
453
633
  } else {
454
634
  Evaluate(0, true);
455
635
  }
456
636
  if (navigator.geolocation && !customCoords) {
457
637
  navigator.geolocation.getCurrentPosition(onPositionUpdate);
458
- };
638
+ }
459
639
  };
460
640
  /* }}} */
461
-
462
- $(document).ready(function () {
463
- const permalink = document.getElementById('permalink');
464
- if (permalink) {
465
- const checkbox = document.createElement('input');
466
- checkbox.type = 'checkbox';
467
- checkbox.name = 'name';
468
- checkbox.value = 'value';
469
- checkbox.id = 'permalink-include-timestamp';
470
- checkbox.checked = true;
471
-
472
- const label = document.createElement('label')
473
- label.htmlFor = 'permalink-include-timestamp';
474
- label.appendChild(document.createTextNode(i18next.t('texts.include timestamp?')));
475
-
476
- permalink.appendChild(label);
477
- permalink.appendChild(checkbox);
478
- }
479
- });