File : core/NoteEditor.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 theTranslator from './uiLib/Translator.js';
26
import theTravelNotesData from '../data/TravelNotesData.js';
27
import NoteDialog from '../dialogs/notesDialog/NoteDialog.js';
28
import Note from '../data/Note.js';
29
import theDataSearchEngine from '../data/DataSearchEngine.js';
30
import theEventDispatcher from './lib/EventDispatcher.js';
31
import theGeometry from './lib/Geometry.js';
32
import theConfig from '../data/Config.js';
33
import WaitUI from '../uis/waitUI/WaitUI.js';
34
import theErrorsUI from '../uis/errorsUI/ErrorsUI.js';
35
import theNoteDialogToolbarData from '../dialogs/notesDialog/toolbar/NoteDialogToolbarData.js';
36
import GeoCoder from './lib/GeoCoder.js';
37
38
import { DISTANCE, INVALID_OBJ_ID } from '../main/Constants.js';
39
40
/* ------------------------------------------------------------------------------------------------------------------------- */
41
/**
42
This class contains all the needed methods fot Notes creation or modifications
43
See theNoteEditor for the one and only one instance of this class
44
*/
45
/* ------------------------------------------------------------------------------------------------------------------------- */
46
47
class NoteEditor {
48
49
    /**
50
    The currently created or edited note
51
    @type {Note}
52
    */
53
54
    #note = null;
55
56
    /**
57
    The route to witch the created or edited note is linked
58
    @type {Route}
59
    */
60
61
    #route = null;
62
63
    /**
64
    A flag indicating when the note is created or edited
65
    @type {Boolean}
66
    */
67
68
    #isNewNote = true;
69
70
    /**
71
    A flag indicating when the note dialog is show when creating a search note
72
    Must be set to null at the startup because theConfig is perhaps not initialized
73
    @type {Boolean}
74
    */
75
76
    #showSearchNoteDialog = null;
77
78
    /**
79
    This method add or update a note to theTravelNotesData and to the map
80
    */
81
82
    #addNote ( ) {
83
        if ( this.#isNewNote ) {
84
            if ( this.#route ) {
85
                this.#route.notes.add ( this.#note );
86
                this.#note.chainedDistance = this.#route.chainedDistance;
87
                this.#route.notes.sort (
88
                    ( first, second ) => first.distance - second.distance
89
                );
90
            }
91
            else {
92
                theTravelNotesData.travel.notes.add ( this.#note );
93
                theEventDispatcher.dispatch ( 'updatetravelnotes' );
94
            }
95
        }
96
        else if ( ! this.#route ) {
97
98
            theEventDispatcher.dispatch ( 'updatetravelnotes' );
99
        }
100
101
        theEventDispatcher.dispatch (
102
            'noteupdated',
103
            {
104
                removedNoteObjId : this.#note.objId,
105
                addedNoteObjId : this.#note.objId
106
            }
107
        );
108
        theEventDispatcher.dispatch ( 'updateroadbook' );
109
    }
110
111
    /**
112
    This method show the Note dialog and then add or update the note
113
    */
114
115
    #noteDialog ( ) {
116
        new NoteDialog ( this.#note, this.#route )
117
            .show ( )
118
            .then ( ( ) => this.#addNote ( ) )
119
            .catch (
120
                err => {
121
                    console.error ( err );
122
                }
123
            );
124
    }
125
126
    /**
127
    This method construct a new Note object
128
    @param {Array.<Number>} latLng The latitude and longitude of the note
129
    */
130
131
    #newNote ( latLng ) {
132
        this.#isNewNote = true;
133
        this.#note = new Note ( );
134
        this.#note.latLng = latLng;
135
        this.#note.iconLatLng = latLng;
136
    }
137
138
    /**
139
    This method add a note for a searh result from osm.
140
    @param {OsmElement} osmElement an object with osm data ( see OsmSearch...)
141
    */
142
143
    async #newSearchNote ( osmElement ) {
144
145
        // Icon content
