<NME> Layer.js
<BEF> import {Evented} from '../core/Events';
import {Map} from '../map/Map';
import * as Util from '../core/Util';

/*
 * @class Layer
 * @inherits Evented
 * @aka L.Layer
 * @aka ILayer
 *
 * A set of methods from the Layer base class that all Leaflet layers use.
 * Inherits all methods, options and events from `L.Evented`.
 *
 * @example
 *
 * ```js
 * var layer = L.marker(latlng).addTo(map);
 * layer.addTo(map);
 * layer.remove();
 * ```
 *
 * @event add: Event
 * Fired after the layer is added to a map
 *
 * @event remove: Event
 * Fired after the layer is removed from a map
 */


export const Layer = Evented.extend({

	// Classes extending `L.Layer` will inherit the following options:
	options: {
		// @option pane: String = 'overlayPane'
		// By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.
		pane: 'overlayPane',

		// @option attribution: String = null
		// String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers.
		attribution: null,

		bubblingMouseEvents: true
	},

	/* @section
	 * Classes extending `L.Layer` will inherit the following methods:
	 *
	 * @method addTo(map: Map|LayerGroup): this
	 * Adds the layer to the given map or layer group.
	 */
	addTo(map) {
		map.addLayer(this);
		return this;
	},

	// @method remove: this
	// Removes the layer from the map it is currently active on.
	remove() {
		return this.removeFrom(this._map || this._mapToAdd);
	},

	// @method removeFrom(map: Map): this
	// Removes the layer from the given map
	//
	// @alternative
	// @method removeFrom(group: LayerGroup): this
	// Removes the layer from the given `LayerGroup`
	removeFrom(obj) {
		if (obj) {
			obj.removeLayer(this);
		}
		return this;
			layer.beforeAdd(this);
		}

		this.whenReady(layer._layerAdd, layer);

		return this;
		return this;
	},

	removeInteractiveTarget(targetEl) {
		delete this._map._targets[Util.stamp(targetEl)];
		return this;
	},

	// @method getAttribution: String
	// Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
	getAttribution() {
		return this.options.attribution;
	},

	_layerAdd(e) {
		const map = e.target;

		// check in case layer gets added and then removed before the map is ready
		if (!map.hasLayer(this)) { return; }

		this._map = map;
		this._zoomAnimated = map._zoomAnimated;

		if (this.getEvents) {
			const events = this.getEvents();
			map.on(events, this);
			this.once('remove', function () {
				map.off(events, this);
			}, this);
		}

		this.onAdd(map);

		this.fire('add');
		map.fire('layeradd', {layer: this});
	}
});

/* @section Extension methods
 * @uninheritable
 *
 * Every layer should extend from `L.Layer` and (re-)implement the following methods.
 *
 * @method onAdd(map: Map): this
 * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer).
 *
 * @method onRemove(map: Map): this
 * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer).
 *
 * @method getEvents(): Object
 * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer.
 *
 * @method getAttribution(): String
 * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.
 *
 * @method beforeAdd(map: Map): this
 * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only.
 */


/* @namespace Map
 * @section Layer events
 *
 * @event layeradd: LayerEvent
 * Fired when a new layer is added to the map.
 *
 * @event layerremove: LayerEvent
 * Fired when some layer is removed from the map
 *
 * @section Methods for Layers and Controls
 */
