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