File : core/mapEditor/MapEditor.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 theTravelNotesData from '../../data/TravelNotesData.js';
27
import theDataSearchEngine from '../../data/DataSearchEngine.js';
28
import theGeometry from '../../core/lib/Geometry.js';
29
import theApiKeysManager from '../../core/ApiKeysManager.js';
30
import MapEditorViewer from './MapEditorViewer.js';
31
import EditedRouteMouseOverEL from './editedRouteEL/EditedRouteMouseOverEL.js';
32
import NoteBulletDragEndEL from './noteBulletEL/NoteBulletDragEndEL.js';
33
import NoteBulletDragEL from './noteBulletEL/NoteBulletDragEL.js';
34
import NoteBulletMouseEnterEL from './noteBulletEL/NoteBulletMouseEnterEL.js';
35
import NoteBulletMouseLeaveEL from './noteBulletEL/NoteBulletMouseLeaveEL.js';
36
import NoteMarkerContextMenuEL from './NoteMarkerEL/NoteMarkerContextMenuEL.js';
37
import NoteMarkerDragEndEL from './NoteMarkerEL/NoteMarkerDragEndEL.js';
38
import NoteMarkerDragEL from './NoteMarkerEL/NoteMarkerDragEL.js';
39
import WayPointContextMenuEL from './wayPointEL/WayPointContextMenuEL.js';
40
import WayPointDragEndEL from './wayPointEL/WayPointDragEndEL.js';
41
import RouteMapContextMenuEL from './RouteEL/RouteMapContextMenuEL.js';
42
import { ROUTE_EDITION_STATUS, LAT_LNG, INVALID_OBJ_ID, TWO, WAY_POINT_ICON_SIZE } from '../../main/Constants.js';
43
import theDevice from '../lib/Device.js';
44
45
import {
46
    LeafletCircleMarker,
47
    LeafletDivIcon,
48
    LeafletMarker,
49
    LeafletPolyline,
50
    LeafletRectangle,
51
    LeafletDomEvent
52
} from '../../leaflet/LeafletImports.js';
53
54
/* ------------------------------------------------------------------------------------------------------------------------- */
55
/**
56
This class performs all the read/write updates on the map
57
58
See theMapEditor for the one and only one instance of this class
59
*/
60
/* ------------------------------------------------------------------------------------------------------------------------- */
61
62
class MapEditor    extends MapEditorViewer {
63
64
    /**
65
    Simple constant for computing if we add a polyline or a marker for the search
66
    @type {Number}
67
    */
68
69
    // eslint-disable-next-line no-magic-numbers
70
    static get #MARKER_BOUNDS_PRECISION ( ) { return 0.01; }
71
72
    /**
73
    Remove a Leaflet object from the map
74
    @param {Number} objId The objId of the object to remove
75
    */
76
77
    #RemoveFromMap ( objId ) {
78
        const layer = theTravelNotesData.mapObjects.get ( objId );
79
        if ( layer ) {
80
            LeafletDomEvent.off ( layer );
81
            theTravelNotesData.map.removeLayer ( layer );
82
            theTravelNotesData.mapObjects.delete ( objId );
83
        }
84
    }
85
86
    /**
87
    The constructor
88
    */
89
90
    constructor ( ) {
91
        super ( );
92
    }
93
94
    /**
95
    This method update a route on the map.
96
    This method is also used for removing a route with the addedRouteObjId = INVALID_OBJ_ID.
97
    This method is also used for adding a route with the removedRouteObjId = INVALID_OBJ_ID.
98
    This method is called by the 'routeupdated' event listener.
99
    @param {Number} removedRouteObjId The objId of the route to remove
100
    @param {Number} addedRouteObjId The objId of the route to add
101
    */