146
        if ( osmElement.tags.rcn_ref ) {
147
            this.#note.iconContent =
148
                '<div class=\'TravelNotes-MapNote TravelNotes-MapNoteCategory-0073\'>' +
149
                '<svg viewBox=\'0 0 20 20\'><text x=\'10\' y=\'14\'>' +
150
                osmElement.tags.rcn_ref +
151
                '</text></svg></div>';
152
        }
153
        else {
154
            this.#note.iconContent = theNoteDialogToolbarData.preDefinedIconDataFromName ( osmElement.description );
155
        }
156
157
        // Note data from the osmElement
158
        this.#note.url = osmElement.tags.website || '';
159
        this.#note.phone = osmElement.tags.phone || '';
160
        this.#note.tooltipContent = osmElement.description || '';
161
        this.#note.popupContent = osmElement.tags.name || '';
162
163
        // Note address...
164
        if (
165
            osmElement.tags [ 'addr:street' ]
166
            &&
167
            osmElement.tags [ 'addr:city' ]
168
        ) {
169
170
            // ...from the osmElement
171
            this.#note.address =
172
                ( osmElement.tags [ 'addr:housenumber' ] ? osmElement.tags [ 'addr:housenumber' ] + ' ' : '' ) +
173
                osmElement.tags [ 'addr:street' ] +
174
                ' <span class="TravelNotes-NoteHtml-Address-City">' + osmElement.tags [ 'addr:city' ] + '</span>';
175
        }
176
        else {
177
178
            // ... or searching with geoCoder
179
            const waitUI = new WaitUI ( );
180
            waitUI.createUI ( );
181
            waitUI.showInfo ( 'Creating address' );
182
            let geoCoderData = null;
183
            try {
184
                geoCoderData = await new GeoCoder ( ).getAddressAsync ( [ osmElement.lat, osmElement.lon ] );
185
            }
186
            catch ( err ) {
187
                console.error ( err );
188
            }
189
            waitUI.close ( );
190
            this.#note.address = geoCoderData.street;
191
            if ( '' !== geoCoderData.city ) {
192
                this.#note.address +=
193
                    ' <span class="TravelNotes-NoteHtml-Address-City">' + geoCoderData.city + '</span>';
194
            }
195
        }
196
197
        // dialog, when asked by the user or when the icon is empty
198
        if ( this.osmSearchNoteDialog || '' === this.#note.iconContent ) {
199
            this.#noteDialog ( );
200
        }
201
        else {
202
            this.#addNote ( );
203
        }
204
    }
205
206
    /**
207
    The constructor
208
    */
209
210
    constructor ( ) {
211
        Object.freeze ( this );
212
    }
213
214
    /**
215
    get the status of the osmSearchNoteDialog flag
216
    @type {Boolean}
217
    */
218
219
    get osmSearchNoteDialog ( ) {
220
        if ( null === this.#showSearchNoteDialog ) {
221
222
            // First time the status is asked, searching in theConfig
223
            this.#showSearchNoteDialog = theConfig.osmSearch.showSearchNoteDialog;
224
        }
225
        return this.#showSearchNoteDialog;
226
    }
227
228
    /**
229
    change the status of the osmSearchNoteDialog flag
230
    */
231
232
    changeOsmSearchNoteDialog ( ) {
233
        this.#showSearchNoteDialog = ! this.osmSearchNoteDialog;
234
    }
235
236
    /**
237
    This method add a route note.
238
    @param {Number} routeObjId objId of the route to witch the note will be attached
239
    @param {Array.<Number>} latLng the lat and lng of the point selected by the user
240
    */
