File : core/mapEditor/MapEditorViewer.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 theConfig from '../../data/Config.js';
26
import theDataSearchEngine from '../../data/DataSearchEngine.js';
27
import theGeometry from '../../core/lib/Geometry.js';
28
import theUtilities from '../../core/uiLib/Utilities.js';
29
import theTravelNotesData from '../../data/TravelNotesData.js';
30
import theRouteHTMLViewsFactory from '../../viewsFactories/RouteHTMLViewsFactory.js';
31
import theNoteHTMLViewsFactory from '../../viewsFactories/NoteHTMLViewsFactory.js';
32
import RouteMouseOverOrMoveEL from './RouteEL/RouteMouseOverOrMoveEL.js';
33
import theHTMLSanitizer from '../htmlSanitizer/HTMLSanitizer.js';
34
import NoteLeafletObjects from './NoteLeafletObjects.js';
35
36
import { GEOLOCATION_STATUS, ROUTE_EDITION_STATUS, NOT_FOUND, ZERO, TWO } from '../../main/Constants.js';
37
import theTranslator from '../../core/uiLib/Translator.js';
38
import theDevice from '../../core/lib/Device.js';
39
40
import {
41
    LeafletCircle,
42
    LeafletCircleMarker,
43
    LeafletDivIcon,
44
    LeafletLatLng,
45
    LeafletLayerGroup,
46
    LeafletMarker,
47
    LeafletPolyline,
48
    LeafletTileLayer,
49
    LeafletDomEvent,
50
    LeafletUtil
51
} from '../../leaflet/LeafletImports.js';
52
53
/* ------------------------------------------------------------------------------------------------------------------------- */
54
/**
55
This class performs all the readonly updates on the map
56
57
Ssee theMapEditor for read/write updates on the map
58
*/
59
/* ------------------------------------------------------------------------------------------------------------------------- */
60
61
class MapEditorViewer {
62
63
    /**
64
    A reference to the L.tileLayer  object that contains the current map
65
    @type {LeafletObject}
66
    */
67
68
    #currentLayer;
69
70
    /**
71
    A reference to the L.circleMarker object used for the geolocation
72
    @type {LeafletObject}
73
    */
74
75
    #geolocationCircle;
76
77
    /**
78
    Simple constant for the max possible zoom
79
    @type {Number}
80
    */
81
82
    // eslint-disable-next-line no-magic-numbers
83
    static get #DEFAULT_MAX_ZOOM ( ) { return 18; }
84
85
    /**
86
    Simple constant for the min possible zoom
87
    @type {Number}
88
    */
89
90
    // eslint-disable-next-line no-magic-numbers
91
    static get #DEFAULT_MIN_ZOOM ( ) { return 0; }
92
93
    /**
94
    Simple constant for the z-index css value for notes
95
    @type {Number}
96
    */
97
98
    // eslint-disable-next-line no-magic-numbers
99
    static get #NOTE_Z_INDEX_OFFSET ( ) { return 100; }
100
101
    /**
102
    The constructor
103
    */
104
105
    constructor ( ) {
106
        Object.freeze ( this );
107
        this.#currentLayer = null;
108
        this.#geolocationCircle = null;
109
    }
110
111
    /**
112
    Add a Leaflet object to the map
113
    @param {Number} objId The objId to use
114
    @param {LeafletObject} leafletObject The Leaflet object to add
115
    */
116
117
    addToMap ( objId, leafletObject ) {
118
        leafletObject.objId = objId;
119
        leafletObject.addTo ( theTravelNotesData.map );
120
        theTravelNotesData.mapObjects.set ( objId, leafletObject );
121
    }
122
123
    /**
124
    This method add a route on the map
125
    This method is called by the 'routeupdated' event listener of the viewer
126
    and by the MapEditor.updateRoute( ) method
127
    @param {Number} routeObjId The objId of the route to add
128
    @return {Route} the added Route
129
    */
130
131
    addRoute ( routeObjId ) {
132
        const route = theDataSearchEngine.getRoute ( routeObjId );
133
134
        // an array of points is created
135
        const latLng = [];
136
        const itineraryPointsIterator = route.itinerary.itineraryPoints.iterator;
137
        while ( ! itineraryPointsIterator.done ) {
138
            latLng.push ( itineraryPointsIterator.value.latLng );
139
        }
140
141
        // the leaflet polyline is created and added to the map
142
        const polyline = new LeafletPolyline (
143
            latLng,
144
            {
145
                color : route.color,
146
                weight : route.width,
147
                dashArray : route.dashString
148
            }
149
        );
150
        this.addToMap ( route.objId, polyline );
151
152
        // popup is created
153
        if ( ! theDevice.isTouch ) {
154
            polyline.bindPopup (
155
                layer => theHTMLSanitizer.clone (
156
                    theRouteHTMLViewsFactory.getRouteHeaderHTML (
157
                        'TravelNotes-Map-',
158
                        theDataSearchEngine.getRoute ( layer.objId )
159
                    )
160
                )
161
            );
162
            LeafletDomEvent.on ( polyline, 'click', clickEvent => clickEvent.target.openPopup ( clickEvent.latlng ) );
163
        }
164
165
        // tooltip is created
166
        if ( ROUTE_EDITION_STATUS.notEdited === route.editionStatus ) {
167
            polyline.bindTooltip (
168
                route.computedName,
169
                { sticky : true, direction : 'right' }
170
            );
171
            if ( ! theDevice.isTouch ) {
172
                LeafletDomEvent.on ( polyline, 'mouseover', RouteMouseOverOrMoveEL.handleEvent );
173
                LeafletDomEvent.on ( polyline, 'mousemove', RouteMouseOverOrMoveEL.handleEvent );
174
            }
175
        }
176
177
        // notes are added
178
        const notesIterator = route.notes.iterator;
179
        while ( ! notesIterator.done ) {
180
            this.addNote ( notesIterator.value.objId );
181
        }
182
183
        return route;
184
    }
185
186
    /**
187
    This method add a note on the map
188
    This method is called by the 'noteupdated' event listener of the viewer
189
    and indirectly by the MapEditor.updateNote( ) method
190
    @param {Number} noteObjId The objId of the note to add
191
    @return {NoteLeafletObjects} An object with a reference to the Leaflet objects of the note
192
    */
193
194
    addNote ( noteObjId ) {
195
        const note = theDataSearchEngine.getNoteAndRoute ( noteObjId ).note;
196
197
        // first a marker is created at the note position. This marker is empty and transparent, so
198
        // not visible on the map but the marker can be dragged
199
        const bullet = new LeafletMarker (
200
            note.latLng,
201
            {
202
                icon : new LeafletDivIcon (
203
                    {
204
                        iconSize : [ theConfig.note.grip.size, theConfig.note.grip.size ],
205
                        iconAnchor : [ theConfig.note.grip.size / TWO, theConfig.note.grip.size / TWO ],
206
                        html : '<div></div>',
207
                        className : 'TravelNotes-Map-Note-Bullet'
208
                    }
209
                ),
210
                opacity : theConfig.note.grip.opacity,
211
                draggable : ! theTravelNotesData.travel.readOnly
212
            }
213
        );
214
        bullet.objId = note.objId;
215
216
        // a second marker is now created. The icon created by the user is used for this marker
217
        const marker = new LeafletMarker (
218
            note.iconLatLng,
219
            {
220
                zIndexOffset : MapEditorViewer.#NOTE_Z_INDEX_OFFSET,
221
                icon : new LeafletDivIcon (
222
                    {
223
                        iconSize : [ note.iconWidth, note.iconHeight ],
224
                        iconAnchor : [ note.iconWidth / TWO, note.iconHeight / TWO ],
225
                        popupAnchor : [ ZERO, -note.iconHeight / TWO ],
226
                        html : note.iconContent,
227
                        className : 'TravelNotes-Map-AllNotes '
228
                    }
229
                ),
230
                draggable : ! theTravelNotesData.travel.readOnly
231
            }
232
        );
233
        marker.objId = note.objId;
234
235
        // a popup is binded to the the marker...
236
        marker.bindPopup (
237
            layer => theHTMLSanitizer.clone (
238
                theNoteHTMLViewsFactory.getNoteTextHTML (
239
                    'TravelNotes-Map-',
240
                    theDataSearchEngine.getNoteAndRoute ( layer.objId )
241
                )
242
            )
243
        );
244
245
        // ... and also a tooltip
246
        if ( ZERO !== note.tooltipContent.length ) {
247
            marker.bindTooltip (
248
                layer => theDataSearchEngine.getNoteAndRoute ( layer.objId ).note.tooltipContent
249
            );
250
            marker.getTooltip ( ).options.offset [ ZERO ] = note.iconWidth / TWO;
251
        }
252
253
        // Finally a polyline is created between the 2 markers
254
        const polyline = new LeafletPolyline ( [ note.latLng, note.iconLatLng ], theConfig.note.polyline );
255
        polyline.objId = note.objId;
256
257
        // The 3 objects are added to a layerGroup
258
        const layerGroup = new LeafletLayerGroup ( [ marker, polyline, bullet ] );
259
        layerGroup.markerId = LeafletUtil.stamp ( marker );
260
        layerGroup.polylineId = LeafletUtil.stamp ( polyline );
261
        layerGroup.bulletId = LeafletUtil.stamp ( bullet );
262
263
        // and the layerGroup added to the leaflet map and JavaScript map
264
        this.addToMap ( note.objId, layerGroup );
265
266
        if ( theConfig.note.haveBackground ) {
267
            document.querySelectorAll ( '.TravelNotes-MapNote,.TravelNotes-SvgIcon' ).forEach (
268
                noteIcon => noteIcon.classList.add ( 'TravelNotes-Map-Note-Background' )
269
            );
270
        }
271
        return new NoteLeafletObjects ( marker, polyline, bullet );
272
    }
273
274
    /**
275
    This method zoom to a point or an array of points
276
    @param {Array.<Number>} latLng the point
277
    @param {Array.<Array.<Array.<number>>>} geometry the array of points...
278
    */
279
280
    zoomTo ( latLng, geometry ) {
281
        if ( geometry ) {
282
            let latLngs = [];
283
            geometry.forEach ( geometryPart => latLngs = latLngs.concat ( geometryPart ) );
284
            theTravelNotesData.map.fitBounds ( theGeometry.getLatLngBounds ( latLngs ) );
285
        }
286
        else {
287
            theTravelNotesData.map.setView ( latLng, theConfig.itineraryPoint.zoomFactor );
288
        }
289
    }
290
291
    /**
292
    This method changes the background map.
293
    This method is called by the 'layerchange' event listener of the viewer
294
    and by the MapEditor.setLayer( ) method
295
    @param {MapLayer} layer The layer to set
296
    @param {String} url The url to use for this layer (reminder: url !== layer.url !!! See MapEditor.setLayer)
297
    */
298
299
    setLayer ( layer, url ) {
300
        const leafletLayer =
301
            'wmts' === layer.service.toLowerCase ( )
302
                ?
303
                new LeafletTileLayer ( url )
304
                :
305
                new ( LeafletTileLayer.WMS ) ( url, layer.wmsOptions );
306
307
        if ( this.#currentLayer ) {
308
            theTravelNotesData.map.removeLayer ( this.#currentLayer );
309
        }
310
        theTravelNotesData.map.addLayer ( leafletLayer );
311
        this.#currentLayer = leafletLayer;
312
        if ( ! theTravelNotesData.travel.readOnly ) {
313
314
            // strange... see Issue ♯79 ... zoom is not correct on read only file
315
            // when the background map have bounds...
316
            if ( theTravelNotesData.map.getZoom ( ) < ( layer.minZoom || MapEditorViewer.#DEFAULT_MIN_ZOOM ) ) {
317
                theTravelNotesData.map.setZoom ( layer.minZoom || MapEditorViewer.#DEFAULT_MIN_ZOOM );
318
            }
319
            theTravelNotesData.map.setMinZoom ( layer.minZoom || MapEditorViewer.#DEFAULT_MIN_ZOOM );
320
            if ( theTravelNotesData.map.getZoom ( ) > ( layer.maxZoom || MapEditorViewer.#DEFAULT_MAX_ZOOM ) ) {
321
                theTravelNotesData.map.setZoom ( layer.maxZoom || MapEditorViewer.#DEFAULT_MAX_ZOOM );
322
            }
323
            theTravelNotesData.map.setMaxZoom ( layer.maxZoom || MapEditorViewer.#DEFAULT_MAX_ZOOM );
324
            if ( layer.bounds ) {
325
                if (
326
                    ! theTravelNotesData.map.getBounds ( ).intersects ( layer.bounds )
327
                    ||
328
                    theTravelNotesData.map.getBounds ( ).contains ( layer.bounds )
329
                ) {
330
                    theTravelNotesData.map.setMaxBounds ( null );
331
                    theTravelNotesData.map.fitBounds ( layer.bounds );
332
                    theTravelNotesData.map.setZoom ( layer.minZoom || MapEditorViewer.#DEFAULT_MIN_ZOOM );
333
                }
334
                theTravelNotesData.map.setMaxBounds ( layer.bounds );
335
            }
336
            else {
337
                theTravelNotesData.map.setMaxBounds ( null );
338
            }
339
        }
340
        theTravelNotesData.map.fire ( 'baselayerchange', leafletLayer );
341
    }
342
343
    /**
344
    This method is called when the geolocation status is changed
345
    @param {GEOLOCATION_STATUS} geoLocationStatus The geolocation status
346
    */
347
348
    onGeolocationStatusChanged ( geoLocationStatus ) {
349
        if ( GEOLOCATION_STATUS.active === geoLocationStatus ) {
350
            return;
351
        }
352
        if ( this.#geolocationCircle ) {
353
            theTravelNotesData.map.removeLayer ( this.#geolocationCircle );
354
            this.#geolocationCircle = null;
355
        }
356
    }
357
358
    /**
359
    Click on the geo location circle event listener
360
    @param {Event} clickEvent The event to handle
361
    */
362
363
    #onGeoLocationPositionClick ( clickEvent ) {
364
        const copiedMessage = theTranslator.getText ( 'MapEditorViewer -Copied to clipboard' );
365
        const tooltipContent = clickEvent.target.getTooltip ( ).getContent ( );
366
        if ( NOT_FOUND === tooltipContent.indexOf ( copiedMessage ) ) {
367
            navigator.clipboard.writeText (    tooltipContent.replaceAll ( '<br/>', '\n' )    )
368
                .then (
369
                    ( ) => {
370
                        clickEvent.target.getTooltip ( ).setContent (
371
                            tooltipContent + '<br/><br/>' +
372
                            theTranslator.getText ( 'MapEditorViewer -Copied to clipboard' )
373
                        );
374
                    }
375
                )
376
                .catch ( ( ) => console.error ( 'Failed to copy to clipboard ' ) );
377
        }
378
    }
379
380
    /**
381
    This method is called when the geolocation position is changed
382
    @param {GeolocationPosition} position a JS GeolocationPosition object
383
    */
384
385
    onGeolocationPositionChanged ( position ) {
386
        let zoomToPosition = theConfig.geoLocation.zoomToPosition;
387
        if ( this.#geolocationCircle ) {
388
            theTravelNotesData.map.removeLayer ( this.#geolocationCircle );
389
            zoomToPosition = false;
390
        }
391
392
        let tooltip =
393
            'Position (+/- ' + position.coords.accuracy.toFixed ( ZERO ) + ' m) : <br/>'
394
            + theUtilities.formatLatLngDMS ( [ position.coords.latitude, position.coords.longitude ] )
395
            + '<br/>'
396
            + theUtilities.formatLatLng ( [ position.coords.latitude, position.coords.longitude ] )
397
            + '<br/>';
398
        if ( position.coords.altitude ) {
399
            tooltip += '<br/> Altitude  :<br/>'
400
            + position.coords.altitude.toFixed ( ZERO ) + ' m';
401
            if ( position.coords.altitudeAccuracy ) {
402
                tooltip += '(+/- ' + position.coords.altitudeAccuracy.toFixed ( ZERO ) + ' m)';
403
            }
404
        }
405
406
        if ( ZERO === theConfig.geoLocation.marker.radius ) {
407
            this.#geolocationCircle = new LeafletCircle (
408
                new LeafletLatLng ( position.coords.latitude, position.coords.longitude ),
409
                theConfig.geoLocation.marker
410
            );
411
            this.#geolocationCircle.setRadius ( position.coords.accuracy.toFixed ( ZERO ) );
412
        }
413
        else {
414
            this.#geolocationCircle = new LeafletCircleMarker (
415
                new LeafletLatLng ( position.coords.latitude, position.coords.longitude ),
416
                theConfig.geoLocation.marker
417
            );
418
        }
419
        this.#geolocationCircle
420
            .bindTooltip ( tooltip )
421
            .addTo ( theTravelNotesData.map );
422
        if ( ! theConfig.geoLocation.watch ) {
423
            this.#geolocationCircle
424
                .bindPopup ( tooltip )
425
                .openPopup ( );
426
        }
427
        this.#geolocationCircle.on (
428
            'click',
429
            clickEvent => { this.#onGeoLocationPositionClick ( clickEvent ); }
430
        );
431
        if ( zoomToPosition ) {
432
            theTravelNotesData.map.setView (
433
                new LeafletLatLng ( position.coords.latitude, position.coords.longitude ),
434
                theConfig.geoLocation.zoomFactor
435
            );
436
        }
437
    }
438
439
}
440
441
export default MapEditorViewer;
442
443
/* --- End of file --------------------------------------------------------------------------------------------------------- */
444