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