102
103
    updateRoute ( removedRouteObjId, addedRouteObjId ) {
104
        if ( INVALID_OBJ_ID !== removedRouteObjId ) {
105
            const route = theDataSearchEngine.getRoute ( removedRouteObjId );
106
            this.#RemoveFromMap ( route.objId );
107
108
            const notesIterator = route.notes.iterator;
109
            while ( ! notesIterator.done ) {
110
                this.#RemoveFromMap ( notesIterator.value.objId );
111
            }
112
113
            const wayPointsIterator = route.wayPoints.iterator;
114
            while ( ! wayPointsIterator.done ) {
115
                this.#RemoveFromMap ( wayPointsIterator.value.objId );
116
            }
117
        }
118
        if ( INVALID_OBJ_ID !== addedRouteObjId ) {
119
            const route = this.addRoute ( addedRouteObjId );
120
            const polyline = theTravelNotesData.mapObjects.get ( addedRouteObjId );
121
122
            if ( ! theTravelNotesData.travel.readOnly ) {
123
                LeafletDomEvent.on ( polyline, 'contextmenu', RouteMapContextMenuEL.handleEvent );
124
                if ( ROUTE_EDITION_STATUS.notEdited !== route.editionStatus && ! theDevice.isTouch ) {
125
                    LeafletDomEvent.on ( polyline, 'mouseover', EditedRouteMouseOverEL.handleEvent );
126
                }
127
                const notesIterator = route.notes.iterator;
128
                while ( ! notesIterator.done ) {
129
                    const layerGroup = theTravelNotesData.mapObjects.get ( notesIterator.value.objId );
130
                    const marker = layerGroup.getLayer ( layerGroup.markerId );
131
                    const bullet = layerGroup.getLayer ( layerGroup.bulletId );
132
                    LeafletDomEvent.on ( bullet, 'dragend', NoteBulletDragEndEL.handleEvent );
133
                    LeafletDomEvent.on ( bullet, 'drag',    NoteBulletDragEL.handleEvent );
134
                    LeafletDomEvent.on ( bullet, 'mouseenter', NoteBulletMouseEnterEL.handleEvent );
135
                    LeafletDomEvent.on ( bullet, 'mouseleave', NoteBulletMouseLeaveEL.handleEvent );
136
                    LeafletDomEvent.on ( marker, 'contextmenu', NoteMarkerContextMenuEL.handleEvent );
137
                    LeafletDomEvent.on ( marker, 'dragend', NoteMarkerDragEndEL.handleEvent );
138
                    LeafletDomEvent.on ( marker, 'drag', NoteMarkerDragEL.handleEvent );
139
                }
140
            }
141
142
            // waypoints are added
143
            if ( ! theTravelNotesData.travel.readOnly && ROUTE_EDITION_STATUS.notEdited !== route.editionStatus ) {
144
                const wayPointsIterator = theTravelNotesData.travel.editedRoute.wayPoints.iterator;
145
                while ( ! wayPointsIterator.done ) {
146
                    this.addWayPoint (
147
                        wayPointsIterator.value,
148
                        wayPointsIterator .first ? 'A' : ( wayPointsIterator.last ? 'B' : wayPointsIterator.index )
149
                    );
150
                }
151
            }
152
        }
153
    }
154
155
    /**
156
    This method update the properties of a route on the map
157
    This method is called by the 'routepropertiesupdated' event listener.
158
    @param {Number} routeObjId The objId of the route to update
159
    */
160
161
    updateRouteProperties ( routeObjId ) {
162
        const polyline = theTravelNotesData.mapObjects.get ( routeObjId );
163
        const route = theDataSearchEngine.getRoute ( routeObjId );
164
        polyline.setStyle (
165
            {
166
                color : route.color,
167
                weight : route.width,
168
                dashArray : route.dashString
169
            }
170
        );
171
    }
172
173
    /**
174
    This method update a note on the map.
175
    This method is also used for removing a note with the addedNoteObjId = INVALID_OBJ_ID.
176
    This method is also used for adding a note with the removedNoteObjId = INVALID_OBJ_ID.
177
    This method is called by the 'noteupdated' event listener.
178
    @param {Number} removedNoteObjId The objId of the note to remove
179
    @param {Number} addedNoteObjId The objId of the note to add
180
    */