Map.include({
	// @method addLayer(layer: Layer): this
	// Adds the given layer to the map
	addLayer(layer) {
		if (!layer._layerAdd) {
			throw new Error('The provided object is not a Layer.');
		}

		const id = Util.stamp(layer);
		if (this._layers[id]) { return this; }
		this._layers[id] = layer;

		layer._mapToAdd = this;

		if (layer.beforeAdd) {
			layer.beforeAdd(this);
		}

		this.whenReady(layer._layerAdd, layer);

		return this;
	},

	// @method removeLayer(layer: Layer): this
	// Removes the given layer from the map.
	removeLayer(layer) {
		const id = Util.stamp(layer);

		if (!this._layers[id]) { return this; }

		if (this._loaded) {
			layer.onRemove(this);
		}

		delete this._layers[id];

		if (this._loaded) {
			this.fire('layerremove', {layer});
			layer.fire('remove');
		}

		layer._map = layer._mapToAdd = null;

		return this;
	},

	// @method hasLayer(layer: Layer): Boolean
	// Returns `true` if the given layer is currently added to the map
	hasLayer(layer) {
		return Util.stamp(layer) in this._layers;
	},

	/* @method eachLayer(fn: Function, context?: Object): this
	 * Iterates over the layers of the map, optionally specifying context of the iterator function.
	 * ```
	 * map.eachLayer(function(layer){
	 *     layer.bindPopup('Hello');
	 * });
	 * ```
	 */
	eachLayer(method, context) {
		for (const i in this._layers) {
			method.call(context, this._layers[i]);
		}
		return this;
	},

	_addLayers(layers) {
		layers = layers ? (Util.isArray(layers) ? layers : [layers]) : [];

		for (let i = 0, len = layers.length; i < len; i++) {
			this.addLayer(layers[i]);
		}
	},

	_addZoomLimit(layer) {
		if (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {
			this._zoomBoundLayers[Util.stamp(layer)] = layer;
			this._updateZoomLevels();
		}
	},

	_removeZoomLimit(layer) {
		const id = Util.stamp(layer);

		if (this._zoomBoundLayers[id]) {
			delete this._zoomBoundLayers[id];
			this._updateZoomLevels();
		}
	},

	_updateZoomLevels() {
		let minZoom = Infinity,
		    maxZoom = -Infinity;
		const oldZoomSpan = this._getZoomSpan();

		for (const i in this._zoomBoundLayers) {
			const options = this._zoomBoundLayers[i].options;

			minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom);
			maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom);
		}

		this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom;
		this._layersMinZoom = minZoom === Infinity ? undefined : minZoom;

		// @section Map state change events
		// @event zoomlevelschange: Event
		// Fired when the number of zoomlevels on the map is changed due
		// to adding or removing a layer.
		if (oldZoomSpan !== this._getZoomSpan()) {
			this.fire('zoomlevelschange');
		}

		if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
			this.setZoom(this._layersMaxZoom);
		}
		if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
			this.setZoom(this._layersMinZoom);
		}
	}
});

<MSG> Unit tests for #4257, attributionControl layeradd/layerremove

