1(function (global, factory) { 2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/control/Control'), require('ol/Observable'), require('ol/layer/Group')) : 3 typeof define === 'function' && define.amd ? define(['ol/control/Control', 'ol/Observable', 'ol/layer/Group'], factory) : 4 (global.LayerSwitcher = factory(global.ol.control.Control,global.ol.Observable,global.ol.layer.Group)); 5}(this, (function (Control,ol_Observable,LayerGroup) { 'use strict'; 6 7Control = 'default' in Control ? Control['default'] : Control; 8LayerGroup = 'default' in LayerGroup ? LayerGroup['default'] : LayerGroup; 9 10/** 11 * @protected 12 */ 13const CSS_PREFIX = 'layer-switcher-'; 14/** 15 * OpenLayers LayerSwitcher Control, displays a list of layers and groups 16 * associated with a map which have a `title` property. 17 * 18 * To be shown in the LayerSwitcher panel layers must have a `title` property; 19 * base map layers should have a `type` property set to `base`. Group layers 20 * (`LayerGroup`) can be used to visually group layers together; a group 21 * with a `fold` property set to either `'open'` or `'close'` will be displayed 22 * with a toggle. 23 * 24 * See [BaseLayerOptions](#baselayeroptions) for a full list of LayerSwitcher 25 * properties for layers (`TileLayer`, `ImageLayer`, `VectorTile` etc.) and 26 * [GroupLayerOptions](#grouplayeroptions) for group layer (`LayerGroup`) 27 * LayerSwitcher properties. 28 * 29 * Layer and group properties can either be set by adding extra properties 30 * to their options when they are created or via their set method. 31 * 32 * Specify a `title` for a Layer by adding a `title` property to it's options object: 33 * ```javascript 34 * var lyr = new ol.layer.Tile({ 35 * // Specify a title property which will be displayed by the layer switcher 36 * title: 'OpenStreetMap', 37 * visible: true, 38 * source: new ol.source.OSM() 39 * }) 40 * ``` 41 * 42 * Alternatively the properties can be set via the `set` method after a layer has been created: 43 * ```javascript 44 * var lyr = new ol.layer.Tile({ 45 * visible: true, 46 * source: new ol.source.OSM() 47 * }) 48 * // Specify a title property which will be displayed by the layer switcher 49 * lyr.set('title', 'OpenStreetMap'); 50 * ``` 51 * 52 * To create a LayerSwitcher and add it to a map, create a new instance then pass it to the map's [`addControl` method](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html#addControl). 53 * ```javascript 54 * var layerSwitcher = new LayerSwitcher({ 55 * reverse: true, 56 * groupSelectStyle: 'group' 57 * }); 58 * map.addControl(layerSwitcher); 59 * ``` 60 * 61 * @constructor 62 * @extends {ol/control/Control~Control} 63 * @param opt_options LayerSwitcher options, see [LayerSwitcher Options](#options) and [RenderOptions](#renderoptions) which LayerSwitcher `Options` extends for more details. 64 */ 65class LayerSwitcher extends Control { 66 constructor(opt_options) { 67 const options = Object.assign({}, opt_options); 68 const element = document.createElement('div'); 69 super({ element: element, target: options.target }); 70 this.activationMode = options.activationMode || 'mouseover'; 71 this.startActive = options.startActive === true; 72 // TODO Next: Rename to showButtonContent 73 this.label = options.label !== undefined ? options.label : ''; 74 // TODO Next: Rename to hideButtonContent 75 this.collapseLabel = 76 options.collapseLabel !== undefined ? options.collapseLabel : '\u00BB'; 77 // TODO Next: Rename to showButtonTitle 78 this.tipLabel = options.tipLabel ? options.tipLabel : 'Legend'; 79 // TODO Next: Rename to hideButtonTitle 80 this.collapseTipLabel = options.collapseTipLabel 81 ? options.collapseTipLabel 82 : 'Collapse legend'; 83 this.groupSelectStyle = LayerSwitcher.getGroupSelectStyle(options.groupSelectStyle); 84 this.reverse = options.reverse !== false; 85 this.mapListeners = []; 86 this.hiddenClassName = 'ol-unselectable ol-control layer-switcher'; 87 if (LayerSwitcher.isTouchDevice_()) { 88 this.hiddenClassName += ' touch'; 89 } 90 this.shownClassName = 'shown'; 91 element.className = this.hiddenClassName; 92 this.button = document.createElement('button'); 93 element.appendChild(this.button); 94 this.panel = document.createElement('div'); 95 this.panel.className = 'panel'; 96 element.appendChild(this.panel); 97 LayerSwitcher.enableTouchScroll_(this.panel); 98 element.classList.add(CSS_PREFIX + 'group-select-style-' + this.groupSelectStyle); 99 element.classList.add(CSS_PREFIX + 'activation-mode-' + this.activationMode); 100 if (this.activationMode === 'click') { 101 // TODO Next: Remove in favour of layer-switcher-activation-mode-click 102 element.classList.add('activationModeClick'); 103 this.button.onclick = (e) => { 104 const evt = e || window.event; 105 if (this.element.classList.contains(this.shownClassName)) { 106 this.hidePanel(); 107 } 108 else { 109 this.showPanel(); 110 } 111 evt.preventDefault(); 112 }; 113 } 114 else { 115 this.button.onmouseover = () => { 116 this.showPanel(); 117 }; 118 this.button.onclick = (e) => { 119 const evt = e || window.event; 120 this.showPanel(); 121 evt.preventDefault(); 122 }; 123 this.panel.onmouseout = (evt) => { 124 if (!this.panel.contains(evt.relatedTarget)) { 125 this.hidePanel(); 126 } 127 }; 128 } 129 this.updateButton(); 130 } 131 /** 132 * Set the map instance the control is associated with. 133 * @param map The map instance. 134 */ 135 setMap(map) { 136 // Clean up listeners associated with the previous map 137 for (let i = 0; i < this.mapListeners.length; i++) { 138 ol_Observable.unByKey(this.mapListeners[i]); 139 } 140 this.mapListeners.length = 0; 141 // Wire up listeners etc. and store reference to new map 142 super.setMap(map); 143 if (map) { 144 if (this.startActive) { 145 this.showPanel(); 146 } 147 else { 148 this.renderPanel(); 149 } 150 if (this.activationMode !== 'click') { 151 this.mapListeners.push(map.on('pointerdown', () => { 152 this.hidePanel(); 153 })); 154 } 155 } 156 } 157 /** 158 * Show the layer panel. Fires `'show'` event. 159 * @fires LayerSwitcher#show 160 */ 161 showPanel() { 162 if (!this.element.classList.contains(this.shownClassName)) { 163 this.element.classList.add(this.shownClassName); 164 this.updateButton(); 165 this.renderPanel(); 166 } 167 /** 168 * Event triggered after the panel has been shown. 169 * Listen to the event via the `on` or `once` methods; for example: 170 * ```js 171 * var layerSwitcher = new LayerSwitcher(); 172 * map.addControl(layerSwitcher); 173 * 174 * layerSwitcher.on('show', evt => { 175 * console.log('show', evt); 176 * }); 177 * @event LayerSwitcher#show 178 */ 179 this.dispatchEvent('show'); 180 } 181 /** 182 * Hide the layer panel. Fires `'hide'` event. 183 * @fires LayerSwitcher#hide 184 */ 185 hidePanel() { 186 if (this.element.classList.contains(this.shownClassName)) { 187 this.element.classList.remove(this.shownClassName); 188 this.updateButton(); 189 } 190 /** 191 * Event triggered after the panel has been hidden. 192 * @event LayerSwitcher#hide 193 */ 194 this.dispatchEvent('hide'); 195 } 196 /** 197 * Update button text content and attributes based on current 198 * state 199 */ 200 updateButton() { 201 if (this.element.classList.contains(this.shownClassName)) { 202 this.button.textContent = this.collapseLabel; 203 this.button.setAttribute('title', this.collapseTipLabel); 204 this.button.setAttribute('aria-label', this.collapseTipLabel); 205 } 206 else { 207 this.button.textContent = this.label; 208 this.button.setAttribute('title', this.tipLabel); 209 this.button.setAttribute('aria-label', this.tipLabel); 210 } 211 } 212 /** 213 * Re-draw the layer panel to represent the current state of the layers. 214 */ 215 renderPanel() { 216 this.dispatchEvent('render'); 217 LayerSwitcher.renderPanel(this.getMap(), this.panel, { 218 groupSelectStyle: this.groupSelectStyle, 219 reverse: this.reverse 220 }); 221 this.dispatchEvent('rendercomplete'); 222 } 223 /** 224 * **_[static]_** - Re-draw the layer panel to represent the current state of the layers. 225 * @param map The OpenLayers Map instance to render layers for 226 * @param panel The DOM Element into which the layer tree will be rendered 227 * @param options Options for panel, group, and layers 228 */ 229 static renderPanel(map, panel, options) { 230 // Create the event. 231 const render_event = new Event('render'); 232 // Dispatch the event. 233 panel.dispatchEvent(render_event); 234 options = options || {}; 235 options.groupSelectStyle = LayerSwitcher.getGroupSelectStyle(options.groupSelectStyle); 236 LayerSwitcher.ensureTopVisibleBaseLayerShown(map, options.groupSelectStyle); 237 while (panel.firstChild) { 238 panel.removeChild(panel.firstChild); 239 } 240 // Reset indeterminate state for all layers and groups before 241 // applying based on groupSelectStyle 242 LayerSwitcher.forEachRecursive(map, function (l, _idx, _a) { 243 l.set('indeterminate', false); 244 }); 245 if (options.groupSelectStyle === 'children' || 246 options.groupSelectStyle === 'none') { 247 // Set visibile and indeterminate state of groups based on 248 // their children's visibility 249 LayerSwitcher.setGroupVisibility(map); 250 } 251 else if (options.groupSelectStyle === 'group') { 252 // Set child indetermiate state based on their parent's visibility 253 LayerSwitcher.setChildVisibility(map); 254 } 255 const ul = document.createElement('ul'); 256 panel.appendChild(ul); 257 // passing two map arguments instead of lyr as we're passing the map as the root of the layers tree 258 LayerSwitcher.renderLayers_(map, map, ul, options, function render(_changedLyr) { 259 LayerSwitcher.renderPanel(map, panel, options); 260 }); 261 // Create the event. 262 const rendercomplete_event = new Event('rendercomplete'); 263 // Dispatch the event. 264 panel.dispatchEvent(rendercomplete_event); 265 } 266 /** 267 * **_[static]_** - Determine if a given layer group contains base layers 268 * @param grp Group to test 269 */ 270 static isBaseGroup(grp) { 271 if (grp instanceof LayerGroup) { 272 const lyrs = grp.getLayers().getArray(); 273 return lyrs.length && lyrs[0].get('type') === 'base'; 274 } 275 else { 276 return false; 277 } 278 } 279 static setGroupVisibility(map) { 280 // Get a list of groups, with the deepest first 281 const groups = LayerSwitcher.getGroupsAndLayers(map, function (l) { 282 return (l instanceof LayerGroup && 283 !l.get('combine') && 284 !LayerSwitcher.isBaseGroup(l)); 285 }).reverse(); 286 // console.log(groups.map(g => g.get('title'))); 287 groups.forEach(function (grp) { 288 // TODO Can we use getLayersArray, is it public in the esm build? 289 const descendantVisibility = grp.getLayersArray().map(function (l) { 290 const state = l.getVisible(); 291 // console.log('>', l.get('title'), state); 292 return state; 293 }); 294 // console.log(descendantVisibility); 295 if (descendantVisibility.every(function (v) { 296 return v === true; 297 })) { 298 grp.setVisible(true); 299 grp.set('indeterminate', false); 300 } 301 else if (descendantVisibility.every(function (v) { 302 return v === false; 303 })) { 304 grp.setVisible(false); 305 grp.set('indeterminate', false); 306 } 307 else { 308 grp.setVisible(true); 309 grp.set('indeterminate', true); 310 } 311 }); 312 } 313 static setChildVisibility(map) { 314 // console.log('setChildVisibility'); 315 const groups = LayerSwitcher.getGroupsAndLayers(map, function (l) { 316 return (l instanceof LayerGroup && 317 !l.get('combine') && 318 !LayerSwitcher.isBaseGroup(l)); 319 }); 320 groups.forEach(function (grp) { 321 const group = grp; 322 // console.log(group.get('title')); 323 const groupVisible = group.getVisible(); 324 const groupIndeterminate = group.get('indeterminate'); 325 group 326 .getLayers() 327 .getArray() 328 .forEach(function (l) { 329 l.set('indeterminate', false); 330 if ((!groupVisible || groupIndeterminate) && l.getVisible()) { 331 l.set('indeterminate', true); 332 } 333 }); 334 }); 335 } 336 /** 337 * Ensure only the top-most base layer is visible if more than one is visible. 338 * @param map The map instance. 339 * @param groupSelectStyle 340 * @protected 341 */ 342 static ensureTopVisibleBaseLayerShown(map, groupSelectStyle) { 343 let lastVisibleBaseLyr; 344 LayerSwitcher.forEachRecursive(map, function (lyr, _idx, _arr) { 345 if (lyr.get('type') === 'base' && lyr.getVisible()) { 346 lastVisibleBaseLyr = lyr; 347 } 348 }); 349 if (lastVisibleBaseLyr) 350 LayerSwitcher.setVisible_(map, lastVisibleBaseLyr, true, groupSelectStyle); 351 } 352 /** 353 * **_[static]_** - Get an Array of all layers and groups displayed by the LayerSwitcher (has a `'title'` property) 354 * contained by the specified map or layer group; optionally filtering via `filterFn` 355 * @param grp The map or layer group for which layers are found. 356 * @param filterFn Optional function used to filter the returned layers 357 */ 358 static getGroupsAndLayers(grp, filterFn) { 359 const layers = []; 360 filterFn = 361 filterFn || 362 function (_lyr, _idx, _arr) { 363 return true; 364 }; 365 LayerSwitcher.forEachRecursive(grp, function (lyr, idx, arr) { 366 if (lyr.get('title')) { 367 if (filterFn(lyr, idx, arr)) { 368 layers.push(lyr); 369 } 370 } 371 }); 372 return layers; 373 } 374 /** 375 * Toggle the visible state of a layer. 376 * Takes care of hiding other layers in the same exclusive group if the layer 377 * is toggle to visible. 378 * @protected 379 * @param map The map instance. 380 * @param lyr layer whose visibility will be toggled. 381 * @param visible Set whether the layer is shown 382 * @param groupSelectStyle 383 * @protected 384 */ 385 static setVisible_(map, lyr, visible, groupSelectStyle) { 386 // console.log(lyr.get('title'), visible, groupSelectStyle); 387 lyr.setVisible(visible); 388 if (visible && lyr.get('type') === 'base') { 389 // Hide all other base layers regardless of grouping 390 LayerSwitcher.forEachRecursive(map, function (l, _idx, _arr) { 391 if (l != lyr && l.get('type') === 'base') { 392 l.setVisible(false); 393 } 394 }); 395 } 396 if (lyr instanceof LayerGroup && 397 !lyr.get('combine') && 398 groupSelectStyle === 'children') { 399 lyr.getLayers().forEach((l) => { 400 LayerSwitcher.setVisible_(map, l, lyr.getVisible(), groupSelectStyle); 401 }); 402 } 403 } 404 /** 405 * Render all layers that are children of a group. 406 * @param map The map instance. 407 * @param lyr Layer to be rendered (should have a title property). 408 * @param idx Position in parent group list. 409 * @param options Options for groups and layers 410 * @protected 411 */ 412 static renderLayer_(map, lyr, idx, options, render) { 413 const li = document.createElement('li'); 414 const lyrTitle = lyr.get('title'); 415 const checkboxId = LayerSwitcher.uuid(); 416 const label = document.createElement('label'); 417 if (lyr instanceof LayerGroup && !lyr.get('combine')) { 418 const isBaseGroup = LayerSwitcher.isBaseGroup(lyr); 419 li.classList.add('group'); 420 if (isBaseGroup) { 421 li.classList.add(CSS_PREFIX + 'base-group'); 422 } 423 // Group folding 424 if (lyr.get('fold')) { 425 li.classList.add(CSS_PREFIX + 'fold'); 426 li.classList.add(CSS_PREFIX + lyr.get('fold')); 427 const btn = document.createElement('button'); 428 btn.onclick = function (e) { 429 const evt = e || window.event; 430 LayerSwitcher.toggleFold_(lyr, li); 431 evt.preventDefault(); 432 }; 433 li.appendChild(btn); 434 } 435 if (!isBaseGroup && options.groupSelectStyle != 'none') { 436 const input = document.createElement('input'); 437 input.type = 'checkbox'; 438 input.id = checkboxId; 439 input.checked = lyr.getVisible(); 440 input.indeterminate = lyr.get('indeterminate'); 441 input.onchange = function (e) { 442 const target = e.target; 443 LayerSwitcher.setVisible_(map, lyr, target.checked, options.groupSelectStyle); 444 render(lyr); 445 }; 446 li.appendChild(input); 447 label.htmlFor = checkboxId; 448 } 449 label.innerHTML = lyrTitle; 450 li.appendChild(label); 451 const ul = document.createElement('ul'); 452 li.appendChild(ul); 453 LayerSwitcher.renderLayers_(map, lyr, ul, options, render); 454 } 455 else { 456 li.className = 'layer'; 457 const input = document.createElement('input'); 458 if (lyr.get('type') === 'base') { 459 input.type = 'radio'; 460 } 461 else { 462 input.type = 'checkbox'; 463 } 464 input.id = checkboxId; 465 input.checked = lyr.get('visible'); 466 input.indeterminate = lyr.get('indeterminate'); 467 input.onchange = function (e) { 468 const target = e.target; 469 LayerSwitcher.setVisible_(map, lyr, target.checked, options.groupSelectStyle); 470 render(lyr); 471 }; 472 li.appendChild(input); 473 label.htmlFor = checkboxId; 474 label.innerHTML = lyrTitle; 475 const rsl = map.getView().getResolution(); 476 if (rsl >= lyr.getMaxResolution() || rsl < lyr.getMinResolution()) { 477 label.className += ' disabled'; 478 } 479 else if (lyr.getMinZoom && lyr.getMaxZoom) { 480 const zoom = map.getView().getZoom(); 481 if (zoom <= lyr.getMinZoom() || zoom > lyr.getMaxZoom()) { 482 label.className += ' disabled'; 483 } 484 } 485 li.appendChild(label); 486 } 487 return li; 488 } 489 /** 490 * Render all layers that are children of a group. 491 * @param map The map instance. 492 * @param lyr Group layer whose children will be rendered. 493 * @param elm DOM element that children will be appended to. 494 * @param options Options for groups and layers 495 * @protected 496 */ 497 static renderLayers_(map, lyr, elm, options, render) { 498 let lyrs = lyr.getLayers().getArray().slice(); 499 if (options.reverse) 500 lyrs = lyrs.reverse(); 501 for (let i = 0, l; i < lyrs.length; i++) { 502 l = lyrs[i]; 503 if (l.get('title')) { 504 elm.appendChild(LayerSwitcher.renderLayer_(map, l, i, options, render)); 505 } 506 } 507 } 508 /** 509 * **_[static]_** - Call the supplied function for each layer in the passed layer group 510 * recursing nested groups. 511 * @param lyr The layer group to start iterating from. 512 * @param fn Callback which will be called for each layer 513 * found under `lyr`. 514 */ 515 static forEachRecursive(lyr, fn) { 516 lyr.getLayers().forEach(function (lyr, idx, a) { 517 fn(lyr, idx, a); 518 if (lyr instanceof LayerGroup) { 519 LayerSwitcher.forEachRecursive(lyr, fn); 520 } 521 }); 522 } 523 /** 524 * **_[static]_** - Generate a UUID 525 * Adapted from http://stackoverflow.com/a/2117523/526860 526 * @returns {String} UUID 527 */ 528 static uuid() { 529 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 530 const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; 531 return v.toString(16); 532 }); 533 } 534 /** 535 * Apply workaround to enable scrolling of overflowing content within an 536 * element. Adapted from https://gist.github.com/chrismbarr/4107472 537 * @param elm Element on which to enable touch scrolling 538 * @protected 539 */ 540 static enableTouchScroll_(elm) { 541 if (LayerSwitcher.isTouchDevice_()) { 542 let scrollStartPos = 0; 543 elm.addEventListener('touchstart', function (event) { 544 scrollStartPos = this.scrollTop + event.touches[0].pageY; 545 }, false); 546 elm.addEventListener('touchmove', function (event) { 547 this.scrollTop = scrollStartPos - event.touches[0].pageY; 548 }, false); 549 } 550 } 551 /** 552 * Determine if the current browser supports touch events. Adapted from 553 * https://gist.github.com/chrismbarr/4107472 554 * @returns {Boolean} True if client can have 'TouchEvent' event 555 * @protected 556 */ 557 static isTouchDevice_() { 558 try { 559 document.createEvent('TouchEvent'); 560 return true; 561 } 562 catch (e) { 563 return false; 564 } 565 } 566 /** 567 * Fold/unfold layer group 568 * @param lyr Layer group to fold/unfold 569 * @param li List item containing layer group 570 * @protected 571 */ 572 static toggleFold_(lyr, li) { 573 li.classList.remove(CSS_PREFIX + lyr.get('fold')); 574 lyr.set('fold', lyr.get('fold') === 'open' ? 'close' : 'open'); 575 li.classList.add(CSS_PREFIX + lyr.get('fold')); 576 } 577 /** 578 * If a valid groupSelectStyle value is not provided then return the default 579 * @param groupSelectStyle The string to check for validity 580 * @returns The value groupSelectStyle, if valid, the default otherwise 581 * @protected 582 */ 583 static getGroupSelectStyle(groupSelectStyle) { 584 return ['none', 'children', 'group'].indexOf(groupSelectStyle) >= 0 585 ? groupSelectStyle 586 : 'children'; 587 } 588} 589// Expose LayerSwitcher as ol.control.LayerSwitcher if using a full build of 590// OpenLayers 591if (window['ol'] && window['ol']['control']) { 592 window['ol']['control']['LayerSwitcher'] = LayerSwitcher; 593} 594 595return LayerSwitcher; 596 597}))); 598