181
182
    updateNote ( removedNoteObjId, addedNoteObjId ) {
183
        let isPopupOpen = false;
184
        if ( INVALID_OBJ_ID !== removedNoteObjId ) {
185
            const layerGroup = theTravelNotesData.mapObjects.get ( removedNoteObjId );
186
            if ( layerGroup ) {
187
                isPopupOpen = layerGroup.getLayer ( layerGroup.markerId ).isPopupOpen ( );
188
            }
189
            this.#RemoveFromMap ( removedNoteObjId );
190
        }
191
        if ( INVALID_OBJ_ID !== addedNoteObjId ) {
192
            const noteObjects = this.addNote ( addedNoteObjId );
193
            if ( isPopupOpen ) {
194
                noteObjects.marker.openPopup ( );
195
            }
196
            if ( ! theTravelNotesData.travel.readOnly ) {
197
                LeafletDomEvent.on ( noteObjects.bullet, 'dragend', NoteBulletDragEndEL.handleEvent );
198
                LeafletDomEvent.on ( noteObjects.bullet, 'drag',    NoteBulletDragEL.handleEvent );
199
                LeafletDomEvent.on ( noteObjects.bullet, 'mouseenter',    NoteBulletMouseEnterEL.handleEvent );
200
                LeafletDomEvent.on ( noteObjects.bullet, 'mouseleave',    NoteBulletMouseLeaveEL.handleEvent );
201
                LeafletDomEvent.on ( noteObjects.marker, 'contextmenu', NoteMarkerContextMenuEL.handleEvent );
202
                LeafletDomEvent.on ( noteObjects.marker, 'dragend', NoteMarkerDragEndEL.handleEvent );
203
                LeafletDomEvent.on ( noteObjects.marker, 'drag', NoteMarkerDragEL.handleEvent );
204
            }
205
        }
206
    }
207
208
    /**
209
    This method removes an object from the map.
210
    This method is called by the 'removeobject' event listener
211
    @param {Number} objId The objId of the object to remove
212
    */