241
242
    newRouteNote ( routeObjId, latLng ) {
243
244
        this.#route = theDataSearchEngine.getRoute ( routeObjId );
245
246
        // the nearest point and distance on the route is searched
247
        const latLngDistance = theGeometry.getClosestLatLngDistance ( this.#route, latLng );
248
249
        // the note is created
250
        this.#newNote ( latLngDistance.latLng );
251
        this.#note.distance = latLngDistance.distance;
252
253
        // and the dialog displayed
254
        this.#noteDialog ( );
255
    }
256
257
    /**
258
    This method add a route note for a searh result from osm.
259
    @param {OsmElement} osmElement an object with osm data ( see OsmSearch...)
260
    */
261
262
    newSearchRouteNote ( osmElement ) {
263
        const nearestRouteData = theDataSearchEngine.getNearestRouteData ( [ osmElement.lat, osmElement.lon ] );
264
        if ( ! nearestRouteData.route ) {
265
            theErrorsUI.showError ( theTranslator.getText ( 'NoteEditor - No route was found' ) );
266
            return;
267
        }
268
        this.#newNote ( nearestRouteData.latLngOnRoute );
269
        this.#note.iconLatLng = [ osmElement.lat, osmElement.lon ];
270
        this.#note.distance = nearestRouteData.distanceOnRoute;
271
        this.#route = nearestRouteData.route;
272
        this.#newSearchNote ( osmElement );
273
    }
274
275
    /**
276
    This method add a travel note for a searh result from osm.
277
    @param {OsmElement} osmElement an object with osm data ( see OsmSearch...)
278
    */
279
280
    newSearchTravelNote ( osmElement ) {
281
        this.#route = null;
282
        this.#newNote ( [ osmElement.lat, osmElement.lon ] );
283
        this.#newSearchNote ( osmElement );
284
    }
285
286
    /**
287
    This method add a travel note
288
    @param {Array.<Number>} latLng The latitude and longitude of the note
289
    */
290
291
    newTravelNote ( latLng ) {
292
        this.#route = null;
293
        this.#newNote ( latLng );
294
        this.#noteDialog ( );
295
    }
296
297
    /**
298
    This method start the edition of a note
299
    @param {Number} noteObjId The objId of the note to be edited
300
    */
301
302
    editNote ( noteObjId ) {
303
        this.#isNewNote = false;
304
        const noteAndRoute = theDataSearchEngine.getNoteAndRoute ( noteObjId );
305
        this.#route = noteAndRoute.route;
306
        this.#note = noteAndRoute.note;
307
        this.#noteDialog ( );
308
    }
309
310
    /**
311
    This method remove a note
312
    @param {Number} noteObjId The objId of the note to be removed
313
    */
314
315
    removeNote ( noteObjId ) {
316
317
        // the note and the route are searched
318
        const noteAndRoute = theDataSearchEngine.getNoteAndRoute ( noteObjId );
319
        if ( noteAndRoute.route ) {
320
321
            // it's a route note
322
            noteAndRoute.route.notes.remove ( noteObjId );
323
        }
324
        else {
325
326
            // it's a travel note
327
            theTravelNotesData.travel.notes.remove ( noteObjId );
328
            theEventDispatcher.dispatch ( 'updatetravelnotes' );
329
        }
330
        theEventDispatcher.dispatch (
331
            'noteupdated',
332
            {
333
                removedNoteObjId : noteObjId,
334
                addedNoteObjId : INVALID_OBJ_ID
335
            }
336
        );
337
        theEventDispatcher.dispatch ( 'updateroadbook' );
338
    }
339
340
    /**
341
    This method hide all notes on the map. The notes are always visible in the roadbook and UI
342
    */
343
344
    hideNotes ( ) {
345
        let notesIterator = theTravelNotesData.travel.notes.iterator;
346
        while ( ! notesIterator.done ) {
347
            theEventDispatcher.dispatch ( 'removeobject', { objId : notesIterator.value.objId } );
348
        }
349
        const routesIterator = theTravelNotesData.travel.routes.iterator;
350
        while ( ! routesIterator.done ) {
351
            notesIterator = routesIterator.value.notes.iterator;
352
            while ( ! notesIterator.done ) {
353
                theEventDispatcher.dispatch ( 'removeobject', { objId : notesIterator.value.objId } );
354
            }
355
        }
356
        if ( INVALID_OBJ_ID !== theTravelNotesData.editedRouteObjId ) {
357
            notesIterator = theTravelNotesData.travel.editedRoute.notes.iterator;
358
            while ( ! notesIterator.done ) {
359
                theEventDispatcher.dispatch ( 'removeobject', { objId : notesIterator.value.objId } );
360
            }
361
        }
362
    }
363
364
    /**
365
    This method show all notes on the map.
366
    */
367
368
    showNotes ( ) {
369
        this.hideNotes ( );
370
        const notesIterator = theTravelNotesData.travel.notes.iterator;
371
        while ( ! notesIterator.done ) {
372
            theEventDispatcher.dispatch (
373
                'noteupdated',
374
                {
375
                    removedNoteObjId : INVALID_OBJ_ID,
376
                    addedNoteObjId : notesIterator.value.objId
377
                }
378
            );
379
        }
380
        const routesIterator = theTravelNotesData.travel.routes.iterator;
381
        while ( ! routesIterator.done ) {
382
            if ( ! routesIterator.value.hidden ) {
383
                theEventDispatcher.dispatch (
384
                    'routeupdated',
385
                    {
386
                        removedRouteObjId : routesIterator.value.objId,
387
                        addedRouteObjId : routesIterator.value.objId
388
                    }
389
                );
390
            }
391
        }
392
    }
393
394
    /**
395
    This method transform a travel note into a route note.
396
    The nearest point on a route is selected for the note
397
    @param {Number} noteObjId The objId of the note
398
    */
399
400
    attachNoteToRoute ( noteObjId ) {
401
        const note = theDataSearchEngine.getNoteAndRoute ( noteObjId ).note;
402
        const nearestRouteData = theDataSearchEngine.getNearestRouteData ( note.latLng );
403
404
        if ( nearestRouteData.route ) {
405
            theTravelNotesData.travel.notes.remove ( noteObjId );
406
            note.distance = nearestRouteData.distanceOnRoute;
407
            note.latLng = nearestRouteData.latLngOnRoute;
408
            note.chainedDistance = nearestRouteData.route.chainedDistance;
409
            nearestRouteData.route.notes.add ( note );
410
            nearestRouteData.route.notes.sort (
411
                ( first, second ) => first.distance - second.distance
412
            );
413
414
            theEventDispatcher.dispatch (
415
                'noteupdated',
416
                {
417
                    removedNoteObjId : noteObjId,
418
                    addedNoteObjId : noteObjId
419
                }
420
            );
421
            theEventDispatcher.dispatch ( 'updatetravelnotes' );
422
            theEventDispatcher.dispatch ( 'updateroadbook' );
423
        }
424
    }
425
426
    /**
427
    This method transform a route note into a travel note.
428
    @param {Number} noteObjId The objId of the note
429
    */
430
431
    detachNoteFromRoute ( noteObjId ) {
432
        const noteAndRoute = theDataSearchEngine.getNoteAndRoute ( noteObjId );
433
        noteAndRoute.route.notes.remove ( noteObjId );
434
        noteAndRoute.note.distance = DISTANCE.invalid;
435
        noteAndRoute.note.chainedDistance = DISTANCE.defaultValue;
436
        theTravelNotesData.travel.notes.add ( noteAndRoute.note );
437
438
        theEventDispatcher.dispatch ( 'updatetravelnotes' );
439
        theEventDispatcher.dispatch ( 'updateroadbook' );
440
    }
441
442
    /**
443
    This method is called when a note is dropped in the TravelNotesPaneUI and then notes reordered.
444
    @param {Number} draggedNoteObjId The objId of the dragged note
445
    @param {Number} targetNoteObjId The objId of the note on witch the drop was executed
446
    @param {Boolean} draggedBefore when true the dragged note is moved before the target note
447
    when false after
448
    */
449
450
    travelNoteDropped ( draggedNoteObjId, targetNoteObjId, draggedBefore ) {
451
        theTravelNotesData.travel.notes.moveTo ( draggedNoteObjId, targetNoteObjId, draggedBefore );
452
        theEventDispatcher.dispatch ( 'updatetravelnotes' );
453
        theEventDispatcher.dispatch ( 'updateroadbook' );
454
    }
455
}
456
457
/* ------------------------------------------------------------------------------------------------------------------------- */
458
/**
459
The one and only one instance of NoteEditor class
460
@type {NoteEditor}
461
*/
462
/* ------------------------------------------------------------------------------------------------------------------------- */
463
464
const theNoteEditor = new NoteEditor ( );
465
466
export default theNoteEditor;
467
468
/* --- End of file --------------------------------------------------------------------------------------------------------- */
469