File : main/travelNotes/AppLoader.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
    - v4.3.2:
23
        - Issue #82 : Not possible to open a distant file when the port of TravelNotes is not standard ( 80 or 443 )
24
25
Doc reviewed 202208
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
Loader for the app. Load all the json files needed (config, translations, map layers...) and add event listeners
46
to the document
47
*/
48
/* ------------------------------------------------------------------------------------------------------------------------- */
49
50
class AppLoader {
51
52
    /**
53
    The url of the TaN file in the fil parameter of the url
54
    @type {String}
55
    */
56
57
    #travelUrl;
58
59
    /**
60
     * The lat found in the lat url parameters
61
     * @type {Number}
62
     */
63
64
    #latUrl;
65
66
    /**
67
     * The lon found in the lon url parameters
68
     * @type {Number}
69
     */
70
71
    #lonUrl;
72
73
    /**
74
    the language in the lng parameter of the url
75
    @type {String}
76
    */
77
78
    #language;
79
80
    /**
81
    The path of the app + TravelNotes ( first part of the json file names )
82
    @type {String}
83
    */
84
85
    #originAndPath;
86
87
    /**
88
    An error message used when loading the json files
89
    @type {String}
90
    */
91
92
    #errorMessage;
93
94
    /**
95
    Read the url. Search a 'fil' parameter and a 'lng' parameter in the url.
96
    */
97
98
    #readURL ( ) {
99
        const docURL = new URL ( window.location );
100
101
        // 'fil' parameter
102
        let strTravelUrl = docURL.searchParams.get ( 'fil' );
103
        if ( strTravelUrl && ZERO !== strTravelUrl.length ) {
104
            try {
105
                strTravelUrl = atob ( strTravelUrl );
106
107
                // Verify that non illegal chars are present in the 'fil' parameter
108
                if ( strTravelUrl.match ( /[^\w-%:./]/ ) ) {
109
110
                    throw new Error ( 'invalid char in the url encoded in the fil parameter' );
111
                }
112
113
                // Verify that the given url is on the same server and uses the same protocol
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
        // 'lng' parameter (lng as 'language and not lng as longitude...). lng must be 2 letters...
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
    Loading the config.json file from the server
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
            // overload of language
172
            config.travelNotes.language = this.#language || config.travelNotes.language;
173
174
            // some special settings for the demo
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
                // eslint-disable-next-line no-magic-numbers
180
                config.note.maxManeuversNotes = 12;
181
                config.note.haveBackground = true;
182
                config.noteDialog.theDevil.addButton = false;
183
                // eslint-disable-next-line no-magic-numbers
184
                config.printRouteMap.maxTiles = 10;
185
                config.route.showDragTooltip = NOT_FOUND;
186
            }
187
188
            // default config overload with user config
189
            new ConfigOverloader ( ).overload ( config );
190
191
            // language setting for providers
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
    Loading translations
204
    @param {Object} translationPromiseResult The response of the fetch for the TravelNotesXX.json file
205
    @param {Object} defaultTranslationPromiseResult The response of the fetch for the TravelNotesEN.json file
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
    Loading the NoteDialog config
239
    @param {Object} noteDialogPromiseResult The response of the fetch for the TravelNotesNoteDialogXX.json file
240
    @param {Object} defaultNoteDialogPromiseResult The response of the fetch for the TravelNotesNoteDialogEN.json file
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
    Loading the OsmSearch dictionary
276
    @param {Object} searchDictPromiseResult The response of the fetch for the TravelNotesSearchDictionaryXX.csv file
277
    @param {Object} defaultSearchDictPromiseResult The response of the fetch for the TravelNotesSearchDictionaryEN.csv file
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
    Loading map layers
311
    @param {Object} layersPromiseResult The response of the fetch for the TravelNotesLayers.json file
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
    Loading json files from the server
332
    */
333
334
    async #loadJsonFiles ( ) {
335
336
        // loading the files in //
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
        /* eslint-disable no-magic-numbers */
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
        /* eslint-enable no-magic-numbers */
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
    Loading theTravelNotes
371
    */
372
373
    #loadTravelNotes ( ) {
374
        EventListenersLoader.addEventsListeners ( );
375
376
        document.body.style [ 'font-size' ] = String ( theConfig.fontSize.initialValue ) + 'mm';
377
378
        // mapDiv must be extensible for leaflet
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
    The constructor
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
    Load the complete app
407
    */
408
409
    async loadApp ( ) {
410
411
        // creating a reference of TravelNotes in the browser window object
412
        window.TaN = theTravelNotes;
413
414
        // reading url
415
        this.#readURL ( );
416
417
        // loading config
418
        if ( ! await this.#loadConfig ( ) ) {
419
            document.body.textContent = 'Not possible to load the TravelNotesConfig.json file. ';
420
            return;
421
        }
422
423
        // set the language to the config language if nothing in the url
424
        this.#language = this.#language || theConfig.travelNotes.language || 'fr';
425
426
        // creating the errors UI... needed for #loadJsonFiles ( ) method
427
        theErrorsUI.createUI ( );
428
429
        // loading json files
430
        if ( await this.#loadJsonFiles ( ) ) {
431
            this.#loadTravelNotes ( );
432
        }
433
434
    }
435
}
436
437
export default AppLoader;
438
439
/* --- End of file --------------------------------------------------------------------------------------------------------- */
440