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