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