1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | |
26 | |
27 | |
28 | import theTravelNotes from './TravelNotes.js'; |
29 | import theTravelNotesData from '../../data/TravelNotesData.js'; |
30 | import theConfig from '../../data/Config.js'; |
31 | import ConfigOverloader from '../../data/ConfigOverloader.js'; |
32 | import theTranslator from '../../core/uiLib/Translator.js'; |
33 | import theNoteDialogToolbarData from '../../dialogs/notesDialog/toolbar/NoteDialogToolbarData.js'; |
34 | import theOsmSearchDictionary from '../../core/osmSearch/OsmSearchDictionary.js'; |
35 | import theMapLayersCollection from '../../data/MapLayersCollection.js'; |
36 | import theErrorsUI from '../../uis/errorsUI/ErrorsUI.js'; |
37 | import EventListenersLoader from './EventListenersLoader.js'; |
38 | |
39 | import { LAT_LNG, ZERO, ONE, NOT_FOUND, HTTP_STATUS_OK } from '../Constants.js'; |
40 | |
41 | import { LeafletMap } from '../../leaflet/LeafletImports.js'; |
42 | |
43 | |
44 | |
45 | |
46 | |
47 | |
48 | |
49 | |
50 | class AppLoader { |
51 | |
52 | |
53 | |
54 | |
55 | |
56 | |
57 | #travelUrl; |
58 | |
59 | |
60 | |
61 | |
62 | |
63 | |
64 | #latUrl; |
65 | |
66 | |
67 | |
68 | |
69 | |
70 | |
71 | #lonUrl; |
72 | |
73 | |
74 | |
75 | |
76 | |
77 | |
78 | #language; |
79 | |
80 | |
81 | |
82 | |
83 | |
84 | |
85 | #originAndPath; |
86 | |
87 | |
88 | |
89 | |
90 | |
91 | |
92 | #errorMessage; |
93 | |
94 | |
95 | |
96 | |
97 | |
98 | #readURL ( ) { |
99 | const docURL = new URL ( window.location ); |
100 | |
101 | |
102 | let strTravelUrl = docURL.searchParams.get ( 'fil' ); |
103 | if ( strTravelUrl && ZERO !== strTravelUrl.length ) { |
104 | try { |
105 | strTravelUrl = atob ( strTravelUrl ); |
106 | |
107 | |
108 | if ( strTravelUrl.match ( /[^\w-%:./]/ ) ) { |
109 | |
110 | throw new Error ( 'invalid char in the url encoded in the fil parameter' ); |
111 | } |
112 | |
113 | |
114 | const travelURL = new URL ( |
115 | strTravelUrl, |
116 | docURL.protocol + '//' + docURL.hostname + ( '' === docURL.port ? '' : ':' + docURL.port ) |
117 | ); |
118 | if ( Url = |
119 | docURL.protocol && travelURL.protocol && docURL.protocol === travelURL.protocol |
120 | && |
121 | docURL.hostname && travelURL.hostname && docURL.hostname === travelURL.hostname |
122 | ) { |
123 | this.#travelUrl = encodeURI ( travelURL.href ); |
124 | } |
125 | else { |
126 | throw new Error ( 'The distant file is not on the same site than the app' ); |
127 | } |
128 | } |
129 | catch ( err ) { |
130 | if ( err instanceof Error ) { |
131 | console.error ( err ); |
132 | } |
133 | } |
134 | } |
135 | |
136 | |
137 | const urlLng = docURL.searchParams.get ( 'lng' ); |
138 | if ( urlLng ) { |
139 | if ( urlLng.match ( /^[A-Z,a-z]{2}$/ ) ) { |
140 | this.#language = urlLng.toLowerCase ( ); |
141 | } |
142 | } |
143 | |
144 | const latUrl = docURL.searchParams.get ( 'lat' ); |
145 | if ( latUrl ) { |
146 | this.#latUrl = Number.parseFloat ( latUrl ); |
147 | if ( Number.isNaN ( this.#latUrl ) ) { |
148 | this.#latUrl = null; |
149 | } |
150 | } |
151 | |
152 | const lonUrl = docURL.searchParams.get ( 'lon' ); |
153 | if ( lonUrl ) { |
154 | this.#lonUrl = Number.parseFloat ( lonUrl ); |
155 | if ( Number.isNaN ( this.#lonUrl ) ) { |
156 | this.#lonUrl = null; |
157 | } |
158 | } |
159 | } |
160 | |
161 | |
162 | |
163 | |
164 | |
165 | async #loadConfig ( ) { |
166 | const configResponse = await fetch ( this.#originAndPath + 'Config.json' ); |
167 | |
168 | if ( HTTP_STATUS_OK === configResponse.status && configResponse.ok ) { |
169 | const config = await configResponse.json ( ); |
170 | |
171 | |
172 | config.travelNotes.language = this.#language || config.travelNotes.language; |
173 | |
174 | |
175 | if ( 'wwwouaiebe.github.io' === window.location.hostname ) { |
176 | config.ApiKeysDialog.haveUnsecureButtons = true; |
177 | config.errorsUI.showHelp = true; |
178 | config.mapLayersToolbar.theDevil.addButton = false; |
179 | |
180 | config.note.maxManeuversNotes = 12; |
181 | config.note.haveBackground = true; |
182 | config.noteDialog.theDevil.addButton = false; |
183 | |
184 | config.printRouteMap.maxTiles = 10; |
185 | config.route.showDragTooltip = NOT_FOUND; |
186 | } |
187 | |
188 | |
189 | new ConfigOverloader ( ).overload ( config ); |
190 | |
191 | |
192 | theTravelNotesData.providers.forEach ( |
193 | provider => { |
194 | provider.userLanguage = theConfig.travelNotes.language; |
195 | } |
196 | ); |
197 | return true; |
198 | } |
199 | return false; |
200 | } |
201 | |
202 | |
203 | |
204 | |
205 | |
206 | |
207 | |
208 | async #loadTranslations ( translationPromiseResult, defaultTranslationPromiseResult ) { |
209 | if ( |
210 | 'fulfilled' === translationPromiseResult.status |
211 | && |
212 | HTTP_STATUS_OK === translationPromiseResult.value.status |
213 | && |
214 | translationPromiseResult.value.ok |
215 | ) { |
216 | theTranslator.setTranslations ( await translationPromiseResult.value.json ( ) ); |
217 | return true; |
218 | } |
219 | if ( |
220 | 'fulfilled' === defaultTranslationPromiseResult.status |
221 | && |
222 | HTTP_STATUS_OK === defaultTranslationPromiseResult.value.status |
223 | && |
224 | defaultTranslationPromiseResult.value.ok |
225 | ) { |
226 | theTranslator.setTranslations ( await defaultTranslationPromiseResult.value.json ( ) ); |
227 | this.#errorMessage += |
228 | 'Not possible to load the TravelNotes' + |
229 | this.#language.toUpperCase ( ) + |
230 | '.json file. English will be used. '; |
231 | return true; |
232 | } |
233 | this.#errorMessage += 'Not possible to load the translations. '; |
234 | return false; |
235 | } |
236 | |
237 | |
238 | |
239 | |
240 | |
241 | |
242 | |
243 | async #loadNoteDialogConfig ( noteDialogPromiseResult, defaultNoteDialogPromiseResult ) { |
244 | if ( |
245 | 'fulfilled' === noteDialogPromiseResult.status |
246 | && |
247 | HTTP_STATUS_OK === noteDialogPromiseResult.value.status |
248 | && |
249 | noteDialogPromiseResult.value.ok |
250 | ) { |
251 | const noteDialogData = await noteDialogPromiseResult.value.json ( ); |
252 | theNoteDialogToolbarData.loadJson ( noteDialogData ); |
253 | return true; |
254 | } |
255 | if ( |
256 | 'fulfilled' === defaultNoteDialogPromiseResult.status |
257 | && |
258 | HTTP_STATUS_OK === defaultNoteDialogPromiseResult.value.status |
259 | && |
260 | defaultNoteDialogPromiseResult.value.ok |
261 | ) { |
262 | const defaultNoteDialogData = await defaultNoteDialogPromiseResult.value.json ( ); |
263 | theNoteDialogToolbarData.loadJson ( defaultNoteDialogData ); |
264 | this.#errorMessage += |
265 | 'Not possible to load the TravelNotesNoteDialog' + |
266 | this.#language.toUpperCase ( ) + |
267 | '.json file. English will be used. '; |
268 | return true; |
269 | } |
270 | this.#errorMessage += 'Not possible to load the translations for the note dialog. '; |
271 | return false; |
272 | } |
273 | |
274 | |
275 | |
276 | |
277 | |
278 | |
279 | |
280 | async #loadOsmSearchDictionary ( searchDictPromiseResult, defaultSearchDictPromiseResult ) { |
281 | if ( |
282 | 'fulfilled' === searchDictPromiseResult.status |
283 | && |
284 | HTTP_STATUS_OK === searchDictPromiseResult.value.status |
285 | && |
286 | searchDictPromiseResult.value.ok |
287 | ) { |
288 | theOsmSearchDictionary.parseDictionary ( await searchDictPromiseResult.value.text ( ) ); |
289 | return true; |
290 | } |
291 | if ( |
292 | 'fulfilled' === defaultSearchDictPromiseResult.status |
293 | && |
294 | HTTP_STATUS_OK === defaultSearchDictPromiseResult.value.status |
295 | && |
296 | defaultSearchDictPromiseResult.value.ok |
297 | ) { |
298 | theOsmSearchDictionary.parseDictionary ( await defaultSearchDictPromiseResult.value.text ( ) ); |
299 | this.#errorMessage += |
300 | 'Not possible to load the TravelNotesSearchDictionary' + |
301 | this.#language.toUpperCase ( ) + |
302 | '.csv file. English will be used. '; |
303 | return true; |
304 | } |
305 | this.#errorMessage += 'Not possible to load the search dictionary. OSM search will not be available.'; |
306 | return true; |
307 | } |
308 | |
309 | |
310 | |
311 | |
312 | |
313 | |
314 | async #loadMapLayers ( layersPromiseResult ) { |
315 | if ( |
316 | 'fulfilled' === layersPromiseResult.status |
317 | && |
318 | HTTP_STATUS_OK === layersPromiseResult.value.status |
319 | && |
320 | layersPromiseResult.value.ok |
321 | ) { |
322 | theMapLayersCollection.addMapLayers ( await layersPromiseResult.value.json ( ) ); |
323 | return true; |
324 | } |
325 | this.#errorMessage += |
326 | 'Not possible to load the TravelNotesLayers.json file. Only the OpenStreetMap background will be available. '; |
327 | return true; |
328 | } |
329 | |
330 | |
331 | |
332 | |
333 | |
334 | async #loadJsonFiles ( ) { |
335 | |
336 | |
337 | const results = await Promise.allSettled ( [ |
338 | fetch ( this.#originAndPath + this.#language.toUpperCase ( ) + '.json' ), |
339 | fetch ( this.#originAndPath + 'EN.json' ), |
340 | fetch ( this.#originAndPath + 'NoteDialog' + this.#language.toUpperCase ( ) + '.json' ), |
341 | fetch ( this.#originAndPath + 'NoteDialogEN.json' ), |
342 | fetch ( this.#originAndPath + 'SearchDictionary' + this.#language.toUpperCase ( ) + '.csv' ), |
343 | fetch ( this.#originAndPath + 'SearchDictionaryEN.csv' ), |
344 | fetch ( this.#originAndPath + 'Layers.json' ) |
345 | ] ); |
346 | |
347 | |
348 | const jsonSuccess = |
349 | await this.#loadTranslations ( results [ 0 ], results [ 1 ] ) |
350 | && |
351 | await this.#loadNoteDialogConfig ( results [ 2 ], results [ 3 ] ) |
352 | && |
353 | await this.#loadOsmSearchDictionary ( results [ 4 ], results [ 5 ] ) |
354 | && |
355 | await this.#loadMapLayers ( results [ 6 ] ); |
356 | |
357 | |
358 | |
359 | if ( '' !== this.#errorMessage && jsonSuccess ) { |
360 | theErrorsUI.showError ( this.#errorMessage ); |
361 | } |
362 | else if ( '' !== this.#errorMessage ) { |
363 | document.body.textContent = this.#errorMessage; |
364 | } |
365 | |
366 | return jsonSuccess; |
367 | } |
368 | |
369 | |
370 | |
371 | |
372 | |
373 | #loadTravelNotes ( ) { |
374 | EventListenersLoader.addEventsListeners ( ); |
375 | |
376 | document.body.style [ 'font-size' ] = String ( theConfig.fontSize.initialValue ) + 'mm'; |
377 | |
378 | |
379 | const mapDiv = document.createElement ( 'div' ); |
380 | mapDiv.id = 'TravelNotes-Map'; |
381 | document.body.appendChild ( mapDiv ); |
382 | theTravelNotesData.map = new LeafletMap ( mapDiv.id, { attributionControl : false, zoomControl : false } ) |
383 | .setView ( [ LAT_LNG.defaultValue, LAT_LNG.defaultValue ], ZERO ); |
384 | |
385 | if ( this.#travelUrl ) { |
386 | theTravelNotes.addReadOnlyTravel ( this.#travelUrl ); |
387 | } |
388 | else { |
389 | EventListenersLoader.addUnloadEventsListeners ( ); |
390 | theTravelNotes.addToolbarsMenusUIs ( this.#latUrl, this.#lonUrl ); |
391 | } |
392 | } |
393 | |
394 | |
395 | |
396 | |
397 | |
398 | constructor ( ) { |
399 | Object.freeze ( this ); |
400 | this.#originAndPath = |
401 | window.location.href.substring ( ZERO, window.location.href.lastIndexOf ( '/' ) + ONE ) + 'TravelNotes'; |
402 | this.#errorMessage = ''; |
403 | } |
404 | |
405 | |
406 | |
407 | |
408 | |
409 | async loadApp ( ) { |
410 | |
411 | |
412 | window.TaN = theTravelNotes; |
413 | |
414 | |
415 | this.#readURL ( ); |
416 | |
417 | |
418 | if ( ! await this.#loadConfig ( ) ) { |
419 | document.body.textContent = 'Not possible to load the TravelNotesConfig.json file. '; |
420 | return; |
421 | } |
422 | |
423 | |
424 | this.#language = this.#language || theConfig.travelNotes.language || 'fr'; |
425 | |
426 | |
427 | theErrorsUI.createUI ( ); |
428 | |
429 | |
430 | if ( await this.#loadJsonFiles ( ) ) { |
431 | this.#loadTravelNotes ( ); |
432 | } |
433 | |
434 | } |
435 | } |
436 | |
437 | export default AppLoader; |
438 | |
439 | |
440 | |