Source: BKGWebMap/Control/LayerSwitcher.js

/*
 * Copyright (c) 2013 Bundesamt für Kartographie und Geodäsie.
 * See license.txt in the BKG WebMap distribution or repository for the
 * full text of the license.
 *
 * Author: Dirk Thalheim
 */

/**
 * @requires OpenLayers/BaseTypes/Class.js
 * @requires OpenLayers/BaseTypes/Element.js
 * @requires OpenLayers/Util.js
 * @requires OpenLayers/Control.js
 * @requires OpenLayers/Events.js
 * @requires BKGWebMap/Control.js
 * @requires BKGWebMap/Util.js
 * @requires BKGWebMap/Util/Toggler.js
 * @requires BKGWebMap/Control/SidePanel.js
 */

//noinspection JSUnusedLocalSymbols
/**
 * @classdesc Eigenes Control-Element zur Anzeige eines Layerswitchers
 *
 * @constructor BKGWebMap.Control.LayerSwitcher
 * @param {object} options - Optionen für das Controlelement
 */
BKGWebMap.Control.LayerSwitcher = OpenLayers.Class(BKGWebMap.Control.SidePanel, {

    /**
     * Eventhandler
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type  {OpenLayers.Events}
     */
    events: null,

    /**
     * Setzt die Reihenfolge der Layer im Layerswitcher.
     * In Abhängigkeit von hinzufügen in Map auf- oder absteigend 
	 * sortiert.
     *  
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {boolean}
     */
    ascending: true,

    /**  
     * Eine Kopie der Stati der Layer der Map zum Zeitpunkt der letzten 
     * Aktualisierung des LayerSwitchers.
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {object[]}
     */
    layerStates: null,

    /**
     * Texte für Labels
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {object.<string,string>}
     */
    labels: {
        baseLayer: 'Hintergrundkarte',
        overlays: 'Overlays'
    },

    /**
     * Tooltip für Toggle-Button
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {string}
     */
    title: 'Layerswitcher ein-/ausblenden',

    /**
     * CSS-Klasse für content
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {string}
     */
    style: 'wmLayerSwitcherPanel',

    /**
     * Breite für Layerswitcher
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {int}
     */
    size: 175,

    /**
     * LayerBaum für die Darstellung
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {object}
     */
    layerTree: null,

    /**
     * Nach Initialisierung werden automatisch alle Gruppen ab dieser Ebene im Layerbaum eingeklappt. Standart ist
     * <code>-1</code>. Dies klappt alle Ebenen auf.
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @type {int}
     */
    closeLevels: -1,

    initialize: function(options) {
        BKGWebMap.Control.SidePanel.prototype.initialize.apply(this, [options]);

        this.layerStates = [];
        this.layerEntries = [];
        this.outsiteViewport = this.div != null;

        this.layerTree = {
            baselayers: { name: this.labels.baseLayer, layers: [], groups: {}},
            overlays: { name: this.labels.overlays, layers: [], groups: {}}
        };
    },

    destroy: function() {
        if(this.events) {
            this.events.destroy();
            this.events = null;
        }

        //clear out layers info and unregister their events 
        this.clearLayers();
        this.layerEntries = null;

        this.map.events.un({
            addlayer: this.redraw,
            changelayer: this.redraw,
            removelayer: this.redraw,
            changebaselayer: this.redraw,
            scope: this
        });

        OpenLayers.Control.prototype.destroy.apply(this, arguments);
    },

    /**
     * Registriert Control für Layer-Map-Events
     * @param {OpenLayers.Map} map
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    setMap: function(map) {
        OpenLayers.Control.prototype.setMap.apply(this, arguments);

        this.map.events.on({
            addlayer: this.redraw,
            changelayer: this.redraw,
            removelayer: this.redraw,
            changebaselayer: this.redraw,
            scope: this
        });
    },

    /**
     * Erstellt die HTML-Elemente des LayerSwitcher
     *
     * @return {HTMLElement} Eine Referenz zum DOMElement welches die Legende beinhaltet.
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    draw: function() {
        var div = this.outsiteViewport ? this.div : BKGWebMap.Control.SidePanel.prototype.draw.apply(this);

        this.loadContents(this.outsiteViewport ? div : this.content);
        
        // populate div with current info
        this.redraw();    

        return div;
    },

    /**
     * Checks if the layer state has changed since the last redraw() call.
     * 
     * @return {boolean} <code>true</code> wenn sich der Status seit dem letzten Rendern geändert hat
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    checkRedraw: function() {
        if ( !this.layerStates.length || (this.map.layers.length != this.layerStates.length) ) {
            return true;
        } 

        for (var i=0, len=this.layerStates.length; i<len; i++) {
            var layerState = this.layerStates[i];
            var layer = this.map.layers[i];
            if ( (layerState.name != layer.name) || 
                 (layerState.inRange != layer.inRange) || 
                 (layerState.id != layer.id) ||
                 (layerState.visibility != layer.visibility) ||
                 (layerState.group != layer.group) ) {
                return true;
            }    
        }
        return false;
    },

    /**
     * Ermittelt die aktuellen Layerstati.
     * @return {object[]}
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    getCurrentLayerStates: function() {
        var len = this.map.layers.length;
        var layerStates = new Array(len);
        for (var i=0; i < len; i++) {
            var layer = this.map.layers[i];
            layerStates[i] = {
                'name': layer.name,
                'visibility': layer.visibility,
                'inRange': layer.inRange,
                'id': layer.id,
                'group': layer.group
            };
        }
        return layerStates;
    },

    /**
     * Ermittelt den aktuellen Status der Kartenlayer und baut daraus den Layerswitcher neu.
     *
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    redraw: function() {
        // Sofern sich der Status nicht geändert hat ist nichts zu tun.
        if (!this.checkRedraw()) { 
            return;
        } 

        // Anzeige der Layer im Layerswitcher zurücksetzen
        this.clearLayers();
        
        // Status wird vor redraw gespeichert, da während des 
        // redraws Änderungen vorgenommen werden, die sonst eine 
        // Endlosschleife verursachen könnten.
        this.layerStates = this.getCurrentLayerStates();    

        var layers = this.map.layers.slice();
//        if (!this.ascending) { layers.reverse(); }

        var baselayers = BKGWebMap.Util.grep(layers, function(layer) { return layer.isBaseLayer; });
        var overlays = BKGWebMap.Util.grep(layers, function(layer) { return !layer.isBaseLayer && layer.displayInLayerSwitcher; });
        if (!this.ascending) { overlays.reverse(); }

        // layerTree aufbauen. Dabei Baselayer und Overviews getrennt behandeln.
        this.updateLayerTree(this.layerTree.baselayers, baselayers);
        this.updateLayerTree(this.layerTree.overlays, overlays);

        // leere Gruppen ab zweiter Hierarchieebene entfernen
        this.removeEmptyGroups(this.layerTree.overlays);

        // layerTree rendern
        for(var id in this.layerTree) {
            //noinspection JSUnfilteredForInLoop
            var group = this.layerTree[id];
            if(BKGWebMap.Util.isEmpty(group.groups) && BKGWebMap.Util.isEmpty(group.layers)) continue;

            this.renderLayerTree(group, 0);
            this.toc.appendChild(group.titleDiv);
            this.toc.appendChild(group.contentDiv);
        }
    },

    /**
     * Aktualisiert den LayerTree
     * @param layerGroup {object} aktueller Knoten im Layerbaum
     * @param layers {array} Liste der Layer die dem Baum hinzugefügt werden
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    updateLayerTree: function(layerGroup, layers) {
        for(var i= 0; i < layers.length; i++) {
            var layer = layers[i];
            var group;
            if (!layer.group) {
                group = layerGroup;
            } else {
                // TODO: weitere Untergruppen?
                if(layerGroup.groups[layer.group] === undefined) {
                    layerGroup.groups[layer.group] = {name: layer.group, layers: [], groups: {}};
                }
                group = layerGroup.groups[layer.group];
            }
            group.layers.push(layer);
        }
    },

    /**
     * Entfernt alle leeren Layergruppen aus dem Baum
     * @param tree {object} Der aktuelle Knoten im Layerbaum
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    removeEmptyGroups: function(tree) {
        for(var i=tree.groups.length-1; i >= 0; i++) {
            var group = tree.groups[i];
            this.removeEmptyGroups(group);
            if(!group.groups && !group.layers) {
                group.groups.splice(i, 1);
            }
        }
    },

    /**
     * Erstellt und arrangiert alle HTML-Elemente im Layertree
     * @param tree {object} der Layerbaum
     * @param level {int} aktuelle Ebenennummer im Gesamtbaum
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    renderLayerTree: function(tree, level) {
        // Überschrift- und Inhaltsdiv für Gruppe
        if(!tree.titleDiv) {
            tree.titleDiv = document.createElement('dt');
            tree.titleDiv.innerHTML = tree.name;

            tree.contentDiv = document.createElement('dd');
            OpenLayers.Element.addClass(tree.contentDiv, tree.name.toLowerCase());

            tree.toggler = new BKGWebMap.Util.Toggler(
                    tree.titleDiv, tree.contentDiv,
                    { closed: this.closeLevels >= 0 && !(level < this.closeLevels) }
            );
        }

        // Alle Untergruppen
        if(tree.groups) {
            var grouplist = document.createElement('dl');

            for (var key in tree.groups) {
                //noinspection JSUnfilteredForInLoop
                var group = tree.groups[key];
                this.renderLayerTree(group, level + 1);
                grouplist.appendChild(group.titleDiv);
                grouplist.appendChild(group.contentDiv);
            }
            tree.contentDiv.appendChild(grouplist);
        }

        // Alle direkten Layer in der Gruppe
        for(var i=0; i<tree.layers.length; i++) {
            this.renderLayerEntry(tree.layers[i], tree.contentDiv);
        }

    },

    /** 
     * Erzeugt die HTML-Darstellung für einen Layer-Eintrag 
     * @param {OpenLayers.Layer} layer - Der aktuelle Layer
     * @param {HTMLElement} groupDiv - HTML-Element, in das Layereintrag angehängt werden soll
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    renderLayerEntry: function(layer, groupDiv) {
        var layerEntry = this.getLayerRenderer(layer, this);
        layerEntry = layerEntry || new BKGWebMap.Control.LayerSwitcher.LayerEntry(layer, this);
    	var div = layerEntry.draw();
    	if(div != null) {
            groupDiv.appendChild(div);
    	}

        this.layerEntries.push(layerEntry);
    },

    /**
     * Platzhalter zur Bereitstellung eigener Renderer für Layereinträge im LayerSwitcher
     * @param {OpenLayers.Layer} layer - Der aktuelle Layer
     * @param {BKGWebMap.Control.LayerSwitcher} parent - der Referenz auf diesen LayerSwitcher
     * @return {OpenLayers.Class<BKGWebMap.Control.LayerSwitcher.LayerEntry>}
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    getLayerRenderer: function(layer, parent) {
        return null;
    },

    /** 
     * Löscht Visualisierung der Layer.
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    clearLayers: function() {
        while (this.toc.firstChild) {
            this.toc.removeChild(this.toc.firstChild);
        }

        // layertree leeren
        this.clearLayerTree(this.layerTree.baselayers);
        this.clearLayerTree(this.layerTree.overlays);

        if(this.layerEntries) {
            BKGWebMap.Util.each(this.layerEntries, function(index, entry) { entry.destroy(); });
        }
        this.layerEntries = [];
    },

    /**
     * Löscht die Visualisierung des Layerbaums
     * @memberOf BKGWebMap.Control.LayerSwitcher
     * @param tree {object} aktueller Knoten im Baum
     */
    clearLayerTree: function(tree) {
        // HTML Inhalt zurücksetzen
        if(tree.contentDiv) {
            while (tree.contentDiv.firstChild) {
                tree.contentDiv.removeChild(tree.contentDiv.firstChild);
            }
        }

        // Layer zurücksetzen
        tree.layers = [];

        // Gruppen zurücksetzen
        if(tree.groups) {
            for(var key in tree.groups) {
                //noinspection JSUnfilteredForInLoop
                this.clearLayerTree(tree.groups[key]);
            }
        }
    },

    /** 
     * Setzt Layout-Divs und Labels für den LayerSwitcher
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    loadContents: function(div) {
        // TOC
        this.toc = document.createElement("dl");
        OpenLayers.Element.addClass(this.toc, 'toc');
        div.appendChild(this.toc);

        // Pseudo-Events registrieren, um Interaktion mit LayerSwitcher zu erlauben
        this.events = new OpenLayers.Events(this, div, null, true);

        this.events.on({
            "mousedown": this.onmousedown,
            "mousemove": this.onmousemove,
            "mouseup": this.onmouseup,
            "mouseout": this.onmouseout,
            "click": BKGWebMap.Util.stopEvent,
            "dblclick": BKGWebMap.Util.stopEvent,
            "touchstart": BKGWebMap.Util.stopEvent,
            scope: this
        });
    },
        
    /**
     * @param {Event} evt
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    onmousedown: function (evt) {
        this.mousedown = true;
        OpenLayers.Event.stop(evt, true);
    },

    /**
     * @param {Event} evt
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    onmousemove: function (evt) {
        if (this.mousedown) {
            OpenLayers.Event.stop(evt, true);
        }
    },

    /**
     * @param {Event} evt
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    onmouseup: function (evt) {
        if (this.mousedown) {
            this.mousedown = false;
            OpenLayers.Event.stop(evt, true);
        }
    },

    /**
     * @memberOf BKGWebMap.Control.LayerSwitcher
     */
    onmouseout: function () {
        this.mousedown = false;
    },

    CLASS_NAME: "BKGWebMap.Control.LayerSwitcher"
});

/**
 * Factory-Funktion zur Generierung eines LayerSwitcher Steuerelement.
 * @param {Array<OpenLayers.Control>} controls - Liste der Steuerelemente, in die die neue erzeugten Steuerelemente
 *                                               eingefügt werden sollen.
 * @param {object} config - Konfiguration für Steuerelement (s. Konstruktor BKGWebMap.Control.LayerSwitcher).
 */
BKGWebMap.Control.FACTORIES['layerSwitcher'] = function(controls, config) {
    if (!config) return;

    config = (typeof config === 'boolean') ? {} : config;

    var sidePanel = BKGWebMap.Util.grep(controls, function(c) {return c.CLASS_NAME === 'BKGWebMap.Control.SidePanel'});
    config.div = config.div || null;

    if (!config.div && sidePanel.length > 0) {
        // Layerswitcher in SidePanel einfügen
        var title = document.createElement('h3');
        title.innerHTML = 'Ebenenauswahl';
        sidePanel[0].content.appendChild(title);
        config.div = document.createElement('div');
        OpenLayers.Element.addClass(config.div, 'layerSwitcher');
        sidePanel[0].content.appendChild(config.div);
    }

    controls.push(new BKGWebMap.Control.LayerSwitcher(config));
};