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