App.js (14633B)
1 import './App.css'; 2 import './leaflet-fixes.css'; 3 import L from 'leaflet'; 4 import 'leaflet/dist/leaflet.css'; 5 import {MapWithSidebars} from '~/lib/leaflet.map.sidebars'; 6 import '~/lib/leaflet.control.printPages/control'; 7 import '~/lib/leaflet.control.caption'; 8 import config from './config'; 9 import '~/lib/leaflet.control.coordinates'; 10 import enableLayersControlHotkeys from '~/lib/leaflet.control.layers.hotkeys'; 11 import '~/lib/leaflet.hashState/Leaflet.Map'; 12 import '~/lib/leaflet.hashState/Leaflet.Control.Layers'; 13 import {fixAll} from '~/lib/leaflet.fixes'; 14 import './adaptive.css'; 15 import '~/lib/leaflet.control.panoramas'; 16 import '~/lib/leaflet.control.track-list/track-list'; 17 import '~/lib/leaflet.control.track-list/control-ruler'; 18 import '~/lib/leaflet.control.track-list/track-list.hash-state'; 19 import enableLayersControlAdaptiveHeight from '~/lib/leaflet.control.layers.adaptive-height'; 20 import enableLayersMinimize from '~/lib/leaflet.control.layers.minimize'; 21 import enableLayersConfig from '~/lib/leaflet.control.layers.configure'; 22 import raiseControlsOnFocus from '~/lib/leaflet.controls.raise-on-focus'; 23 import {getLayers} from './layers'; 24 import '~/lib/leaflet.control.layers.events'; 25 import '~/lib/leaflet.control.jnx'; 26 import '~/lib/leaflet.control.jnx/hash-state'; 27 import '~/lib/leaflet.control.azimuth'; 28 import {SessionsControl} from '~/lib/leaflet.control.sessions'; 29 import {hashState, bindHashStateReadOnly} from '~/lib/leaflet.hashState/hashState'; 30 import {LocateControl} from '~/lib/leaflet.control.locate'; 31 import {notify} from '~/lib/notifications'; 32 import ZoomDisplay from '~/lib/leaflet.control.zoom-display'; 33 import * as logging from '~/lib/logging'; 34 import safeLocalStorage from '~/lib/safe-localstorage'; 35 import {ExternalMaps} from '~/lib/leaflet.control.external-maps'; 36 import {SearchControl} from '~/lib/leaflet.control.search'; 37 import '~/lib/leaflet.placemark'; 38 import '~/vendored/mapbbcode/FunctionButton'; 39 import Contextmenu from '~/lib/contextmenu'; 40 import iconMenu from './images/menu.png'; 41 42 const locationErrorMessage = { 43 0: 'Your browser does not support geolocation.', 44 1: 'Geolocation is blocked for this site. Please, enable in browser setting.', 45 2: 'Failed to acquire position for unknown reason.', 46 }; 47 48 const minimizeStateAuto = 0; 49 const minimizeStateMinimized = 1; 50 const minimizeStateExpanded = 2; 51 52 function isInIframe() { 53 // Check if the window is not the top window 54 return window.self !== window.top; 55 } 56 57 function setUp() { // eslint-disable-line complexity 58 const startInfo = { 59 href: window.location.href, 60 localStorageKeys: Object.keys(safeLocalStorage), 61 mobile: L.Browser.mobile, 62 }; 63 fixAll(); 64 65 function validateMinimizeState(state) { 66 state = Number(state); 67 if (state === minimizeStateMinimized || state === minimizeStateExpanded) { 68 return state; 69 } 70 return minimizeStateAuto; 71 } 72 const minimizeState = hashState.getState('min') ?? []; 73 const minimizeControls = { 74 tracks: validateMinimizeState(minimizeState[0]), 75 layers: validateMinimizeState(minimizeState[1]), 76 print: validateMinimizeState(minimizeState[2]), 77 search: validateMinimizeState(minimizeState[3]), 78 }; 79 80 const map = new MapWithSidebars('map', { 81 zoomControl: false, 82 fadeAnimation: false, 83 attributionControl: false, 84 inertiaMaxSpeed: 1500, 85 worldCopyJump: true, 86 maxZoom: 18 87 } 88 ); 89 90 const tracklist = new L.Control.TrackList({ 91 keysToExcludeOnCopyLink: ['q', 'r'] 92 }); 93 94 /* controls top-left corner */ 95 96 new L.Control.Caption(config.caption, { 97 position: 'topleft' 98 } 99 ).addTo(map); 100 101 new ZoomDisplay().addTo(map); 102 103 const searchOptions = { 104 position: 'topleft', 105 stackHorizontally: true, 106 maxMapWidthToMinimize: 620, 107 }; 108 if (minimizeControls.search === minimizeStateMinimized) { 109 searchOptions.maxMapHeightToMinimize = Infinity; 110 searchOptions.maxMapWidthToMinimize = Infinity; 111 } else if (minimizeControls.search === minimizeStateExpanded) { 112 searchOptions.maxMapHeightToMinimize = 0; 113 searchOptions.maxMapWidthToMinimize = 0; 114 } 115 const searchControl = new SearchControl(searchOptions) 116 .addTo(map) 117 .enableHashState('q'); 118 map.getPlacemarkHashStateInterface().enableHashState('r'); 119 120 new L.Control.Scale({ 121 imperial: false, 122 position: 'topleft', 123 stackHorizontally: true 124 }).addTo(map); 125 126 let sessionsControl; 127 if (!isInIframe()) { 128 sessionsControl = new SessionsControl(tracklist, {noButton: true}).addTo(map); 129 const size = L.Browser.touch ? 30 : 26; 130 const offset = L.Browser.touch ? 7 : 5; 131 const menuButton = L.functionButtons( 132 [ 133 { 134 content: iconMenu, 135 title: 'Menu', 136 bgPos: [-offset, -offset], 137 imageSize: size, 138 }, 139 ], 140 {position: 'topleft'} 141 ); 142 menuButton.addTo(map); 143 menuButton.on('clicked', (e) => { 144 new Contextmenu([ 145 { 146 text: 'Recent sessions', 147 callback: () => sessionsControl.showSessionListWindow(), 148 }, 149 { 150 text: 'Copy share link', 151 callback: () => tracklist.copyAllTracksToClipboard(e, true), 152 }, 153 ]).show(e); 154 }); 155 } 156 157 new ExternalMaps({position: 'topleft'}).addTo(map); 158 159 new L.Control.TrackList.Ruler(tracklist).addTo(map); 160 161 const panoramas = new L.Control.Panoramas() 162 .addTo(map) 163 .enableHashState('n2'); 164 L.Control.Panoramas.hashStateUpgrader(panoramas).enableHashState('n'); 165 166 new L.Control.Coordinates(config.elevationTileUrl, {position: 'topleft'}).addTo(map); 167 168 const azimuthControl = new L.Control.Azimuth({position: 'topleft'}).addTo(map); 169 170 const locateControl = new LocateControl({ 171 position: 'topleft', 172 showError: function({code, message}) { 173 let customMessage = locationErrorMessage[code]; 174 if (!customMessage) { 175 customMessage = `Geolocation error: ${message}`; 176 } 177 notify(customMessage); 178 } 179 }).addTo(map); 180 let {valid: validPositionInHash} = map.validateState(hashState.getState('m')); 181 map.enableHashState('m', [config.defaultZoom, ...config.defaultLocation]); 182 183 /* controls top-right corner */ 184 185 const layersControl = L.control.layers(null, null, {collapsed: false}); 186 const areHotkeysEnabled = !L.Browser.touch || !L.Browser.mobile; 187 if (areHotkeysEnabled) { 188 enableLayersControlHotkeys(layersControl); 189 } 190 enableLayersControlAdaptiveHeight(layersControl); 191 enableLayersMinimize(layersControl); 192 enableLayersConfig(layersControl, getLayers(), {withHotkeys: areHotkeysEnabled}); 193 layersControl.addTo(map); 194 layersControl.enableHashState('l'); 195 196 /* controls bottom-left corner */ 197 198 const attribution = L.control.attribution({ 199 position: 'bottomleft', 200 prefix: false, 201 }); 202 map.on('resize', function() { 203 if (map.getSize().y > 567) { 204 map.addControl(attribution); 205 // Hack to keep control at the bottom of the map 206 const container = attribution._container; 207 const parent = container.parentElement; 208 parent.appendChild(container); 209 } else { 210 map.removeControl(attribution); 211 } 212 }); 213 if (map.getSize().y > 567) { 214 map.addControl(attribution); 215 } 216 217 const printControl = new L.Control.PrintPages({position: 'bottomleft'}) 218 .addTo(map) 219 .enableHashState('p'); 220 if ( 221 minimizeControls.print === minimizeStateMinimized || 222 (minimizeControls.print === minimizeStateAuto && !printControl.hasPages()) 223 ) { 224 printControl.setMinimized(); 225 } 226 227 const jnxControl = new L.Control.JNX(layersControl, {position: 'bottomleft'}) 228 .addTo(map) 229 .enableHashState('j'); 230 231 /* controls bottom-right corner */ 232 233 tracklist.addTo(map); 234 const tracksHashParams = tracklist.hashParams(); 235 236 let hasTrackParamsInHash = false; 237 for (let param of tracksHashParams) { 238 if (hashState.hasKey(param)) { 239 hasTrackParamsInHash = true; 240 break; 241 } 242 } 243 244 if (sessionsControl) { 245 (async() => { 246 await sessionsControl.loadSession(); 247 await sessionsControl.consumeSessionFromHash(); 248 if (await sessionsControl.importOldSessions() && !hasTrackParamsInHash) { 249 notify( 250 'If some tracks disappeared from the tracks list, ' + 251 'you can find them in the new list of recent sessions in the upper left corner.' 252 ); 253 } 254 })(); 255 } 256 257 if (hashState.hasKey('autoprofile') && hasTrackParamsInHash) { 258 tracklist.once('loadedTracksFromParam', () => { 259 const track = tracklist.tracks()[0]; 260 if (track) { 261 tracklist.showElevationProfileForTrack(track); 262 } 263 }); 264 } 265 266 // This is not quite correct: minimizeControls should have effect only during loading, but the way it is 267 // implemented, it will affect expanding when loading track from hash param during session. 268 // But as parameter is expected to be found only when site is embedded using iframe, 269 // the latter scenario is not very probable. 270 if (minimizeControls.tracks !== minimizeStateMinimized) { 271 tracklist.on('loadedTracksFromParam', () => tracklist.setExpanded()); 272 } 273 274 for (let param of tracksHashParams) { 275 bindHashStateReadOnly(param, tracklist.loadTrackFromParam.bind(tracklist, param)); 276 } 277 278 /* set map position */ 279 280 if (!validPositionInHash) { 281 if (hasTrackParamsInHash) { 282 tracklist.whenLoadDone(() => tracklist.setViewToAllTracks(true)); 283 } else { 284 locateControl.moveMapToCurrentLocation(config.defaultZoom); 285 } 286 } 287 288 /* adaptive layout */ 289 290 if ( 291 minimizeControls.layers === minimizeStateAuto && L.Browser.mobile || 292 minimizeControls.layers === minimizeStateMinimized 293 ) { 294 layersControl.setMinimized(); 295 } 296 297 if (L.Browser.mobile) { 298 map.on('mousedown dragstart', () => layersControl.setMinimized()); 299 } 300 301 if ( 302 minimizeControls.tracks === minimizeStateAuto && L.Browser.mobile && !tracklist.hasTracks() || 303 minimizeControls.tracks === minimizeStateMinimized 304 ) { 305 tracklist.setMinimized(); 306 } 307 308 raiseControlsOnFocus(map); 309 310 /* track list and azimuth measure interaction */ 311 312 tracklist.on('startedit', () => azimuthControl.disableControl()); 313 tracklist.on('elevation-shown', () => azimuthControl.hideProfile()); 314 azimuthControl.on('enabled', () => { 315 tracklist.stopEditLine(); 316 }); 317 azimuthControl.on('elevation-shown', () => tracklist.hideElevationProfile()); 318 319 /* setup events logging */ 320 321 function getLayerLoggingInfo(layer) { 322 if (layer.meta) { 323 return {title: layer.meta.title}; 324 } else if (layer.__customLayer) { 325 return {custom: true, title: layer.__customLayer.title, url: layer._url}; 326 } 327 return null; 328 } 329 330 function getLatLngBoundsLoggingInfo(latLngBounds) { 331 return { 332 west: latLngBounds.getWest(), 333 south: latLngBounds.getSouth(), 334 east: latLngBounds.getEast(), 335 north: latLngBounds.getNorth(), 336 }; 337 } 338 339 function getErrorLoggingInfo(error) { 340 return error 341 ? { 342 name: error.name, 343 message: error.message, 344 stack: error.stack, 345 } 346 : null; 347 } 348 349 function logUsedMaps() { 350 const layers = []; 351 map.eachLayer((layer) => { 352 const layerInfo = getLayerLoggingInfo(layer); 353 if (layerInfo) { 354 layers.push(layerInfo); 355 } 356 }); 357 const bounds = map.getBounds(); 358 logging.logEvent('activeLayers', { 359 layers, 360 view: getLatLngBoundsLoggingInfo(bounds), 361 }); 362 } 363 364 L.DomEvent.on(document, 'mousemove click touchend', L.Util.throttle(logUsedMaps, 30000)); 365 366 printControl.on('mapRenderEnd', function(e) { 367 logging.logEvent('mapRenderEnd', { 368 eventId: e.eventId, 369 success: e.success, 370 error: getErrorLoggingInfo(e.error), 371 }); 372 }); 373 374 printControl.on('mapRenderStart', function(e) { 375 const layers = []; 376 map.eachLayer((layer) => { 377 const layerInfo = getLayerLoggingInfo(layer); 378 if (layer.options?.print && layerInfo) { 379 layers.push({ 380 ...getLayerLoggingInfo(layer), 381 scaleDependent: layer.options.scaleDependent 382 }); 383 } 384 }); 385 logging.logEvent('mapRenderStart', { 386 eventId: e.eventId, 387 action: e.action, 388 scale: e.scale, 389 resolution: e.resolution, 390 pages: e.pages.map((page) => getLatLngBoundsLoggingInfo(page.latLngBounds)), 391 zooms: e.zooms, 392 layers 393 }); 394 }); 395 396 jnxControl.on('tileExportStart', function(e) { 397 logging.logEvent('tileExportStart', { 398 eventId: e.eventId, 399 layer: getLayerLoggingInfo(e.layer), 400 zoom: e.zoom, 401 bounds: getLatLngBoundsLoggingInfo(e.bounds), 402 }); 403 }); 404 405 jnxControl.on('tileExportEnd', function(e) { 406 logging.logEvent('tileExportEnd', { 407 eventId: e.eventId, 408 success: e.success, 409 error: getErrorLoggingInfo(e.error), 410 }); 411 }); 412 413 searchControl.on('resultreceived', function(e) { 414 logging.logEvent('SearchProviderSelected', { 415 provider: e.provider, 416 query: e.query, 417 }); 418 if (e.provider === 'Links' && e.result.error) { 419 logging.logEvent('SearchLinkError', { 420 query: e.query, 421 result: e.result, 422 }); 423 } 424 if (e.provider === 'Coordinates') { 425 logging.logEvent('SearchCoordinates', { 426 query: e.query, 427 result: e.result, 428 }); 429 } 430 }); 431 432 logging.logEvent('start', startInfo); 433 logUsedMaps(); 434 } 435 436 export {setUp};
