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