App.js (14481B)
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 .addTo(map); 187 enableLayersControlHotKeys(layersControl); 188 enableLayersControlAdaptiveHeight(layersControl); 189 enableLayersMinimize(layersControl); 190 enableLayersConfig(layersControl, getLayers()); 191 layersControl.enableHashState('l'); 192 193 /* controls bottom-left corner */ 194 195 const attribution = L.control.attribution({ 196 position: 'bottomleft', 197 prefix: false, 198 }); 199 map.on('resize', function() { 200 if (map.getSize().y > 567) { 201 map.addControl(attribution); 202 // Hack to keep control at the bottom of the map 203 const container = attribution._container; 204 const parent = container.parentElement; 205 parent.appendChild(container); 206 } else { 207 map.removeControl(attribution); 208 } 209 }); 210 if (map.getSize().y > 567) { 211 map.addControl(attribution); 212 } 213 214 const printControl = new L.Control.PrintPages({position: 'bottomleft'}) 215 .addTo(map) 216 .enableHashState('p'); 217 if ( 218 minimizeControls.print === minimizeStateMinimized || 219 (minimizeControls.print === minimizeStateAuto && !printControl.hasPages()) 220 ) { 221 printControl.setMinimized(); 222 } 223 224 const jnxControl = new L.Control.JNX(layersControl, {position: 'bottomleft'}) 225 .addTo(map) 226 .enableHashState('j'); 227 228 /* controls bottom-right corner */ 229 230 tracklist.addTo(map); 231 const tracksHashParams = tracklist.hashParams(); 232 233 let hasTrackParamsInHash = false; 234 for (let param of tracksHashParams) { 235 if (hashState.hasKey(param)) { 236 hasTrackParamsInHash = true; 237 break; 238 } 239 } 240 241 if (sessionsControl) { 242 (async() => { 243 await sessionsControl.loadSession(); 244 await sessionsControl.consumeSessionFromHash(); 245 if (await sessionsControl.importOldSessions() && !hasTrackParamsInHash) { 246 notify( 247 'If some tracks disappeared from the tracks list, ' + 248 'you can find them in the new list of recent sessions in the upper left corner.' 249 ); 250 } 251 })(); 252 } 253 254 if (hashState.hasKey('autoprofile') && hasTrackParamsInHash) { 255 tracklist.once('loadedTracksFromParam', () => { 256 const track = tracklist.tracks()[0]; 257 if (track) { 258 tracklist.showElevationProfileForTrack(track); 259 } 260 }); 261 } 262 263 // This is not quite correct: minimizeControls should have effect only during loading, but the way it is 264 // implemented, it will affect expanding when loading track from hash param during session. 265 // But as parameter is expected to be found only when site is embedded using iframe, 266 // the latter scenario is not very probable. 267 if (minimizeControls.tracks !== minimizeStateMinimized) { 268 tracklist.on('loadedTracksFromParam', () => tracklist.setExpanded()); 269 } 270 271 for (let param of tracksHashParams) { 272 bindHashStateReadOnly(param, tracklist.loadTrackFromParam.bind(tracklist, param)); 273 } 274 275 /* set map position */ 276 277 if (!validPositionInHash) { 278 if (hasTrackParamsInHash) { 279 tracklist.whenLoadDone(() => tracklist.setViewToAllTracks(true)); 280 } else { 281 locateControl.moveMapToCurrentLocation(config.defaultZoom); 282 } 283 } 284 285 /* adaptive layout */ 286 287 if ( 288 minimizeControls.layers === minimizeStateAuto && L.Browser.mobile || 289 minimizeControls.layers === minimizeStateMinimized 290 ) { 291 layersControl.setMinimized(); 292 } 293 294 if (L.Browser.mobile) { 295 map.on('mousedown dragstart', () => layersControl.setMinimized()); 296 } 297 298 if ( 299 minimizeControls.tracks === minimizeStateAuto && L.Browser.mobile && !tracklist.hasTracks() || 300 minimizeControls.tracks === minimizeStateMinimized 301 ) { 302 tracklist.setMinimized(); 303 } 304 305 raiseControlsOnFocus(map); 306 307 /* track list and azimuth measure interaction */ 308 309 tracklist.on('startedit', () => azimuthControl.disableControl()); 310 tracklist.on('elevation-shown', () => azimuthControl.hideProfile()); 311 azimuthControl.on('enabled', () => { 312 tracklist.stopEditLine(); 313 }); 314 azimuthControl.on('elevation-shown', () => tracklist.hideElevationProfile()); 315 316 /* setup events logging */ 317 318 function getLayerLoggingInfo(layer) { 319 if (layer.meta) { 320 return {title: layer.meta.title}; 321 } else if (layer.__customLayer) { 322 return {custom: true, title: layer.__customLayer.title, url: layer._url}; 323 } 324 return null; 325 } 326 327 function getLatLngBoundsLoggingInfo(latLngBounds) { 328 return { 329 west: latLngBounds.getWest(), 330 south: latLngBounds.getSouth(), 331 east: latLngBounds.getEast(), 332 north: latLngBounds.getNorth(), 333 }; 334 } 335 336 function getErrorLoggingInfo(error) { 337 return error 338 ? { 339 name: error.name, 340 message: error.message, 341 stack: error.stack, 342 } 343 : null; 344 } 345 346 function logUsedMaps() { 347 const layers = []; 348 map.eachLayer((layer) => { 349 const layerInfo = getLayerLoggingInfo(layer); 350 if (layerInfo) { 351 layers.push(layerInfo); 352 } 353 }); 354 const bounds = map.getBounds(); 355 logging.logEvent('activeLayers', { 356 layers, 357 view: getLatLngBoundsLoggingInfo(bounds), 358 }); 359 } 360 361 L.DomEvent.on(document, 'mousemove click touchend', L.Util.throttle(logUsedMaps, 30000)); 362 363 printControl.on('mapRenderEnd', function(e) { 364 logging.logEvent('mapRenderEnd', { 365 eventId: e.eventId, 366 success: e.success, 367 error: getErrorLoggingInfo(e.error), 368 }); 369 }); 370 371 printControl.on('mapRenderStart', function(e) { 372 const layers = []; 373 map.eachLayer((layer) => { 374 const layerInfo = getLayerLoggingInfo(layer); 375 if (layer.options?.print && layerInfo) { 376 layers.push({ 377 ...getLayerLoggingInfo(layer), 378 scaleDependent: layer.options.scaleDependent 379 }); 380 } 381 }); 382 logging.logEvent('mapRenderStart', { 383 eventId: e.eventId, 384 action: e.action, 385 scale: e.scale, 386 resolution: e.resolution, 387 pages: e.pages.map((page) => getLatLngBoundsLoggingInfo(page.latLngBounds)), 388 zooms: e.zooms, 389 layers 390 }); 391 }); 392 393 jnxControl.on('tileExportStart', function(e) { 394 logging.logEvent('tileExportStart', { 395 eventId: e.eventId, 396 layer: getLayerLoggingInfo(e.layer), 397 zoom: e.zoom, 398 bounds: getLatLngBoundsLoggingInfo(e.bounds), 399 }); 400 }); 401 402 jnxControl.on('tileExportEnd', function(e) { 403 logging.logEvent('tileExportEnd', { 404 eventId: e.eventId, 405 success: e.success, 406 error: getErrorLoggingInfo(e.error), 407 }); 408 }); 409 410 searchControl.on('resultreceived', function(e) { 411 logging.logEvent('SearchProviderSelected', { 412 provider: e.provider, 413 query: e.query, 414 }); 415 if (e.provider === 'Links' && e.result.error) { 416 logging.logEvent('SearchLinkError', { 417 query: e.query, 418 result: e.result, 419 }); 420 } 421 if (e.provider === 'Coordinates') { 422 logging.logEvent('SearchCoordinates', { 423 query: e.query, 424 result: e.result, 425 }); 426 } 427 }); 428 429 logging.logEvent('start', startInfo); 430 logUsedMaps(); 431 } 432 433 export {setUp};