213
214
    removeObject ( objId ) { this.#RemoveFromMap ( objId ); }
215
216
    /**
217
    This method removes all objects from the map.
218
    This method is called by the 'removeallobjects' event listener
219
    */
220
221
    removeAllObjects ( ) {
222
        theTravelNotesData.mapObjects.forEach (
223
            mapObject => {
224
                LeafletDomEvent.off ( mapObject );
225
                theTravelNotesData.map.removeLayer ( mapObject );
226
            }
227
        );
228
        theTravelNotesData.mapObjects.clear ( );
229
    }
230
231
    /**
232
    This method add a WayPoint to the map.
233
    This method is called by the 'addwaypoint' event listener.
234
    @param {WayPoint} wayPoint The wayPoint to add
235
    @param {string|number} letter The letter or number to show with the WayPoint
236
    */
237
238
    addWayPoint ( wayPoint, letter ) {
239
        if ( ( LAT_LNG.defaultValue === wayPoint.lat ) && ( LAT_LNG.defaultValue === wayPoint.lng ) ) {
240
            return;
241
        }
242
243
        // a HTML element is created, with different class name, depending of the waypont position. See also WayPoints.css
244
        const iconHtml = '<div class="TravelNotes-Map-WayPoint TravelNotes-Map-WayPoint' +
245
        ( 'A' === letter ? 'Start' : ( 'B' === letter ? 'End' : 'Via' ) ) +
246
        '"></div><div class="TravelNotes-Map-WayPointText">' + letter + '</div>';
247
248
        // a leaflet marker is created...
249
        const marker = new LeafletMarker (
250
            wayPoint.latLng,
251
            {
252
                icon : new LeafletDivIcon (
253
                    {
254
                        iconSize : [ WAY_POINT_ICON_SIZE, WAY_POINT_ICON_SIZE ],
255
                        iconAnchor : [
256
                            WAY_POINT_ICON_SIZE / TWO,
257
                            WAY_POINT_ICON_SIZE
258
                        ],
259
                        html : iconHtml,
260
                        className : 'TravelNotes-Map-WayPointStyle'
261
                    }
262
                ),
263
                draggable : true
264
            }
265
        );
266
267
        marker.bindTooltip (
268
            tooltipWayPoint => theDataSearchEngine.getWayPoint ( tooltipWayPoint.objId ).fullName
269
        );
270
        marker.getTooltip ( ).options.offset = [
271
            WAY_POINT_ICON_SIZE / TWO,
272
            -WAY_POINT_ICON_SIZE / TWO
273
        ];
274
275
        LeafletDomEvent.on ( marker, 'contextmenu', WayPointContextMenuEL.handleEvent );
276
277
        // ... and added to the map...
278
        marker.objId = wayPoint.objId;
279
        this.addToMap ( wayPoint.objId, marker );
280
281
        // ... and a dragend event listener is created
282
        LeafletDomEvent.on ( marker, 'dragend', WayPointDragEndEL.handleEvent );
283
    }
284
285
    /**
286
    This method add an itinerary point marker to the map (= a leaflet.circleMarker object).
287
    This method is called by the 'additinerarypointmarker' event listener.
288
    @param {Number} objId A unique identifier to attach to the circleMarker
289
    @param {Array.<Number>} latLng The latitude and longitude of the itinerary point marker
290
    */
291
292
    addItineraryPointMarker ( objId, latLng ) {
293
        this.addToMap (
294
            objId,
295
            new LeafletCircleMarker ( latLng, theConfig.itineraryPoint.marker )
296
        );
297
    }
298
299
    /**
300
    This method add an search point marker to the map
301
    (= a leaflet.circleMarker object or a polyline, depending of the zoom and the geometry parameter).
302
    This method is called by the 'addsearchpointmarker' event listener.
303
    @param {Number} objId A unique identifier to attach to the circleMarker
304
    @param {Array.<Number>} latLng The latitude and longitude of the search point marker
305
    @param {?Array.<Array.<number>>} geometry The latitudes and longitudes of the search point marker when a polyline
306
    can be showed
307
    */
308
309
    addSearchPointMarker ( objId, latLng, geometry ) {
310
        let showGeometry = false;
311
        if ( geometry ) {
312
            let latLngs = [];
313
            geometry.forEach (
314
                geometryPart => { latLngs = latLngs.concat ( geometryPart ); }
315
            );
316
            const geometryBounds = theGeometry.getLatLngBounds ( latLngs );
317
            const mapBounds = theTravelNotesData.map.getBounds ( );
318
            showGeometry =
319
                (
320
                    ( geometryBounds.getEast ( ) - geometryBounds.getWest ( ) )
321
                    /
322
                    ( mapBounds.getEast ( ) - mapBounds.getWest ( ) )
323
                ) > MapEditor.#MARKER_BOUNDS_PRECISION
324
                &&
325
                (
326
                    ( geometryBounds.getNorth ( ) - geometryBounds.getSouth ( ) )
327
                    /
328
                    ( mapBounds.getNorth ( ) - mapBounds.getSouth ( ) )
329
                ) > MapEditor.#MARKER_BOUNDS_PRECISION;
330
        }
331
        if ( showGeometry ) {
332
            this.addToMap ( objId, new LeafletPolyline ( geometry, theConfig.osmSearch.searchPointPolyline ) );
333
        }
334
        else {
335
            this.addToMap ( objId, new LeafletCircleMarker ( latLng, theConfig.osmSearch.searchPointMarker ) );
336
        }
337
    }
338
339
    /**
340
    This method add a rectangle to the map.
341
    This method is called by the 'addrectangle' event listener.
342
    @param {Number} objId A unique identifier to attach to the rectangle
343
    @param {Array.<Array.<number>>} bounds The lower left and upper right corner of the rectangle
344
    @param {LeafletObject} properties The Leaflet properties of the rectangle
345
    */
346
347
    addRectangle ( objId, bounds, properties ) {
348
        this.addToMap (
349
            objId,
350
            new LeafletRectangle ( bounds, properties )
351
        );
352
    }
353
354
    /**
355
    This method changes the background map.
356
    This method is called by the 'layerchange' event listener.
357
    @param {MapLayer} layer The layer to set
358
    */
359
360
    setLayer ( layer ) {
361
        const url = theApiKeysManager.getUrl ( layer );
362
        if ( ! url ) {
363
            return;
364
        }
365
366
        super.setLayer ( layer, url );
367
    }
368
}
369
370
/* ------------------------------------------------------------------------------------------------------------------------- */
371
/**
372
The one and only one instance of MapEditor class
373
@type {MapEditor}
374
*/
375
/* ------------------------------------------------------------------------------------------------------------------------- */
376
377
const theMapEditor = new MapEditor ( );
378
379
export default theMapEditor;
380
381
/* --- End of file --------------------------------------------------------------------------------------------------------- */
382