1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
22 | |
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 | |
56 | |
57 | |
58 | |
59 | |
60 | |
61 | class MapEditorViewer { |
62 | |
63 | |
64 | |
65 | |
66 | |
67 | |
68 | #currentLayer; |
69 | |
70 | |
71 | |
72 | |
73 | |
74 | |
75 | #geolocationCircle; |
76 | |
77 | |
78 | |
79 | |
80 | |
81 | |
82 | |
83 | static get #DEFAULT_MAX_ZOOM ( ) { return 18; } |
84 | |
85 | |
86 | |
87 | |
88 | |
89 | |
90 | |
91 | static get #DEFAULT_MIN_ZOOM ( ) { return 0; } |
92 | |
93 | |
94 | |
95 | |
96 | |
97 | |
98 | |
99 | static get #NOTE_Z_INDEX_OFFSET ( ) { return 100; } |
100 | |
101 | |
102 | |
103 | |
104 | |
105 | constructor ( ) { |
106 | Object.freeze ( this ); |
107 | this.#currentLayer = null; |
108 | this.#geolocationCircle = null; |
109 | } |
110 | |
111 | |
112 | |
113 | |
114 | |
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 | |
125 | |
126 | |
127 | |
128 | |
129 | |
130 | |
131 | addRoute ( routeObjId ) { |
132 | const route = theDataSearchEngine.getRoute ( routeObjId ); |
133 | |
134 | |
135 | const latLng = []; |
136 | const itineraryPointsIterator = route.itinerary.itineraryPoints.iterator; |
137 | while ( ! itineraryPointsIterator.done ) { |
138 | latLng.push ( itineraryPointsIterator.value.latLng ); |
139 | } |
140 | |
141 | |
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 | |
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 | |
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 | |
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 | |
188 | |
189 | |
190 | |
191 | |
192 | |
193 | |
194 | addNote ( noteObjId ) { |
195 | const note = theDataSearchEngine.getNoteAndRoute ( noteObjId ).note; |
196 | |
197 | |
198 | |
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 | |
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 | |
236 | marker.bindPopup ( |
237 | layer => theHTMLSanitizer.clone ( |
238 | theNoteHTMLViewsFactory.getNoteTextHTML ( |
239 | 'TravelNotes-Map-', |
240 | theDataSearchEngine.getNoteAndRoute ( layer.objId ) |
241 | ) |
242 | ) |
243 | ); |
244 | |
245 | |
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 | |
254 | const polyline = new LeafletPolyline ( [ note.latLng, note.iconLatLng ], theConfig.note.polyline ); |
255 | polyline.objId = note.objId; |
256 | |
257 | |
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 | |
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 | |
276 | |
277 | |
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 | |
293 | |
294 | |
295 | |
296 | |
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 | |
315 | |
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 | |
345 | |
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 | |
360 | |
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 | |
382 | |
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 | |
444 | |