<DFF> @@ -73,6 +73,10 @@ L.Map.include({
 			layer.beforeAdd(this);
 		}
 
+		if (layer.getAttribution && this.attributionControl) {
+			this.attributionControl.addAttribution(layer.getAttribution());
+		}
+
 		this.whenReady(layer._layerAdd, layer);
 
 		return this;

{'commit': 'diff --git a/spec/suites/control/Control.AttributionSpec.js b/spec/suites/control/Control.AttributionSpec.js\nindex 2f969e3b..562d525a 100644\n--- a/spec/suites/control/Control.AttributionSpec.js\n+++ b/spec/suites/control/Control.AttributionSpec.js\n@@ -65,4 +65,54 @@ describe("Control.Attribution", function () {\n \t\t});\n \t});\n \n+\tdescribe(\'on layer add/remove\', function () {\n+\t\tit(\'changes text\', function () {\n+\t\t\tvar fooLayer = new L.Layer();\n+\t\t\tvar barLayer = new L.Layer();\n+\t\t\tvar bazLayer = new L.Layer();\n+\t\t\tfooLayer.getAttribution = function () { return \'foo\'; };\n+\t\t\tbarLayer.getAttribution = function () { return \'bar\'; };\n+\t\t\tbazLayer.getAttribution = function () { return \'baz\'; };\n+\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix\');\n+\t\t\tmap.addLayer(fooLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\t\t\tmap.addLayer(barLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo, bar\');\n+\t\t\tmap.addLayer(bazLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo, bar, baz\');\n+\n+\t\t\tmap.removeLayer(fooLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | bar, baz\');\n+\t\t\tmap.removeLayer(barLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | baz\');\n+\t\t\tmap.removeLayer(bazLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix\');\n+\t\t});\n+\n+\t\tit(\'keeps count of duplicated attributions\', function () {\n+\t\t\tvar fooLayer = new L.Layer();\n+\t\t\tvar fo2Layer = new L.Layer();\n+\t\t\tvar fo3Layer = new L.Layer();\n+\t\t\tfooLayer.getAttribution = function () { return \'foo\'; };\n+\t\t\tfo2Layer.getAttribution = function () { return \'foo\'; };\n+\t\t\tfo3Layer.getAttribution = function () { return \'foo\'; };\n+\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix\');\n+\t\t\tmap.addLayer(fooLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\t\t\tmap.addLayer(fo2Layer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\t\t\tmap.addLayer(fo3Layer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\n+\t\t\tmap.removeLayer(fooLayer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\t\t\tmap.removeLayer(fo2Layer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix | foo\');\n+\t\t\tmap.removeLayer(fo3Layer);\n+\t\t\texpect(container.innerHTML).to.eql(\'prefix\');\n+\t\t});\n+\t});\n+\n });\ndiff --git a/src/layer/Layer.js b/src/layer/Layer.js\nindex a1c2a527..f21acb86 100644\n--- a/src/layer/Layer.js\n+++ b/src/layer/Layer.js\n@@ -73,6 +73,10 @@ L.Map.include({\n \t\t\tlayer.beforeAdd(this);\n \t\t}\n \n+\t\tif (layer.getAttribution && this.attributionControl) {\n+\t\t\tthis.attributionControl.addAttribution(layer.getAttribution());\n+\t\t}\n+\n \t\tthis.whenReady(layer._layerAdd, layer);\n \n \t\treturn this;\n', 'commit_subject': 'Unit tests for #4257, attributionControl layeradd/layerremove', 'repo_name': 'Leaflet/Leaflet', 'committer': {'name': 'Iván Sánchez Ortega', 'email': 'a15f8b81a160b4eebe5c84e9e3b65c87b9b2f18e@sanchezortega.es', 'time_sec': '1455827460', 'tz_offset': '60', 'date': {'seconds': '1455827460'}}, 'commit_tree': '8f221e10b49942a961a93626155cb226301f4083', 'commit_message': 'Unit tests for #4257, attributionControl layeradd/layerremove\n', 'language_name': 'HTML', 'license': 'bsd-2-clause', 'hunks': '@@ -73,6 +73,10 @@ L.Map.include({\n \t\t\tlayer.beforeAdd(this);\n \t\t}\n \n+\t\tif (layer.getAttribution && this.attributionControl) {\n+\t\t\tthis.attributionControl.addAttribution(layer.getAttribution());\n+\t\t}\n+\n \t\tthis.whenReady(layer._layerAdd, layer);\n \n \t\treturn this;\n', 'addition_count': 4, 'deletion_count': 0, 'src_file': 'a/src/layer/Layer.js', 'tgt_file': 'b/src/layer/Layer.js', 'file_extension': '.js', 'before_file': 'import {Evented} from \'../core/Events\';\nimport {Map} from \'../map/Map\';\nimport * as Util from \'../core/Util\';\n\n/*\n * @class Layer\n * @inherits Evented\n * @aka L.Layer\n * @aka ILayer\n *\n * A set of methods from the Layer base class that all Leaflet layers use.\n * Inherits all methods, options and events from `L.Evented`.\n *\n * @example\n *\n * ```js\n * var layer = L.marker(latlng).addTo(map);\n * layer.addTo(map);\n * layer.remove();\n * ```\n *\n * @event add: Event\n * Fired after the layer is added to a map\n *\n * @event remove: Event\n * Fired after the layer is removed from a map\n */\n\n\nexport const Layer = Evented.extend({\n\n\t// Classes extending `L.Layer` will inherit the following options:\n\toptions: {\n\t\t// @option pane: String = \'overlayPane\'\n\t\t// By default the layer will be added to the map\'s [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.\n\t\tpane: \'overlayPane\',\n\n\t\t// @option attribution: String = null\n\t\t// String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers.\n\t\tattribution: null,\n\n\t\tbubblingMouseEvents: true\n\t},\n\n\t/* @section\n\t * Classes extending `L.Layer` will inherit the following methods:\n\t *\n\t * @method addTo(map: Map|LayerGroup): this\n\t * Adds the layer to the given map or layer group.\n\t */\n\taddTo(map) {\n\t\tmap.addLayer(this);\n\t\treturn this;\n\t},\n\n\t// @method remove: this\n\t// Removes the layer from the map it is currently active on.\n\tremove() {\n\t\treturn this.removeFrom(this._map || this._mapToAdd);\n\t},\n\n\t// @method removeFrom(map: Map): this\n\t// Removes the layer from the given map\n\t//\n\t// @alternative\n\t// @method removeFrom(group: LayerGroup): this\n\t// Removes the layer from the given `LayerGroup`\n\tremoveFrom(obj) {\n\t\tif (obj) {\n\t\t\tobj.removeLayer(this);\n\t\t}\n\t\treturn this;\n\t\t\tlayer.beforeAdd(this);\n\t\t}\n\n\t\tthis.whenReady(layer._layerAdd, layer);\n\n\t\treturn this;\n\t\treturn this;\n\t},\n\n\tremoveInteractiveTarget(targetEl) {\n\t\tdelete this._map._targets[Util.stamp(targetEl)];\n\t\treturn this;\n\t},\n\n\t// @method getAttribution: String\n\t// Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).\n\tgetAttribution() {\n\t\treturn this.options.attribution;\n\t},\n\n\t_layerAdd(e) {\n\t\tconst map = e.target;\n\n\t\t// check in case layer gets added and then removed before the map is ready\n\t\tif (!map.hasLayer(this)) { return; }\n\n\t\tthis._map = map;\n\t\tthis._zoomAnimated = map._zoomAnimated;\n\n\t\tif (this.getEvents) {\n\t\t\tconst events = this.getEvents();\n\t\t\tmap.on(events, this);\n\t\t\tthis.once(\'remove\', function () {\n\t\t\t\tmap.off(events, this);\n\t\t\t}, this);\n\t\t}\n\n\t\tthis.onAdd(map);\n\n\t\tthis.fire(\'add\');\n\t\tmap.fire(\'layeradd\', {layer: this});\n\t}\n});\n\n/* @section Extension methods\n * @uninheritable\n *\n * Every layer should extend from `L.Layer` and (re-)implement the following methods.\n *\n * @method onAdd(map: Map): this\n * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer).\n *\n * @method onRemove(map: Map): this\n * Should contain all clean up code that removes the layer\'s elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer).\n *\n * @method getEvents(): Object\n * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer.\n *\n * @method getAttribution(): String\n * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.\n *\n * @method beforeAdd(map: Map): this\n * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only.\n */\n\n\n/* @namespace Map\n * @section Layer events\n *\n * @event layeradd: LayerEvent\n * Fired when a new layer is added to the map.\n *\n * @event layerremove: LayerEvent\n * Fired when some layer is removed from the map\n *\n * @section Methods for Layers and Controls\n */\nMap.include({\n\t// @method addLayer(layer: Layer): this\n\t// Adds the given layer to the map\n\taddLayer(layer) {\n\t\tif (!layer._layerAdd) {\n\t\t\tthrow new Error(\'The provided object is not a Layer.\');\n\t\t}\n\n\t\tconst id = Util.stamp(layer);\n\t\tif (this._layers[id]) { return this; }\n\t\tthis._layers[id] = layer;\n\n\t\tlayer._mapToAdd = this;\n\n\t\tif (layer.beforeAdd) {\n\t\t\tlayer.beforeAdd(this);\n\t\t}\n\n\t\tthis.whenReady(layer._layerAdd, layer);\n\n\t\treturn this;\n\t},\n\n\t// @method removeLayer(layer: Layer): this\n\t// Removes the given layer from the map.\n\tremoveLayer(layer) {\n\t\tconst id = Util.stamp(layer);\n\n\t\tif (!this._layers[id]) { return this; }\n\n\t\tif (this._loaded) {\n\t\t\tlayer.onRemove(this);\n\t\t}\n\n\t\tdelete this._layers[id];\n\n\t\tif (this._loaded) {\n\t\t\tthis.fire(\'layerremove\', {layer});\n\t\t\tlayer.fire(\'remove\');\n\t\t}\n\n\t\tlayer._map = layer._mapToAdd = null;\n\n\t\treturn this;\n\t},\n\n\t// @method hasLayer(layer: Layer): Boolean\n\t// Returns `true` if the given layer is currently added to the map\n\thasLayer(layer) {\n\t\treturn Util.stamp(layer) in this._layers;\n\t},\n\n\t/* @method eachLayer(fn: Function, context?: Object): this\n\t * Iterates over the layers of the map, optionally specifying context of the iterator function.\n\t * ```\n\t * map.eachLayer(function(layer){\n\t *     layer.bindPopup(\'Hello\');\n\t * });\n\t * ```\n\t */\n\teachLayer(method, context) {\n\t\tfor (const i in this._layers) {\n\t\t\tmethod.call(context, this._layers[i]);\n\t\t}\n\t\treturn this;\n\t},\n\n\t_addLayers(layers) {\n\t\tlayers = layers ? (Util.isArray(layers) ? layers : [layers]) : [];\n\n\t\tfor (let i = 0, len = layers.length; i < len; i++) {\n\t\t\tthis.addLayer(layers[i]);\n\t\t}\n\t},\n\n\t_addZoomLimit(layer) {\n\t\tif (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {\n\t\t\tthis._zoomBoundLayers[Util.stamp(layer)] = layer;\n\t\t\tthis._updateZoomLevels();\n\t\t}\n\t},\n\n\t_removeZoomLimit(layer) {\n\t\tconst id = Util.stamp(layer);\n\n\t\tif (this._zoomBoundLayers[id]) {\n\t\t\tdelete this._zoomBoundLayers[id];\n\t\t\tthis._updateZoomLevels();\n\t\t}\n\t},\n\n\t_updateZoomLevels() {\n\t\tlet minZoom = Infinity,\n\t\t    maxZoom = -Infinity;\n\t\tconst oldZoomSpan = this._getZoomSpan();\n\n\t\tfor (const i in this._zoomBoundLayers) {\n\t\t\tconst options = this._zoomBoundLayers[i].options;\n\n\t\t\tminZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom);\n\t\t\tmaxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom);\n\t\t}\n\n\t\tthis._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom;\n\t\tthis._layersMinZoom = minZoom === Infinity ? undefined : minZoom;\n\n\t\t// @section Map state change events\n\t\t// @event zoomlevelschange: Event\n\t\t// Fired when the number of zoomlevels on the map is changed due\n\t\t// to adding or removing a layer.\n\t\tif (oldZoomSpan !== this._getZoomSpan()) {\n\t\t\tthis.fire(\'zoomlevelschange\');\n\t\t}\n\n\t\tif (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {\n\t\t\tthis.setZoom(this._layersMaxZoom);\n\t\t}\n\t\tif (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {\n\t\t\tthis.setZoom(this._layersMinZoom);\n\t\t}\n\t}\n});\n', 'message': 'Unit tests for #4257, attributionControl layeradd/layerremove\n', 'author_name': 'Iván Sánchez Ortega', 'lang': 'js'}