it("Throws error on map center at minus Infinity longitude", function () {

describe('GridLayer', function () {

	var div, map;

	beforeEach(function () {
		div = document.createElement('div');
		div.style.width = '800px';
		div.style.height = '600px';
		div.style.visibility = 'hidden';

		document.body.appendChild(div);

		map = L.map(div);
	});

	afterEach(function () {
		document.body.removeChild(div);
	});

	describe('#redraw', function () {
		it('can be called before map.setView', function () {
			var grid = L.gridLayer().addTo(map);
			expect(grid.redraw()).to.equal(grid);
		});
	});

	describe('#setOpacity', function () {
		it('can be called before map.setView', function () {
			var grid = L.gridLayer().addTo(map);
			expect(grid.setOpacity(0.5)).to.equal(grid);
		});

		it('works when map has fadeAnimated=false (IE8 is exempt)', function (done) {
			map.remove();
			map = L.map(div, {fadeAnimation: false}).setView([0, 0], 0);

			var grid = L.gridLayer().setOpacity(0.5).addTo(map);
			grid.on('load', function () {
				expect(grid._container.style.opacity).to.equal('0.5');
				done();
			});
		});
	});

	it('positions tiles correctly with wrapping and bounding', function () {

		map.setView([0, 0], 1);

		var tiles = [];

		var grid = L.gridLayer();
		grid.createTile = function (coords) {
			var tile = document.createElement('div');
			tiles.push({coords: coords, tile: tile});
			return tile;
		};

		map.addLayer(grid);

		var loaded = {};

		for (var i = 0; i < tiles.length; i++) {
			var coords = tiles[i].coords,
			    pos = L.DomUtil.getPosition(tiles[i].tile);

			loaded[pos.x + ':' + pos.y] = [coords.x, coords.y];
		}

		expect(loaded).to.eql({
			'144:0': [0, 0],
			'400:0': [1, 0],
			'144:256': [0, 1],
			'400:256': [1, 1],
			'-112:0': [1, 0],
			'656:0': [0, 0],
			'-112:256': [1, 1],
			'656:256': [0, 1]
		});
	});

	describe('tile pyramid', function () {
		var clock;

		beforeEach(function () {
			clock = sinon.useFakeTimers();
		});

		afterEach(function () {
			clock.restore();
		});

		it('removes tiles for unused zoom levels', function (done) {
			map.remove();
			map = L.map(div, {fadeAnimation: false});
			map.setView([0, 0], 1);

			var grid = L.gridLayer();
			var tiles = {};

			grid.createTile = function (coords) {
				tiles[grid._tileCoordsToKey(coords)] = true;
				return document.createElement('div');
			};

			grid.on('tileunload', function (e) {
				delete tiles[grid._tileCoordsToKey(e.coords)];
			});

			grid.on('load', function (e) {
				if (Object.keys(tiles).length === 1) {
					expect(Object.keys(tiles)).to.eql(['0:0:0']);
					grid.off();
					done();
				}
			});

			map.addLayer(grid);
			map.setZoom(0, {animate: false});
			clock.tick(250);
		});
	});

	describe('#createTile', function () {

		beforeEach(function () {
			// Simpler sizes to test.
			div.style.width = '512px';
			div.style.height = '512px';
		});

		afterEach(function () {
			div.style.width = '800px';
			div.style.height = '600px';
		});

		// Passes on Firefox, but fails on phantomJS: done is never called.
		it('only creates tiles for visible area on zoom in', function (done) {
			map.remove();
			map = L.map(div);
			map.setView([0, 0], 10);

			var grid = L.gridLayer(),
			    count = 0,
			    loadCount = 0;
			grid.createTile = function (coords) {
				count++;
				return document.createElement('div');
			};
			var onLoad = function (e) {
				expect(count).to.eql(4);
				count = 0;
				loadCount++;
				if (loadCount === 1) {  // On layer add.
					map.zoomIn();
				} else {  // On zoom in.
					done();
				}
			};
			grid.on('load', onLoad);
			map.addLayer(grid);
		});

	});

	describe("#onAdd", function () {
		it('is called after zoomend on first map load', function () {
			var layer = L.gridLayer().addTo(map);

			var onAdd = layer.onAdd,
			    onAddSpy = sinon.spy();
			layer.onAdd = function () {
				onAdd.apply(this, arguments);
				onAddSpy();
			};

			var onReset = sinon.spy();
			map.on('zoomend', onReset);
			map.setView([0, 0], 0);

			expect(onReset.calledBefore(onAddSpy)).to.be.ok();
		});
	});

	describe("#getMaxZoom, #getMinZoom", function () {
		describe("when a tilelayer is added to a map with no other layers", function () {
			it("has the same zoomlevels as the tilelayer", function () {
				var maxZoom = 10,
				    minZoom = 5;

				map.setView([0, 0], 1);

				L.gridLayer({
					maxZoom: maxZoom,
					minZoom: minZoom
				}).addTo(map);

				expect(map.getMaxZoom()).to.be(maxZoom);
				expect(map.getMinZoom()).to.be(minZoom);
			});
		});

		describe("accessing a tilelayer's properties", function () {
			it('provides a container', function () {
				map.setView([0, 0], 1);

				var layer = L.gridLayer().addTo(map);
				expect(layer.getContainer()).to.be.ok();
			});
		});

		describe("when a tilelayer is added to a map that already has a tilelayer", function () {
			it("has its zoomlevels updated to fit the new layer", function () {
				map.setView([0, 0], 1);

				L.gridLayer({minZoom: 10, maxZoom: 15}).addTo(map);
				expect(map.getMinZoom()).to.be(10);
				expect(map.getMaxZoom()).to.be(15);

				L.gridLayer({minZoom: 5, maxZoom: 10}).addTo(map);
				expect(map.getMinZoom()).to.be(5);  // changed
				expect(map.getMaxZoom()).to.be(15); // unchanged

				L.gridLayer({minZoom: 10, maxZoom: 20}).addTo(map);
				expect(map.getMinZoom()).to.be(5);  // unchanged
				expect(map.getMaxZoom()).to.be(20); // changed

				L.gridLayer({minZoom: 0, maxZoom: 25}).addTo(map);
				expect(map.getMinZoom()).to.be(0); // changed
				expect(map.getMaxZoom()).to.be(25); // changed
			});
		});

		describe("when a gridlayer is removed from a map", function () {
			it("has its zoomlevels updated to only fit the layers it currently has", function () {
				var tiles = [
					L.gridLayer({minZoom: 10, maxZoom: 15}).addTo(map),
					L.gridLayer({minZoom: 5, maxZoom: 10}).addTo(map),
					L.gridLayer({minZoom: 10, maxZoom: 20}).addTo(map),
					L.gridLayer({minZoom: 0, maxZoom: 25}).addTo(map)
				];
				map.whenReady(function () {
					expect(map.getMinZoom()).to.be(0);
					expect(map.getMaxZoom()).to.be(25);

					map.removeLayer(tiles[0]);
					expect(map.getMinZoom()).to.be(0);
					expect(map.getMaxZoom()).to.be(25);

					map.removeLayer(tiles[3]);
					expect(map.getMinZoom()).to.be(5);
					expect(map.getMaxZoom()).to.be(20);

					map.removeLayer(tiles[2]);
					expect(map.getMinZoom()).to.be(5);
					expect(map.getMaxZoom()).to.be(10);

					map.removeLayer(tiles[1]);
					expect(map.getMinZoom()).to.be(0);
					expect(map.getMaxZoom()).to.be(Infinity);
				});
			});
		});
	});

	describe("min/maxNativeZoom option", function () {
		it("calls createTile() with maxNativeZoom when map zoom is larger", function (done) {
			map.setView([0, 0], 10);

			var grid = L.gridLayer({
				maxNativeZoom: 5
			});
			var tileCount = 0;

			grid.createTile = function (coords) {
				expect(coords.z).to.be(5);
				tileCount++;
				return document.createElement('div');
			};
			grid.on('load', function () {
				if (tileCount > 0) {
					done();
				} else {
					done('No tiles loaded');
				}
			});

			map.addLayer(grid);
		});

		it("calls createTile() with minNativeZoom when map zoom is smaller", function (done) {
			map.setView([0, 0], 3);

			var grid = L.gridLayer({
				minNativeZoom: 5
			});
			var tileCount = 0;

			grid.createTile = function (coords) {
				expect(coords.z).to.be(5);
				tileCount++;
				return document.createElement('div');
			};
			grid.on('load', function () {
				if (tileCount > 0) {
					done();
				} else {
					done('No tiles loaded');
				}
			});

			map.addLayer(grid);
		});
	});

	describe("number of 256px tiles loaded in synchronous non-animated grid @800x600px", function () {
		var clock, grid, counts;

		beforeEach(function () {
			clock = sinon.useFakeTimers();

			grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256)
			});

			grid.createTile = function (coords) {
				var tile = document.createElement('div');
				tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
				tile.style.border = '2px solid red';
				return tile;
			};

			counts = {
				tileload: 0,
				tileerror: 0,
				tileloadstart: 0,
				tileunload: 0
			};

			grid.on('tileload tileunload tileerror tileloadstart', function (ev) {
// 				console.log(ev.type);
				counts[ev.type]++;
			});
// 			grid.on('tileunload', function (ev) {
// 				console.log(ev.type, ev.coords, counts);
// 			});

			map.options.fadeAnimation = false;
			map.options.zoomAnimation = false;
		});

		afterEach(function () {
			clock.restore();
			grid.off();
			grid = undefined;
			counts = undefined;
		});

		it("Loads 8 tiles zoom 1", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(8);
				expect(counts.tileload).to.be(8);
				expect(counts.tileunload).to.be(0);
				done();
			});

			map.addLayer(grid).setView([0, 0], 1);
			clock.tick(250);
		});

		it("Loads 5 tiles zoom 0", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(5);
				expect(counts.tileload).to.be(5);
				expect(counts.tileunload).to.be(0);
				done();
			});

			map.addLayer(grid).setView([0, 0], 0);
			clock.tick(250);
		});

		it("Loads 16 tiles zoom 10", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off();

				done();
			});

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		it("Loads 32, unloads 16 tiles zooming in 10-11", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(32);
					expect(counts.tileload).to.be(32);
					expect(counts.tileunload).to.be(16);
					done();
				});

				map.setZoom(11, {animate: false});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		it("Loads 32, unloads 16 tiles zooming out 11-10", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(32);
					expect(counts.tileload).to.be(32);
					expect(counts.tileunload).to.be(16);
					done();
				});

				map.setZoom(10, {animate: false});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 11);
			clock.tick(250);
		});

		it("Loads 32, unloads 16 tiles zooming out 18-10", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(32);
					expect(counts.tileload).to.be(32);
					expect(counts.tileunload).to.be(16);
					done();
				});

				map.setZoom(10, {animate: false});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 18);
			clock.tick(250);
		});

	});

	describe("number of 256px tiles loaded in synchronous animated grid @800x600px", function () {

		var clock, grid, counts;

		beforeEach(function () {
			clock = sinon.useFakeTimers();

			grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256)
			});

			grid.createTile = function (coords) {
				var tile = document.createElement('div');
				tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
				tile.style.border = '2px solid red';
				return tile;
			};

			counts = {
				tileload: 0,
				tileerror: 0,
				tileloadstart: 0,
				tileunload: 0
			};

			grid.on('tileload tileunload tileerror tileloadstart', function (ev) {
				counts[ev.type]++;
			});
		});

		afterEach(function () {
			clock.restore();
			grid.off();
			grid = undefined;
			counts = undefined;
		});

		// Debug helper
		function logTiles(ev) {
			var pending = 0;
			for (var key in grid._tiles) {
				if (!grid._tiles[key].loaded) { pending++; }
			}
			console.log(ev.type + ': ', ev.coords, grid._loading, counts, ' pending: ', pending);
		}

		// animationFrame helper, just runs requestAnimFrame() a given number of times
		function runFrames(n) {
			return _runFrames(n)();
		}

		function _runFrames(n) {
			if (n) {
				return function () {
					clock.tick(40); // 40msec/frame ~= 25fps
					map.fire('_frame');
					L.Util.requestAnimFrame(_runFrames(n - 1));
				};
			} else {
				return L.Util.falseFn;
			}
		}

		// NOTE: This test has different behaviour in PhantomJS and graphical
		// browsers due to CSS animations!
		it.skipInPhantom("Loads 32, unloads 16 tiles zooming in 10-11", function (done) {

// 			grid.on('tileload tileunload tileloadstart load', logTiles);

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

// 				grid.on('load', logTiles);
				grid.on('load', function () {

					// We're one frame into the zoom animation, there are
					// 16 tiles for z10 plus 16 tiles for z11 covering the
					// bounds at the *end* of the zoom-*in* anim
					expect(counts.tileloadstart).to.be(32);
					expect(counts.tileload).to.be(32);
					expect(counts.tileunload).to.be(0);

					// Wait > 250msec for the tile fade-in animation to complete,
					// which triggers the tile pruning
					clock.tick(300);

					// After the zoom-in, the 'outside' 12 tiles (not the 4
					// at the center, still in bounds) have been unloaded.
					expect(counts.tileunload).to.be(12);

					L.Util.requestAnimFrame(function () {
						expect(counts.tileloadstart).to.be(32);
						expect(counts.tileload).to.be(32);
						expect(counts.tileunload).to.be(16);
						done();
					});
				});

				map.setZoom(11, {animate: true});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		it("Loads 32, unloads 16 tiles zooming in 10-18", function (done) {
			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {

					// In this particular scenario, the tile unloads happen in the
					// next render frame after the grid's 'load' event.
					L.Util.requestAnimFrame(function () {
						expect(counts.tileloadstart).to.be(32);
						expect(counts.tileload).to.be(32);
						expect(counts.tileunload).to.be(16);
						done();
					});
				});

				map.setZoom(18, {animate: true});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		// NOTE: This test has different behaviour in PhantomJS and graphical
		// browsers due to CSS animations!
		it.skipInPhantom("Loads 32, unloads 16 tiles zooming out 11-10", function (done) {

// 			grid.on('tileload tileunload load', logTiles);

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

// 				grid.on('load', logTiles);
				grid.on('load', function () {

					grid.off('load');
// 					grid.on('load', logTiles);

					// We're one frame into the zoom animation, there are
					// 16 tiles for z11 plus 4 tiles for z10 covering the
					// bounds at the *beginning* of the zoom-*out* anim
					expect(counts.tileloadstart).to.be(20);
					expect(counts.tileload).to.be(20);
					expect(counts.tileunload).to.be(0);

					// Wait > 250msec for the tile fade-in animation to complete,
					// which triggers the tile pruning
					clock.tick(300);
					L.Util.requestAnimFrame(function () {
						expect(counts.tileunload).to.be(16);

						// The next 'load' event happens when the zoom anim is
						// complete, and triggers loading of all the z10 tiles.
						grid.on('load', function () {
							expect(counts.tileloadstart).to.be(32);
							expect(counts.tileload).to.be(32);
							done();
						});

					});
				});

				map.setZoom(10, {animate: true});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 11);
			clock.tick(250);
		});

		it("Loads 32, unloads 16 tiles zooming out 18-10", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {

					// In this particular scenario, the tile unloads happen in the
					// next render frame after the grid's 'load' event.
					L.Util.requestAnimFrame(function () {
						expect(counts.tileloadstart).to.be(32);
						expect(counts.tileload).to.be(32);
						expect(counts.tileunload).to.be(16);
						done();
					});
				});

				map.setZoom(10, {animate: true});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 18);
			clock.tick(250);
		});

		// NOTE: This test has different behaviour in PhantomJS and graphical
		// browsers due to CSS animations!
		it.skipInPhantom("Loads 290, unloads 275 tiles on MAD-TRD flyTo()", function (done) {

			this.timeout(10000); // This test takes longer than usual due to frames

			var mad = [40.40, -3.7], trd = [63.41, 10.41];

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(12);
				expect(counts.tileload).to.be(12);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				map.on('zoomend', function () {
					expect(counts.tileloadstart).to.be(290);
					expect(counts.tileunload).to.be(275);
					expect(counts.tileload).to.be(290);
					expect(grid._container.querySelectorAll('div').length).to.be(16);	// 15 + container
					done();
				});

				map.flyTo(trd, 12, {animate: true});

// 				map.on('_frame', function () {
// 					console.log('frame', counts);
// 				});

				runFrames(500);
			});

			map.addLayer(grid).setView(mad, 12);
			clock.tick(250);
		});

	});

	describe("configurable tile pruning", function () {
		var clock, grid, counts;

		beforeEach(function () {
			clock = sinon.useFakeTimers();

			grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256)
			});

			grid.createTile = function (coords) {
				var tile = document.createElement('div');
				tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
				tile.style.border = '2px solid red';
				return tile;
			};

			counts = {
				tileload: 0,
				tileerror: 0,
				tileloadstart: 0,
				tileunload: 0
			};

			grid.on('tileload tileunload tileerror tileloadstart', function (ev) {
// 				console.log(ev.type);
				counts[ev.type]++;
			});
// 			grid.on('tileunload', function (ev) {
// 				console.log(ev.type, ev.coords, counts);
// 			});

			map.options.fadeAnimation = false;
			map.options.zoomAnimation = false;
		});

		afterEach(function () {
			clock.restore();
			grid.off();
			grid = undefined;
			counts = undefined;
		});

		it("Loads map, moves forth by 512 px, keepBuffer = 0", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(28);
					expect(counts.tileload).to.be(28);
					expect(counts.tileunload).to.be(12);
					done();
				});

				map.panBy([512, 512], {animate: false});
				clock.tick(250);
			});

			grid.options.keepBuffer = 0;

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		it("Loads map, moves forth and back by 512 px, keepBuffer = 0", function (done) {

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(28);
					expect(counts.tileload).to.be(28);
					expect(counts.tileunload).to.be(12);

					grid.off('load');
					grid.on('load', function () {
						expect(counts.tileloadstart).to.be(40);
						expect(counts.tileload).to.be(40);
						expect(counts.tileunload).to.be(24);
						done();
					});

					map.panBy([-512, -512], {animate: false});
					clock.tick(250);
				});

				map.panBy([512, 512], {animate: false});
				clock.tick(250);
			});

			grid.options.keepBuffer = 0;

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});

		it("Loads map, moves forth and back by 512 px, default keepBuffer", function (done) {

			var spy = sinon.spy();

			grid.on('load', function () {
				expect(counts.tileloadstart).to.be(16);
				expect(counts.tileload).to.be(16);
				expect(counts.tileunload).to.be(0);
				grid.off('load');

				grid.on('load', function () {
					expect(counts.tileloadstart).to.be(28);
					expect(counts.tileload).to.be(28);
					expect(counts.tileunload).to.be(0);
					grid.off('load');

					grid.addEventListener('load', spy);

					map.panBy([-512, -512], {animate: false});
					clock.tick(250);

					expect(spy.called).to.be(false);
					done();
				});

				map.panBy([512, 512], {animate: false});
				clock.tick(250);
			});

			map.addLayer(grid).setView([0, 0], 10);
			clock.tick(250);
		});
	});

	describe("nowrap option", function () {
		it("When false, uses same coords at zoom 0 for all tiles", function (done) {

			var grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256),
				noWrap: false
			});
			var loadedTileKeys = [];

			grid.createTile = function (coords) {
				loadedTileKeys.push(coords.x + ':' + coords.y + ':' + coords.z);
				return document.createElement('div');
			};

			map.addLayer(grid).setView([0, 0], 0);

			grid.on('load', function () {
				expect(loadedTileKeys).to.eql(["0:0:0", "0:0:0", "0:0:0", "0:0:0", "0:0:0"]);
				done();
			});
		});

		it("When true, uses different coords at zoom level 0 for all tiles", function (done) {

			var grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256),
				noWrap: true
			});
			var loadedTileKeys = [];

			grid.createTile = function (coords) {
				loadedTileKeys.push(coords.x + ':' + coords.y + ':' + coords.z);
				return document.createElement('div');
			};

			map.addLayer(grid).setView([0, 0], 0);

			grid.on('load', function () {
				expect(loadedTileKeys).to.eql(['0:0:0', '-1:0:0', '1:0:0', '-2:0:0', '2:0:0']);
				done();
			});
		});

		it("When true and with bounds, loads just one tile at zoom level 0", function (done) {

			var grid = L.gridLayer({
				attribution: 'Grid Layer',
				tileSize: L.point(256, 256),
				bounds: [[-90, -180], [90, 180]],
				noWrap: true
			});
			var loadedTileKeys = [];

			grid.createTile = function (coords) {
				loadedTileKeys.push(coords.x + ':' + coords.y + ':' + coords.z);
				return document.createElement('div');
			};

			map.addLayer(grid).setView([0, 0], 0);

			grid.on('load', function () {
				expect(loadedTileKeys).to.eql(['0:0:0']);
				done();
			});
		});
	});

});


}).to.throwError('Attempted to load an infinite number of tiles');

/*! normalize.css v3.0.1 | MIT License | git.io/normalize */

/**
 * 1. Set default font family to sans-serif.
 * 2. Prevent iOS text size adjust after orientation change, without disabling
 *    user zoom.
 */

html {
  font-family: sans-serif; /* 1 */
  -ms-text-size-adjust: 100%; /* 2 */
  -webkit-text-size-adjust: 100%; /* 2 */
}

/**
 * Remove default margin.
 */

body {
  margin: 0;
}

/* HTML5 display definitions
   ========================================================================== */

/**
 * Correct `block` display not defined for any HTML5 element in IE 8/9.
 * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
 * Correct `block` display not defined for `main` in IE 11.
 */

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
  display: block;
}

/**
 * 1. Correct `inline-block` display not defined in IE 8/9.
 * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
 */

audio,
canvas,
progress,
video {
  display: inline-block; /* 1 */
  vertical-align: baseline; /* 2 */
}

/**
 * Prevent modern browsers from displaying `audio` without controls.
 * Remove excess height in iOS 5 devices.
 */

audio:not([controls]) {
  display: none;
  height: 0;
}

/**
 * Address `[hidden]` styling not present in IE 8/9/10.
 * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
 */

[hidden],
template {
  display: none;
}

/* Links
   ========================================================================== */

/**
 * Remove the gray background color from active links in IE 10.
 */

a {
  background: transparent;
}

/**
 * Improve readability when focused and also mouse hovered in all browsers.
 */

a:active,
a:hover {
  outline: 0;
}

/* Text-level semantics
   ========================================================================== */

/**
 * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
 */

abbr[title] {
  border-bottom: 1px dotted;
  text-decoration: none;
}

/**
 * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
 */

b,
strong {
  font-weight: bold;
}

/**
 * Address styling not present in Safari and Chrome.
 */

dfn {
  font-style: italic;
}

/**
 * Address variable `h1` font-size and margin within `section` and `article`
 * contexts in Firefox 4+, Safari, and Chrome.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/**
 * Address styling not present in IE 8/9.
 */

mark {
  background: #ff0;
  color: #000;
}

/**
 * Address inconsistent and variable font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` affecting `line-height` in all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sup {
  top: -0.5em;
}

sub {
  bottom: -0.25em;
}

/* Embedded content
   ========================================================================== */

/**
 * Remove border when inside `a` element in IE 8/9/10.
 */

img {
  border: 0;
}

/**
 * Correct overflow not hidden in IE 9/10/11.
 */

svg:not(:root) {
  overflow: hidden;
}

/* Grouping content
   ========================================================================== */

/**
 * Address margin not present in IE 8/9 and Safari.
 */

figure {
  margin: 1em 40px;
}

/**
 * Address differences between Firefox and other browsers.
 */

hr {
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  height: 0;
}

/**
 * Contain overflow in all browsers.
 */

pre {
  overflow: auto;
}

/**
 * Address odd `em`-unit font size rendering in all browsers.
 */

code,
kbd,
pre,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}

/* Forms
   ========================================================================== */

/**
 * Known limitation: by default, Chrome and Safari on OS X allow very limited
 * styling of `select`, unless a `border` property is set.
 */

/**
 * 1. Correct color not being inherited.
 *    Known issue: affects color of disabled elements.
 * 2. Correct font properties not being inherited.
 * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
 */

button,
input,
optgroup,
select,
textarea {
  color: inherit; /* 1 */
  font: inherit; /* 2 */
  margin: 0; /* 3 */
}

/**
 * Address `overflow` set to `hidden` in IE 8/9/10/11.
 */

button {
  overflow: visible;
}

/**
 * Address inconsistent `text-transform` inheritance for `button` and `select`.
 * All other form control elements do not inherit `text-transform` values.
 * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
 * Correct `select` style inheritance in Firefox.
 */

button,
select {
  text-transform: none;
}

/**
 * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
 *    and `video` controls.
 * 2. Correct inability to style clickable `input` types in iOS.
 * 3. Improve usability and consistency of cursor style between image-type
 *    `input` and others.
 */

button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
  -webkit-appearance: button; /* 2 */
  cursor: pointer; /* 3 */
}

/**
 * Re-set default cursor for disabled elements.
 */

button[disabled],
html input[disabled] {
  cursor: default;
}

/**
 * Remove inner padding and border in Firefox 4+.
 */

button::-moz-focus-inner,
input::-moz-focus-inner {
  border: 0;
  padding: 0;
}

/**
 * Address Firefox 4+ setting `line-height` on `input` using `!important` in
 * the UA stylesheet.
 */

input {
  line-height: normal;
}

/**
 * It's recommended that you don't attempt to style these elements.
 * Firefox's implementation doesn't respect box-sizing, padding, or width.
 *
 * 1. Address box sizing set to `content-box` in IE 8/9/10.
 * 2. Remove excess padding in IE 8/9/10.
 */

input[type="checkbox"],
input[type="radio"] {
  box-sizing: border-box; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Fix the cursor style for Chrome's increment/decrement buttons. For certain
 * `font-size` values of the `input`, it causes the cursor style of the
 * decrement button to change from `default` to `text`.
 */

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
 * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
 *    (include `-moz` to future-proof).
 */

input[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  -moz-box-sizing: content-box;
  -webkit-box-sizing: content-box; /* 2 */
  box-sizing: content-box;
}

/**
 * Remove inner padding and search cancel button in Safari and Chrome on OS X.
 * Safari (but not Chrome) clips the cancel button when the search input has
 * padding (and `textfield` appearance).
 */

input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * Define consistent border, margin, and padding.
 */

fieldset {
  border: 1px solid #c0c0c0;
  margin: 0 2px;
  padding: 0.35em 0.625em 0.75em;
}

/**
 * 1. Correct `color` not being inherited in IE 8/9/10/11.
 * 2. Remove padding so people aren't caught out if they zero out fieldsets.
 */

legend {
  border: 0; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Remove default vertical scrollbar in IE 8/9/10/11.
 */

textarea {
  overflow: auto;
}

/**
 * Don't inherit the `font-weight` (applied by a rule above).
 * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
 */

optgroup {
  font-weight: bold;
}

/* Tables
   ========================================================================== */

/**
 * Remove most spacing between table cells.
 */

table {
  border-collapse: collapse;
  border-spacing: 0;
}

td,
th {
  padding: 0;
}


---
layout: tutorial_frame
title: Custom Icons Tutorial
---


Contributing to Leaflet
=======================

 1. [Getting Involved](#getting-involved)
 2. [Reporting Bugs](#reporting-bugs)
 3. [Contributing Code](#contributing-code)
 4. [Running the Tests](#running-the-tests)
 5. [Code Coverage](#code-coverage)
 6. [Improving Documentation](#improving-documentation)
 7. [Code of Conduct](#code-of-conduct)
 8. [Thank You](#thank-you)

## Getting Involved

Third-party patches are absolutely essential on our quest to create the best mapping library that will ever exist.
However, they're not the only way to get involved with Leaflet development.
You can help the project tremendously by discovering and [reporting bugs](#reporting-bugs);
[improving documentation](#improving-documentation);
helping others on [Stack Overflow](https://stackoverflow.com/questions/tagged/leaflet),
[GIS Stack Exchange](https://gis.stackexchange.com/questions/tagged/leaflet)
and [GitHub issues](https://github.com/Leaflet/Leaflet/issues);
showing your support for your favorite feature suggestions on [Leaflet UserVoice page](http://leaflet.uservoice.com);
tweeting to [@LeafletJS](http://twitter.com/LeafletJS);
and spreading the word about Leaflet among your colleagues and friends.

## Reporting Bugs

Before reporting a bug on the project's [issues page](https://github.com/Leaflet/Leaflet/issues),
first make sure that your issue is caused by Leaflet, not your application code
(e.g. passing incorrect arguments to methods, etc.).
Second, search the already reported issues for similar cases,
and if it's already reported, just add any additional details in the comments.

After you've made sure that you've found a new Leaflet bug,
here are some tips for creating a helpful report that will make fixing it much easier and quicker:

 * Write a **descriptive, specific title**. Bad: *Problem with polylines*. Good: *Doing X in IE9 causes Z*.
 * Include **browser, OS and Leaflet version** info in the description.
 * Create a **simple test case** that demonstrates the bug (e.g. using [Leaflet playground](http://playground-leaflet.rhcloud.com/)).
 * Check whether the bug can be reproduced in **other browsers**.
 * Check if the bug occurs in the stable version, master, or both.
 * *Bonus tip:* if the bug only appears in the master version but the stable version is fine,
   use `git bisect` to find the exact commit that introduced the bug.

If you just want some help with your project,
try asking on [Stack Overflow](https://stackoverflow.com/questions/tagged/leaflet)
or [GIS Stack Exchange](https://gis.stackexchange.com/questions/tagged/leaflet) instead.

## Contributing Code

### Considerations for Accepting Patches

While we happily accept patches, we're also committed to keeping Leaflet simple, lightweight and blazingly fast.
So bugfixes, performance optimizations and small improvements that don't add a lot of code
are much more likely to get accepted quickly.

Before sending a pull request with a new feature, check if it's been discussed before already
(either on [GitHub issues](https://github.com/Leaflet/Leaflet/issues)
or [Leaflet UserVoice](http://leaflet.uservoice.com/)),
and ask yourself two questions:

 1. Are you sure that this new feature is important enough to justify its presence in the Leaflet core?
    Or will it look better as a plugin in a separate repository?
 2. Is it written in a simple, concise way that doesn't add bulk to the codebase?

If your feature or API improvement did get merged into master,
please consider submitting another pull request with the corresponding [documentation update](#improving-documentation).

### Setting up the Build System

The Leaflet build system uses [Node](http://nodejs.org/), and the [Jake](http://jakejs.com/) Javascript build tool.
To set up the Leaflet build system, install Node then run the following commands in the project root to install Jake:

```
npm install -g jake
npm install
```
or, if you prefer [`yarn`](https://yarnpkg.com/) over `npm`:
```
yarn global add jake
yarn install
```

### Making Changes to Leaflet Source

If you're not yet familiar with the way GitHub works (forking, pull requests, etc.),
be sure to check out the awesome [article about forking](https://help.github.com/articles/fork-a-repo)
on the GitHub Help website — it will get you started quickly.

You should always write each batch of changes (feature, bugfix, etc.) in **its own topic branch**.
Please do not commit to the `master` branch, or your unrelated changes will go into the same pull request.

You should also follow the code style and whitespace conventions of the original codebase.
In particular, use tabs for indentation and spaces for alignment.

Before committing your changes, run `jake lint` to catch any JS errors in the code and fix them.
If you add any new files to the Leaflet source, make sure to also add them to `build/deps.js`
so that the build system knows about them.

Also, please make sure that you have [line endings configured properly](https://help.github.com/articles/dealing-with-line-endings) in Git! Otherwise the diff will show that all lines of a file were changed even if you touched only one.

Happy coding!

### Using RollupJS

The source javascript code for Leaflet is a few dozen files, in the `src/` directory.
But normally, Leaflet is loaded in a web browser as just one javascript file.

In order to create this file, run `npm run-script rollup` or `yarn run rollup`.

You'll find `dist/leaflet-src.js` and `dist/leaflet.js`. The difference is that
`dist/leaflet-src.js` has sourcemaps and it's not uglified, so it's better for
development. `dist/leaflet.js` is uglified and thus is smaller, so it's better
for deployment.

When developing (or bugfixing) core Leaflet functionalities, it's common to use
the webpages in the `debug/` directory, and run the unit tests (`spec/index.html`)
in a graphical browser. This requires regenerating the bundled files quickly.

In order to do so, run `npm run-script watch` or `yarn run rollup`. This will keep
on rebuilding the bundles whenever any source file changes.

## Running the Tests

To run the tests from the command line,
install [PhantomJS](http://phantomjs.org/) (and make sure it's in your `PATH`),
then run:

```
jake test
```

To run all the tests in actual browsers at the same time, you can do:

```
jake test --ff --chrome --safari --ie
```

To run the tests in a browser manually, open `spec/index.html`.

## Code Coverage

To generate a detailed report about test coverage (which helps tremendously when working on test improvements), run:

```
jake test --cov
```

After that, open `coverage/<environment>/index.html` in a browser to see the report.
From there you can click through folders/files to get details on their individual coverage.

## Improving Documentation

The code of the live Leaflet website that contains all documentation and examples is located in the `docs/` directory of the `master` branch
and is automatically generated from a set of HTML and Markdown files by [Jekyll](http://jekyllrb.com/).

The easiest way to make little improvements such as fixing typos without even leaving the browser
is by editing one of the files with the online GitHub editor:
browse the [`docs/ directory`](https://github.com/Leaflet/Leaflet/tree/master/docs),
choose a certain file for editing (e.g. `plugins.md` for the list of Leaflet plugins),
click the Edit button, make changes and follow instructions from there.
Once it gets merged, the changes will immediately appear on the website.

If you need to make edits in a local repository to see how it looks in the process, do the following:

 1. [Install Ruby](http://www.ruby-lang.org/en/) if don't have it yet.
 2. Run `gem install jekyll`.
 3. Enter the directory where you cloned the Leaflet repository
 4. Make sure you are in the `master` branch by running `git checkout master`
 5. Enter the documentation subdirectory by running `cd docs`
 6. Run `jekyll serve --watch`.
 7. Open `localhost:4000` in your web browser.

Now any file changes will be updated when you reload pages automatically.
After committing the changes, just send a pull request.

### API documentation

Since Leaflet 1.0.0-rc1, the API documentation in `reference-1.0.0.html` is handled
via [Leafdoc](https://github.com/Leaflet/Leafdoc). This means that next to the
code for every method, option or property there is a special code comment documenting
that feature. In order to edit the API documentation, just edit these comments in the
source code.

In order to generate the documentation, make sure that the development dependencies
are installed (run either `npm install` or `yarn install`), then just run

```
jake docs
```

and you'll find a `.html` file in the `dist/` directory.

On every release of a new Leaflet version, this file will be generated and copied
over to `docs/reference.html` - there is no need to send pull requests with changes to this file to update the API documentation.

## Code of Conduct

Everyone is invited to participate in the Leaflet community and related projects:
we want to create a welcoming and friendly environment.
Harassment of participants or other unethical and unprofessional behavior will not be tolerated in our spaces.
The [Contributor Covenant](http://contributor-covenant.org/version/1/3/0/)
applies to all projects under the Leaflet organization.
Report any issues to agafonkin@gmail.com.

## Thank You

Not only does your contribution to Leaflet and its community earn our gratitude, but it also makes you AWESOME.
Join [this approved list of awesome people](https://github.com/Leaflet/Leaflet/graphs/contributors)
and help us push the limits of what's possible with online maps!


keep keep keep replace replace replace replace keep keep keep keep keep

isFinite(tileRange.max.y))) { throw new Error('Attempted to load an infinite number of tiles'); }

import {Layer} from '../Layer';
import * as Browser from '../../core/Browser';
import * as Util from '../../core/Util';
import * as DomUtil from '../../dom/DomUtil';
import {Point} from '../../geometry/Point';
import {Bounds} from '../../geometry/Bounds';
import {LatLngBounds, toLatLngBounds as latLngBounds} from '../../geo/LatLngBounds';

/*
 * @class GridLayer
 * @inherits Layer
 * @aka L.GridLayer
 *
 * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
 * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you.
 *
 *
 * @section Synchronous usage
 * @example
 *
 * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile.
 *
 * ```js
 * var CanvasLayer = L.GridLayer.extend({
 *     createTile: function(coords){
 *         // create a <canvas> element for drawing
 *         var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 *
 *         // setup tile width and height according to the options
 *         var size = this.getTileSize();
 *         tile.width = size.x;
 *         tile.height = size.y;
 *
 *         // get a canvas context and draw something on it using coords.x, coords.y and coords.z
 *         var ctx = tile.getContext('2d');
 *
 *         // return the tile so it can be rendered on screen
 *         return tile;
 *     }
 * });
 * ```
 *
 * @section Asynchronous usage
 * @example
 *
 * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback.
 *
 * ```js
 * var CanvasLayer = L.GridLayer.extend({
 *     createTile: function(coords, done){
 *         var error;
 *
 *         // create a <canvas> element for drawing
 *         var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 *
 *         // setup tile width and height according to the options
 *         var size = this.getTileSize();
 *         tile.width = size.x;
 *         tile.height = size.y;
 *
 *         // draw something asynchronously and pass the tile to the done() callback
 *         setTimeout(function() {
 *             done(error, tile);
 *         }, 1000);
 *
 *         return tile;
 *     }
 * });
 * ```
 *
 * @section
 */

export var GridLayer = Layer.extend({

	// @section
	// @aka GridLayer options
	options: {
		// @option tileSize: Number|Point = 256
		// Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
		tileSize: 256,

		// @option opacity: Number = 1.0
		// Opacity of the tiles. Can be used in the `createTile()` function.
		opacity: 1,

		// @option updateWhenIdle: Boolean = depends
		// If `false`, new tiles are loaded during panning, otherwise only after it (for better performance). `true` by default on mobile browsers, otherwise `false`.
		updateWhenIdle: Browser.mobile,

		// @option updateWhenZooming: Boolean = true
		// By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends.
		updateWhenZooming: true,

		// @option updateInterval: Number = 200
		// Tiles will not update more than once every `updateInterval` milliseconds when panning.
		updateInterval: 200,

		// @option zIndex: Number = 1
		// The explicit zIndex of the tile layer.
		zIndex: 1,

		// @option bounds: LatLngBounds = undefined
		// If set, tiles will only be loaded inside the set `LatLngBounds`.
		bounds: null,

		// @option minZoom: Number = 0
		// The minimum zoom level that tiles will be loaded at. By default the entire map.
		minZoom: 0,

		// @option maxZoom: Number = undefined
		// The maximum zoom level that tiles will be loaded at.
		maxZoom: undefined,

		// @option maxNativeZoom: Number = undefined
		// Maximum zoom number the tile source has available. If it is specified,
		// the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
		// from `maxNativeZoom` level and auto-scaled.
		maxNativeZoom: undefined,

		// @option minNativeZoom: Number = undefined
		// Minimum zoom number the tile source has available. If it is specified,
		// the tiles on all zoom levels lower than `minNativeZoom` will be loaded
		// from `minNativeZoom` level and auto-scaled.
		minNativeZoom: undefined,

		// @option noWrap: Boolean = false
		// Whether the layer is wrapped around the antimeridian. If `true`, the
		// GridLayer will only be displayed once at low zoom levels. Has no
		// effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
		// in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
		// tiles outside the CRS limits.
		noWrap: false,

		// @option pane: String = 'tilePane'
		// `Map pane` where the grid layer will be added.
		pane: 'tilePane',

		// @option className: String = ''
		// A custom class name to assign to the tile layer. Empty by default.
		className: '',

		// @option keepBuffer: Number = 2
		// When panning the map, keep this many rows and columns of tiles before unloading them.
		keepBuffer: 2
	},

	initialize: function (options) {
		Util.setOptions(this, options);
	},

	onAdd: function () {
		this._initContainer();

		this._levels = {};
		this._tiles = {};

		this._resetView();
		this._update();
	},

	beforeAdd: function (map) {
		map._addZoomLimit(this);
	},

	onRemove: function (map) {
		this._removeAllTiles();
		DomUtil.remove(this._container);
		map._removeZoomLimit(this);
		this._container = null;
		this._tileZoom = null;
	},

	// @method bringToFront: this
	// Brings the tile layer to the top of all tile layers.
	bringToFront: function () {
		if (this._map) {
			DomUtil.toFront(this._container);
			this._setAutoZIndex(Math.max);
		}
		return this;
	},

	// @method bringToBack: this
	// Brings the tile layer to the bottom of all tile layers.
	bringToBack: function () {
		if (this._map) {
			DomUtil.toBack(this._container);
			this._setAutoZIndex(Math.min);
		}
		return this;
	},

	// @method getContainer: HTMLElement
	// Returns the HTML element that contains the tiles for this layer.
	getContainer: function () {
		return this._container;
	},

	// @method setOpacity(opacity: Number): this
	// Changes the [opacity](#gridlayer-opacity) of the grid layer.
	setOpacity: function (opacity) {
		this.options.opacity = opacity;
		this._updateOpacity();
		return this;
	},

	// @method setZIndex(zIndex: Number): this
	// Changes the [zIndex](#gridlayer-zindex) of the grid layer.
	setZIndex: function (zIndex) {
		this.options.zIndex = zIndex;
		this._updateZIndex();

		return this;
	},

	// @method isLoading: Boolean
	// Returns `true` if any tile in the grid layer has not finished loading.
	isLoading: function () {
		return this._loading;
	},

	// @method redraw: this
	// Causes the layer to clear all the tiles and request them again.
	redraw: function () {
		if (this._map) {
			this._removeAllTiles();
			this._update();
		}
		return this;
	},

	getEvents: function () {
		var events = {
			viewprereset: this._invalidateAll,
			viewreset: this._resetView,
			zoom: this._resetView,
			moveend: this._onMoveEnd
		};

		if (!this.options.updateWhenIdle) {
			// update tiles on move, but not more often than once per given interval
			if (!this._onMove) {
				this._onMove = Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
			}

			events.move = this._onMove;
		}

		if (this._zoomAnimated) {
			events.zoomanim = this._animateZoom;
		}

		return events;
	},

	// @section Extension methods
	// Layers extending `GridLayer` shall reimplement the following method.
	// @method createTile(coords: Object, done?: Function): HTMLElement
	// Called only internally, must be overriden by classes extending `GridLayer`.
	// Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
	// is specified, it must be called when the tile has finished loading and drawing.
	createTile: function () {
		return document.createElement('div');
	},

	// @section
	// @method getTileSize: Point
	// Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
	getTileSize: function () {
		var s = this.options.tileSize;
		return s instanceof Point ? s : new Point(s, s);
	},

	_updateZIndex: function () {
		if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
			this._container.style.zIndex = this.options.zIndex;
		}
	},

	_setAutoZIndex: function (compare) {
		// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)

		var layers = this.getPane().children,
		    edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min

		for (var i = 0, len = layers.length, zIndex; i < len; i++) {

			zIndex = layers[i].style.zIndex;

			if (layers[i] !== this._container && zIndex) {
				edgeZIndex = compare(edgeZIndex, +zIndex);
			}
		}

		if (isFinite(edgeZIndex)) {
			this.options.zIndex = edgeZIndex + compare(-1, 1);
			this._updateZIndex();
		}
	},

	_updateOpacity: function () {
		if (!this._map) { return; }

		// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
		if (Browser.ielt9) { return; }

		DomUtil.setOpacity(this._container, this.options.opacity);

		var now = +new Date(),
		    nextFrame = false,
		    willPrune = false;

		for (var key in this._tiles) {
			var tile = this._tiles[key];
			if (!tile.current || !tile.loaded) { continue; }

			var fade = Math.min(1, (now - tile.loaded) / 200);

			DomUtil.setOpacity(tile.el, fade);
			if (fade < 1) {
				nextFrame = true;
			} else {
				if (tile.active) { willPrune = true; }
				tile.active = true;
			}
		}

		if (willPrune && !this._noPrune) { this._pruneTiles(); }

		if (nextFrame) {
			Util.cancelAnimFrame(this._fadeFrame);
			this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
		}
	},

	_initContainer: function () {
		if (this._container) { return; }

		this._container = DomUtil.create('div', 'leaflet-layer ' + (this.options.className || ''));
		this._updateZIndex();

		if (this.options.opacity < 1) {
			this._updateOpacity();
		}

		this.getPane().appendChild(this._container);
	},

	_updateLevels: function () {

		var zoom = this._tileZoom,
		    maxZoom = this.options.maxZoom;

		if (zoom === undefined) { return undefined; }

		for (var z in this._levels) {
			if (this._levels[z].el.children.length || z === zoom) {
				this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
			} else {
				DomUtil.remove(this._levels[z].el);
				this._removeTilesAtZoom(z);
				delete this._levels[z];
			}
		}

		var level = this._levels[zoom],
		    map = this._map;

		if (!level) {
			level = this._levels[zoom] = {};

			level.el = DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
			level.el.style.zIndex = maxZoom;

			level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
			level.zoom = zoom;

			this._setZoomTransform(level, map.getCenter(), map.getZoom());

			// force the browser to consider the newly added element for transition
			Util.falseFn(level.el.offsetWidth);
		}

		this._level = level;

		return level;
	},

	_pruneTiles: function () {
		if (!this._map) {
			return;
		}

		var key, tile;

		var zoom = this._map.getZoom();
		if (zoom > this.options.maxZoom ||
			zoom < this.options.minZoom) {
			this._removeAllTiles();
			return;
		}

		for (key in this._tiles) {
			tile = this._tiles[key];
			tile.retain = tile.current;
		}

		for (key in this._tiles) {
			tile = this._tiles[key];
			if (tile.current && !tile.active) {
				var coords = tile.coords;
				if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
					this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
				}
			}
		}

		for (key in this._tiles) {
			if (!this._tiles[key].retain) {
				this._removeTile(key);
			}
		}
	},

	_removeTilesAtZoom: function (zoom) {
		for (var key in this._tiles) {
			if (this._tiles[key].coords.z !== zoom) {
				continue;
			}
			this._removeTile(key);
		}
	},

	_removeAllTiles: function () {
		for (var key in this._tiles) {
			this._removeTile(key);
		}
	},

	_invalidateAll: function () {
		for (var z in this._levels) {
			DomUtil.remove(this._levels[z].el);
			delete this._levels[z];
		}
		this._removeAllTiles();

		this._tileZoom = null;
	},

	_retainParent: function (x, y, z, minZoom) {
		var x2 = Math.floor(x / 2),
		    y2 = Math.floor(y / 2),
		    z2 = z - 1,
		    coords2 = new Point(+x2, +y2);
		coords2.z = +z2;

		var key = this._tileCoordsToKey(coords2),
		    tile = this._tiles[key];

		if (tile && tile.active) {
			tile.retain = true;
			return true;

		} else if (tile && tile.loaded) {
			tile.retain = true;
		}

		if (z2 > minZoom) {
			return this._retainParent(x2, y2, z2, minZoom);
		}

		return false;
	},

	_retainChildren: function (x, y, z, maxZoom) {

		for (var i = 2 * x; i < 2 * x + 2; i++) {
			for (var j = 2 * y; j < 2 * y + 2; j++) {

				var coords = new Point(i, j);
				coords.z = z + 1;

				var key = this._tileCoordsToKey(coords),
				    tile = this._tiles[key];

				if (tile && tile.active) {
					tile.retain = true;
					continue;

				} else if (tile && tile.loaded) {
					tile.retain = true;
				}

				if (z + 1 < maxZoom) {
					this._retainChildren(i, j, z + 1, maxZoom);
				}
			}
		}
	},

	_resetView: function (e) {
		var animating = e && (e.pinch || e.flyTo);
		this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
	},

	_animateZoom: function (e) {
		this._setView(e.center, e.zoom, true, e.noUpdate);
	},

	_clampZoom: function (zoom) {
		var options = this.options;

		if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) {
			return options.minNativeZoom;
		}

		if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) {
			return options.maxNativeZoom;
		}

		return zoom;
	},

	_setView: function (center, zoom, noPrune, noUpdate) {
		var tileZoom = this._clampZoom(Math.round(zoom));
		if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
		    (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
			tileZoom = undefined;
		}

		var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);

		if (!noUpdate || tileZoomChanged) {

			this._tileZoom = tileZoom;

			if (this._abortLoading) {
				this._abortLoading();
			}

			this._updateLevels();
			this._resetGrid();

			if (tileZoom !== undefined) {
				this._update(center);
			}

			if (!noPrune) {
				this._pruneTiles();
			}

			// Flag to prevent _updateOpacity from pruning tiles during
			// a zoom anim or a pinch gesture
			this._noPrune = !!noPrune;
		}

		this._setZoomTransforms(center, zoom);
	},

	_setZoomTransforms: function (center, zoom) {
		for (var i in this._levels) {
			this._setZoomTransform(this._levels[i], center, zoom);
		}
	},

	_setZoomTransform: function (level, center, zoom) {
		var scale = this._map.getZoomScale(zoom, level.zoom),
		    translate = level.origin.multiplyBy(scale)
		        .subtract(this._map._getNewPixelOrigin(center, zoom)).round();

		if (Browser.any3d) {
			DomUtil.setTransform(level.el, translate, scale);
		} else {
			DomUtil.setPosition(level.el, translate);
		}
	},

	_resetGrid: function () {
		var map = this._map,
		    crs = map.options.crs,
		    tileSize = this._tileSize = this.getTileSize(),
		    tileZoom = this._tileZoom;

		var bounds = this._map.getPixelWorldBounds(this._tileZoom);
		if (bounds) {
			this._globalTileRange = this._pxBoundsToTileRange(bounds);
		}

		this._wrapX = crs.wrapLng && !this.options.noWrap && [
			Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
			Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
		];
		this._wrapY = crs.wrapLat && !this.options.noWrap && [
			Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
			Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
		];
	},

	_onMoveEnd: function () {
		if (!this._map || this._map._animatingZoom) { return; }

		this._update();
	},

	_getTiledPixelBounds: function (center) {
		var map = this._map,
		    mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
		    scale = map.getZoomScale(mapZoom, this._tileZoom),
		    pixelCenter = map.project(center, this._tileZoom).floor(),
		    halfSize = map.getSize().divideBy(scale * 2);

		return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
	},

	// Private method to load tiles in the grid's active zoom level according to map bounds
	_update: function (center) {
		var map = this._map;
		if (!map) { return; }
		var zoom = this._clampZoom(map.getZoom());

		if (center === undefined) { center = map.getCenter(); }
		if (this._tileZoom === undefined) { return; }	// if out of minzoom/maxzoom

		var pixelBounds = this._getTiledPixelBounds(center),
		    tileRange = this._pxBoundsToTileRange(pixelBounds),
		    tileCenter = tileRange.getCenter(),
		    queue = [],
		    margin = this.options.keepBuffer,
		    noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
		                              tileRange.getTopRight().add([margin, -margin]));

		// Sanity check: panic if the tile range contains Infinity somewhere.
		if (tileRange.min.x === Infinity ||
		    tileRange.min.y === Infinity ||
		    tileRange.max.x === Infinity ||
		    tileRange.max.y === Infinity) { throw new Error('Attempted to load an infinite number of tiles'); }

		for (var key in this._tiles) {
			var c = this._tiles[key].coords;
			if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) {
				this._tiles[key].current = false;
			}
		}

		// _update just loads more tiles. If the tile zoom level differs too much
		// from the map's, let _setView reset levels and prune old tiles.
		if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }

		// create a queue of coordinates to load tiles from
		for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
			for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
				var coords = new Point(i, j);
				coords.z = this._tileZoom;

				if (!this._isValidTile(coords)) { continue; }

				if (!this._tiles[this._tileCoordsToKey(coords)]) {
					queue.push(coords);
				}
			}
		}

		// sort tile queue to load tiles in order of their distance to center
		queue.sort(function (a, b) {
			return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
		});

		if (queue.length !== 0) {
			// if it's the first batch of tiles to load
			if (!this._loading) {
				this._loading = true;
				// @event loading: Event
				// Fired when the grid layer starts loading tiles.
				this.fire('loading');
			}

			// create DOM fragment to append tiles in one batch
			var fragment = document.createDocumentFragment();

			for (i = 0; i < queue.length; i++) {
				this._addTile(queue[i], fragment);
			}

			this._level.el.appendChild(fragment);
		}
	},

	_isValidTile: function (coords) {
		var crs = this._map.options.crs;

		if (!crs.infinite) {
			// don't load tile if it's out of bounds and not wrapped
			var bounds = this._globalTileRange;
			if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
			    (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
		}

		if (!this.options.bounds) { return true; }

		// don't load tile if it doesn't intersect the bounds in options
		var tileBounds = this._tileCoordsToBounds(coords);
		return latLngBounds(this.options.bounds).overlaps(tileBounds);
	},

	_keyToBounds: function (key) {
		return this._tileCoordsToBounds(this._keyToTileCoords(key));
	},

	// converts tile coordinates to its geographical bounds
	_tileCoordsToBounds: function (coords) {

		var map = this._map,
		    tileSize = this.getTileSize(),

		    nwPoint = coords.scaleBy(tileSize),
		    sePoint = nwPoint.add(tileSize),

		    nw = map.unproject(nwPoint, coords.z),
		    se = map.unproject(sePoint, coords.z),
		    bounds = new LatLngBounds(nw, se);

		if (!this.options.noWrap) {
			map.wrapLatLngBounds(bounds);
		}

		return bounds;
	},

	// converts tile coordinates to key for the tile cache
	_tileCoordsToKey: function (coords) {
		return coords.x + ':' + coords.y + ':' + coords.z;
	},

	// converts tile cache key to coordinates
	_keyToTileCoords: function (key) {
		var k = key.split(':'),
		    coords = new Point(+k[0], +k[1]);
		coords.z = +k[2];
		return coords;
	},

	_removeTile: function (key) {
		var tile = this._tiles[key];
		if (!tile) { return; }

		DomUtil.remove(tile.el);

		delete this._tiles[key];

		// @event tileunload: TileEvent
		// Fired when a tile is removed (e.g. when a tile goes off the screen).
		this.fire('tileunload', {
			tile: tile.el,
			coords: this._keyToTileCoords(key)
		});
	},

	_initTile: function (tile) {
		DomUtil.addClass(tile, 'leaflet-tile');

		var tileSize = this.getTileSize();
		tile.style.width = tileSize.x + 'px';
		tile.style.height = tileSize.y + 'px';

		tile.onselectstart = Util.falseFn;
		tile.onmousemove = Util.falseFn;

		// update opacity on tiles in IE7-8 because of filter inheritance problems
		if (Browser.ielt9 && this.options.opacity < 1) {
			DomUtil.setOpacity(tile, this.options.opacity);
		}

		// without this hack, tiles disappear after zoom on Chrome for Android
		// https://github.com/Leaflet/Leaflet/issues/2078
		if (Browser.android && !Browser.android23) {
			tile.style.WebkitBackfaceVisibility = 'hidden';
		}
	},

	_addTile: function (coords, container) {
		var tilePos = this._getTilePos(coords),
		    key = this._tileCoordsToKey(coords);

		var tile = this.createTile(this._wrapCoords(coords), Util.bind(this._tileReady, this, coords));

		this._initTile(tile);

		// if createTile is defined with a second argument ("done" callback),
		// we know that tile is async and will be ready later; otherwise
		if (this.createTile.length < 2) {
			// mark tile as ready, but delay one frame for opacity animation to happen
			Util.requestAnimFrame(Util.bind(this._tileReady, this, coords, null, tile));
		}

		DomUtil.setPosition(tile, tilePos);

		// save tile in cache
		this._tiles[key] = {
			el: tile,
			coords: coords,
			current: true
		};

		container.appendChild(tile);
		// @event tileloadstart: TileEvent
		// Fired when a tile is requested and starts loading.
		this.fire('tileloadstart', {
			tile: tile,
			coords: coords
		});
	},

	_tileReady: function (coords, err, tile) {
		if (!this._map) { return; }

		if (err) {
			// @event tileerror: TileErrorEvent
			// Fired when there is an error loading a tile.
			this.fire('tileerror', {
				error: err,
				tile: tile,
				coords: coords
			});
		}

		var key = this._tileCoordsToKey(coords);

		tile = this._tiles[key];
		if (!tile) { return; }

		tile.loaded = +new Date();
		if (this._map._fadeAnimated) {
			DomUtil.setOpacity(tile.el, 0);
			Util.cancelAnimFrame(this._fadeFrame);
			this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
		} else {
			tile.active = true;
			this._pruneTiles();
		}

		if (!err) {
			DomUtil.addClass(tile.el, 'leaflet-tile-loaded');

			// @event tileload: TileEvent
			// Fired when a tile loads.
			this.fire('tileload', {
				tile: tile.el,
				coords: coords
			});
		}

		if (this._noTilesToLoad()) {
			this._loading = false;
			// @event load: Event
			// Fired when the grid layer loaded all visible tiles.
			this.fire('load');

			if (Browser.ielt9 || !this._map._fadeAnimated) {
				Util.requestAnimFrame(this._pruneTiles, this);
			} else {
				// Wait a bit more than 0.2 secs (the duration of the tile fade-in)
				// to trigger a pruning.
				setTimeout(Util.bind(this._pruneTiles, this), 250);
			}
		}
	},

	_getTilePos: function (coords) {
		return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
	},

	_wrapCoords: function (coords) {
		var newCoords = new Point(
			this._wrapX ? Util.wrapNum(coords.x, this._wrapX) : coords.x,
			this._wrapY ? Util.wrapNum(coords.y, this._wrapY) : coords.y);
		newCoords.z = coords.z;
		return newCoords;
	},

	_pxBoundsToTileRange: function (bounds) {
		var tileSize = this.getTileSize();
		return new Bounds(
			bounds.min.unscaleBy(tileSize).floor(),
			bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
	},

	_noTilesToLoad: function () {
		for (var key in this._tiles) {
			if (!this._tiles[key].loaded) { return false; }
		}
		return true;
	}
});

// @factory L.gridLayer(options?: GridLayer options)
// Creates a new instance of GridLayer with the supplied options.
export function gridLayer(options) {
	return new GridLayer(options);
}


import {Polyline} from './Polyline';
import {LatLng} from '../../geo/LatLng';
import * as LineUtil from '../../geometry/LineUtil';
import {Point} from '../../geometry/Point';
import {Bounds} from '../../geometry/Bounds';
import * as PolyUtil from '../../geometry/PolyUtil';

/*
 * @class Polygon
 * @aka L.Polygon
 * @inherits Polyline
 *
 * A class for drawing polygon overlays on a map. Extends `Polyline`.
 *
 * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points.
 *
 *
 * @example
 *
 * ```js
 * // create a red polygon from an array of LatLng points
 * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
 *
 * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
 *
 * // zoom the map to the polygon
 * map.fitBounds(polygon.getBounds());
 * ```
 *
 * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape:
 *
 * ```js
 * var latlngs = [
 *   [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
 *   [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
 * ];
 * ```
 *
 * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape.
 *
 * ```js
 * var latlngs = [
 *   [ // first polygon
 *     [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
 *     [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
 *   ],
 *   [ // second polygon
 *     [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
 *   ]
 * ];
 * ```
 */

export var Polygon = Polyline.extend({

	options: {
		fill: true
	},

	isEmpty: function () {
		return !this._latlngs.length || !this._latlngs[0].length;
	},

	getCenter: function () {
		// throws error when not yet added to map as this center calculation requires projected coordinates
		if (!this._map) {
			throw new Error('Must add layer to map before using getCenter()');
		}

		var i, j, p1, p2, f, area, x, y, center,
		    points = this._rings[0],
		    len = points.length;

		if (!len) { return null; }

		// polygon centroid algorithm; only uses the first ring if there are multiple

		area = x = y = 0;

		for (i = 0, j = len - 1; i < len; j = i++) {
			p1 = points[i];
			p2 = points[j];

			f = p1.y * p2.x - p2.y * p1.x;
			x += (p1.x + p2.x) * f;
			y += (p1.y + p2.y) * f;
			area += f * 3;
		}

		if (area === 0) {
			// Polygon is so small that all points are on same pixel.
			center = points[0];
		} else {
			center = [x / area, y / area];
		}
		return this._map.layerPointToLatLng(center);
	},

	_convertLatLngs: function (latlngs) {
		var result = Polyline.prototype._convertLatLngs.call(this, latlngs),
		    len = result.length;

		// remove last point if it equals first one
		if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) {
			result.pop();
		}
		return result;
	},

	_setLatLngs: function (latlngs) {
		Polyline.prototype._setLatLngs.call(this, latlngs);
		if (LineUtil._flat(this._latlngs)) {
			this._latlngs = [this._latlngs];
		}
	},

	_defaultShape: function () {
		return LineUtil._flat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0];
	},

	_clipPoints: function () {
		// polygons need a different clipping algorithm so we redefine that

		var bounds = this._renderer._bounds,
		    w = this.options.weight,
		    p = new Point(w, w);

		// increase clip padding by stroke width to avoid stroke on clip edges
		bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p));

		this._parts = [];
		if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
			return;
		}

		if (this.options.noClip) {
			this._parts = this._rings;
			return;
		}

		for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
			clipped = PolyUtil.clipPolygon(this._rings[i], bounds, true);
			if (clipped.length) {
				this._parts.push(clipped);
			}
		}
	},

	_updatePath: function () {
		this._renderer._updatePoly(this, true);
	},

	// Needed by the `Canvas` renderer for interactivity
	_containsPoint: function (p) {
		var inside = false,
		    part, p1, p2, i, j, k, len, len2;

		if (!this._pxBounds.contains(p)) { return false; }

		// ray casting algorithm for detecting if point is in polygon
		for (i = 0, len = this._parts.length; i < len; i++) {
			part = this._parts[i];

			for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
				p1 = part[j];
				p2 = part[k];

				if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
					inside = !inside;
				}
			}
		}

		// also check if it's on polygon stroke
		return inside || Polyline.prototype._containsPoint.call(this, p, true);
	}

});

// @factory L.polygon(latlngs: LatLng[], options?: Polyline options)
export function polygon(latlngs, options) {
	return new Polygon(latlngs, options);
}


leaflet

JavaScript library for mobile-friendly interactive maps

.* CHANGELOG.json FAQ.md debug spec src build


---
layout: tutorial_v2
title: Leaflet on Mobile
---

## Leaflet on Mobile

In this example, you'll learn how to create a fullscreen map tuned for mobile devices like iPhone, iPad or Android phones, and how to easily detect and use the current user location.

{% include frame.html url="example.html" %}

### Preparing the page

First we'll take a look at the HTML & CSS code of the page. To make our map `div` element stretch to all available space (fullscreen), we can use the following CSS code:

{: .css}
	body {
		padding: 0;
		margin: 0;
	}
	html, body, #map {
		height: 100vh;
		width: 100vw;
	}

Also, we need to tell the mobile browser to disable unwanted scaling of the page and set it to its actual size by placing the following line in the `head` section or our HTML page:

	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

### Initializing the map

We'll now initialize the map in the JavaScript code like we did in the [quick start guide](../quick-start/), showing the whole world:

var map = L.map('map').fitWorld();

L.tileLayer('https://api.tiles.mapbox.com/v4/MapID/997/256/{z}/{x}/{y}.png?access_token={accessToken}', {
	attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
	maxZoom: 18
}).addTo(map);

### Geolocation

Leaflet has a very handy shortcut for zooming the map view to the detected location --- `locate` method with the `setView` option, replacing the usual `setView` method in the code:

	map.locate({setView: true, maxZoom: 16});

Here we specify 16 as the maximum zoom when setting the map view automatically. As soon as the user agrees to share its location and it's detected by the browser, the map will set the view to it. Now we have a working fullscreen mobile map! But what if we need to do something after the geolocation completed? Here's what the `locationfound` and `locationerror` events are for. Let's for example add a marker in the detected location, showing accuracy in a popup, by adding an event listener to `locationfound` event before the `locateAndSetView` call:

	function onLocationFound(e) {
		var radius = e.accuracy / 2;

		L.marker(e.latlng).addTo(map)
			.bindPopup("You are within " + radius + " meters from this point").openPopup();

		L.circle(e.latlng, radius).addTo(map);
	}

	map.on('locationfound', onLocationFound);

Excellent! But it would also be nice to show an error message if the geolocation failed:

	function onLocationError(e) {
		alert(e.message);
	}

	map.on('locationerror', onLocationError);

If you have `setView` option set to true and the geolocation failed, it will set the view to the whole world.

Now the example is complete --- try it on your mobile phone: [View the full example →](example.html)

Next steps would be to take a look at the detailed [documentation](../../reference.html) and browse [other examples](../../examples.html).


keep keep keep keep replace keep keep keep keep

import { diffProps, setProperty } from './props';

import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
import { Component } from '../component';
import { Fragment } from '../create-element';
import { diffChildren } from './children';
import { diffProps } from './props';
import { assign, removeNode } from '../util';
import options from '../options';

/**
 * Diff two virtual nodes and apply proper changes to the DOM
 * @param {import('../internal').PreactElement} parentDom The parent of the DOM element
 * @param {import('../internal').VNode} newVNode The new virtual node
 * @param {import('../internal').VNode} oldVNode The old virtual node
 * @param {object} globalContext The current context object. Modified by getChildContext
 * @param {boolean} isSvg Whether or not this element is an SVG node
 * @param {Array<import('../internal').PreactElement>} excessDomChildren
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {Element | Text} oldDom The current attached DOM
 * element any new dom elements should be placed around. Likely `null` on first
 * render (except when hydrating). Can be a sibling DOM element when diffing
 * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
 * @param {boolean} [isHydrating] Whether or not we are in hydration
 */
export function diff(
	parentDom,
	newVNode,
	oldVNode,
	globalContext,
	isSvg,
	excessDomChildren,
	commitQueue,
	oldDom,
	isHydrating
) {
	let tmp,
		newType = newVNode.type;

	// When passing through createElement it assigns the object
	// constructor as undefined. This to prevent JSON-injection.
	if (newVNode.constructor !== undefined) return null;

	if ((tmp = options._diff)) tmp(newVNode);

	try {
		outer: if (typeof newType == 'function') {
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;

			// Necessary for createContext api. Setting this property will pass
			// the context value as `this.context` just for this component.
			tmp = newType.contextType;
			let provider = tmp && globalContext[tmp._id];
			let componentContext = tmp
				? provider
					? provider.props.value
					: tmp._defaultValue
				: globalContext;

			// Get component and set it to `c`
			if (oldVNode._component) {
				c = newVNode._component = oldVNode._component;
				clearProcessingException = c._processingException = c._pendingError;
			} else {
				// Instantiate the new component
				if ('prototype' in newType && newType.prototype.render) {
					newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
				} else {
					newVNode._component = c = new Component(newProps, componentContext);
					c.constructor = newType;
					c.render = doRender;
				}
				if (provider) provider.sub(c);

				c.props = newProps;
				if (!c.state) c.state = {};
				c.context = componentContext;
				c._globalContext = globalContext;
				isNew = c._dirty = true;
				c._renderCallbacks = [];
			}

			// Invoke getDerivedStateFromProps
			if (c._nextState == null) {
				c._nextState = c.state;
			}
			if (newType.getDerivedStateFromProps != null) {
				if (c._nextState == c.state) {
					c._nextState = assign({}, c._nextState);
				}

				assign(
					c._nextState,
					newType.getDerivedStateFromProps(newProps, c._nextState)
				);
			}

			oldProps = c.props;
			oldState = c.state;

			// Invoke pre-render lifecycle methods
			if (isNew) {
				if (
					newType.getDerivedStateFromProps == null &&
					c.componentWillMount != null
				) {
					c.componentWillMount();
				}

				if (c.componentDidMount != null) {
					c._renderCallbacks.push(c.componentDidMount);
				}
			} else {
				if (
					newType.getDerivedStateFromProps == null &&
					newProps !== oldProps &&
					c.componentWillReceiveProps != null
				) {
					c.componentWillReceiveProps(newProps, componentContext);
				}

				if (
					(!c._force &&
						c.shouldComponentUpdate != null &&
						c.shouldComponentUpdate(
							newProps,
							c._nextState,
							componentContext
						) === false) ||
					(newVNode._original === oldVNode._original && !c._processingException)
				) {
					c.props = newProps;
					c.state = c._nextState;
					// More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
					if (newVNode._original !== oldVNode._original) c._dirty = false;
					c._vnode = newVNode;
					newVNode._dom = oldVNode._dom;
					newVNode._children = oldVNode._children;
					if (c._renderCallbacks.length) {
						commitQueue.push(c);
					}

					for (tmp = 0; tmp < newVNode._children.length; tmp++) {
						if (newVNode._children[tmp]) {
							newVNode._children[tmp]._parent = newVNode;
						}
					}

					break outer;
				}

				if (c.componentWillUpdate != null) {
					c.componentWillUpdate(newProps, c._nextState, componentContext);
				}

				if (c.componentDidUpdate != null) {
					c._renderCallbacks.push(() => {
						c.componentDidUpdate(oldProps, oldState, snapshot);
					});
				}
			}

			c.context = componentContext;
			c.props = newProps;
			c.state = c._nextState;

			if ((tmp = options._render)) tmp(newVNode);

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

			tmp = c.render(c.props, c.state, c.context);
			let isTopLevelFragment =
				tmp != null && tmp.type == Fragment && tmp.key == null;
			newVNode._children = isTopLevelFragment
				? tmp.props.children
				: Array.isArray(tmp)
				? tmp
				: [tmp];

			if (c.getChildContext != null) {
				globalContext = assign(assign({}, globalContext), c.getChildContext());
			}

			if (!isNew && c.getSnapshotBeforeUpdate != null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

			diffChildren(
				parentDom,
				newVNode,
				oldVNode,
				globalContext,
				isSvg,
				excessDomChildren,
				commitQueue,
				oldDom,
				isHydrating
			);

			c.base = newVNode._dom;

			if (c._renderCallbacks.length) {
				commitQueue.push(c);
			}

			if (clearProcessingException) {
				c._pendingError = c._processingException = null;
			}

			c._force = false;
		} else if (
			excessDomChildren == null &&
			newVNode._original === oldVNode._original
		) {
			newVNode._children = oldVNode._children;
			newVNode._dom = oldVNode._dom;
		} else {
			newVNode._dom = diffElementNodes(
				oldVNode._dom,
				newVNode,
				oldVNode,
				globalContext,
				isSvg,
				excessDomChildren,
				commitQueue,
				isHydrating
			);
		}

		if ((tmp = options.diffed)) tmp(newVNode);
	} catch (e) {
		newVNode._original = null;
		options._catchError(e, newVNode, oldVNode);
	}

	return newVNode._dom;
}

/**
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {import('../internal').VNode} root
 */
export function commitRoot(commitQueue, root) {
	if (options._commit) options._commit(root, commitQueue);

	commitQueue.some(c => {
		try {
			commitQueue = c._renderCallbacks;
			c._renderCallbacks = [];
			commitQueue.some(cb => {
				cb.call(c);
			});
		} catch (e) {
			options._catchError(e, c._vnode);
		}
	});
}

/**
 * Diff two virtual nodes representing DOM element
 * @param {import('../internal').PreactElement} dom The DOM element representing
 * the virtual nodes being diffed
 * @param {import('../internal').VNode} newVNode The new virtual node
 * @param {import('../internal').VNode} oldVNode The old virtual node
 * @param {object} globalContext The current context object
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
 * @param {*} excessDomChildren
 * @param {Array<import('../internal').Component>} commitQueue List of components
 * which have callbacks to invoke in commitRoot
 * @param {boolean} isHydrating Whether or not we are in hydration
 * @returns {import('../internal').PreactElement}
 */
function diffElementNodes(
	dom,
	newVNode,
	oldVNode,
	globalContext,
	isSvg,
	excessDomChildren,
	commitQueue,
	isHydrating
) {
	let i;
	let oldProps = oldVNode.props;
	let newProps = newVNode.props;

	// Tracks entering and exiting SVG namespace when descending through the tree.
	isSvg = newVNode.type === 'svg' || isSvg;

	if (excessDomChildren != null) {
		for (i = 0; i < excessDomChildren.length; i++) {
			const child = excessDomChildren[i];

			// if newVNode matches an element in excessDomChildren or the `dom`
			// argument matches an element in excessDomChildren, remove it from
			// excessDomChildren so it isn't later removed in diffChildren
			if (
				child != null &&
				((newVNode.type === null
					? child.nodeType === 3
					: child.localName === newVNode.type) ||
					dom == child)
			) {
				dom = child;
				excessDomChildren[i] = null;
				break;
			}
		}
	}

	if (dom == null) {
		if (newVNode.type === null) {
			return document.createTextNode(newProps);
		}

		dom = isSvg
			? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
			: document.createElement(
					newVNode.type,
					newProps.is && { is: newProps.is }
			  );
		// we created a new parent, so none of the previously attached children can be reused:
		excessDomChildren = null;
		// we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate
		isHydrating = false;
	}

	if (newVNode.type === null) {
		if (oldProps !== newProps && dom.data != newProps) {
			dom.data = newProps;
		}
	} else {
		if (excessDomChildren != null) {
			excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
		}

		oldProps = oldVNode.props || EMPTY_OBJ;

		let oldHtml = oldProps.dangerouslySetInnerHTML;
		let newHtml = newProps.dangerouslySetInnerHTML;

		// During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
		// @TODO we should warn in debug mode when props don't match here.
		if (!isHydrating) {
			if (oldProps === EMPTY_OBJ) {
				oldProps = {};
				for (let i = 0; i < dom.attributes.length; i++) {
					oldProps[dom.attributes[i].name] = dom.attributes[i].value;
				}
			}

			if (newHtml || oldHtml) {
				// Avoid re-applying the same '__html' if it did not changed between re-render
				if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
					dom.innerHTML = (newHtml && newHtml.__html) || '';
				}
			}
		}

		diffProps(dom, newProps, oldProps, isSvg, isHydrating);

		// If the new vnode didn't have dangerouslySetInnerHTML, diff its children
		if (newHtml) {
			newVNode._children = [];
		}
		else {
			newVNode._children = newVNode.props.children;
			diffChildren(
				dom,
				newVNode,
				oldVNode,
				globalContext,
				newVNode.type === 'foreignObject' ? false : isSvg,
				excessDomChildren,
				commitQueue,
				EMPTY_OBJ,
				isHydrating
			);
		}

		// (as above, don't diff props during hydration)
		if (!isHydrating) {
			if (
				'value' in newProps &&
				newProps.value !== undefined &&
				newProps.value !== dom.value
			) {
				dom.value = newProps.value == null ? '' : newProps.value;
			}
			if (
				'checked' in newProps &&
				newProps.checked !== undefined &&
				newProps.checked !== dom.checked
			) {
				dom.checked = newProps.checked;
			}
		}
	}

	return dom;
}

/**
 * Invoke or update a ref, depending on whether it is a function or object ref.
 * @param {object|function} ref
 * @param {any} value
 * @param {import('../internal').VNode} vnode
 */
export function applyRef(ref, value, vnode) {
	try {
		if (typeof ref == 'function') ref(value);
		else ref.current = value;
	} catch (e) {
		options._catchError(e, vnode);
	}
}

/**
 * Unmount a virtual node from the tree and apply DOM changes
 * @param {import('../internal').VNode} vnode The virtual node to unmount
 * @param {import('../internal').VNode} parentVNode The parent of the VNode that
 * initiated the unmount
 * @param {boolean} [skipRemove] Flag that indicates that a parent node of the
 * current element is already detached from the DOM.
 */
export function unmount(vnode, parentVNode, skipRemove) {
	let r;
	if (options.unmount) options.unmount(vnode);

	if ((r = vnode.ref)) {
		if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode);
	}

	let dom;
	if (!skipRemove && typeof vnode.type != 'function') {
		skipRemove = (dom = vnode._dom) != null;
	}

	// Must be set to `undefined` to properly clean up `_nextDom`
	// for which `null` is a valid value. See comment in `create-element.js`
	vnode._dom = vnode._nextDom = undefined;

	if ((r = vnode._component) != null) {
		if (r.componentWillUnmount) {
			try {
				r.componentWillUnmount();
			} catch (e) {
				options._catchError(e, parentVNode);
			}
		}

		r.base = r._parentDom = null;
	}

	if ((r = vnode._children)) {
		for (let i = 0; i < r.length; i++) {
			if (r[i]) unmount(r[i], parentVNode, skipRemove);
		}
	}

	if (dom != null) removeNode(dom);
}

/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
	return this.constructor(props, context);
}


import { createElement, lazy } from 'react';

const pause = timeout =>
	new Promise(d => setTimeout(d, timeout), console.log(timeout));

const SubComponent = lazy(() =>
	pause(Math.random() * 1000).then(() => import('./subcomponent.js'))
);

export default function ComponentContainer({ appearance }) {
	return (
		<div>
			GenerateComponents (component #{appearance})
			<SubComponent />
		</div>
	);
}


import {
	createElement,
	h,
	createContext,
	Component,
	Fragment,
	render,
	hydrate,
	cloneElement,
	options
} from '../../';
import { expect } from 'chai';

describe('preact', () => {
	it('should be available as named exports', () => {
		expect(h).to.be.a('function');
		expect(createElement).to.be.a('function');
		expect(Component).to.be.a('function');
		expect(Fragment).to.exist;
		expect(render).to.be.a('function');
		expect(hydrate).to.be.a('function');
		expect(cloneElement).to.be.a('function');
		expect(createContext).to.be.a('function');
		expect(options).to.exist.and.be.an('object');
	});
});


html, body {
	height: 100%;
	margin: 0;
	background: #eee;
	font: 400 16px/1.3 'Helvetica Neue',helvetica,sans-serif;
	text-rendering: optimizeSpeed;
	color: #444;
}

.app {
	display: block;
	flex-direction: column;
	height: 100%;

	> header {
		flex: 0;
		background: #f9f9f9;
		box-shadow: inset 0 -.5px 0 0 rgba(0,0,0,0.2), 0 .5px 0 0 rgba(255,255,255,0.6);

		nav {
			display: inline-block;
			padding: 4px 7px;

			a {
				display: inline-block;
				margin: 2px;
				padding: 4px 10px;
				background-color: rgba(255,255,255,0);
				border-radius: 1em;
				color: #6b1d8f;
				text-decoration: none;
				// transition: all 250ms ease;
				transition: all 250ms cubic-bezier(.2,0,.4,2);
				&:hover {
					background-color: rgba(255,255,255,1);
					box-shadow: 0 0 0 2px #6b1d8f;
				}
				&.active {
					background-color: #6b1d8f;
					color: white;
				}
			}
		}
	}

	> main {
		flex: 1;
		padding: 10px;
	}
}

h1 {
	margin: 0;
	color: #6b1d8f;
	font-weight: 300;
	font-size: 250%;
}

input, textarea {
	box-sizing: border-box;
	margin: 1px;
	padding: .25em .5em;
	background: #fff;
	border: 1px solid #999;
	border-radius: 3px;
	font: inherit;
	color: #000;
	outline: none;

	&:focus {
		border-color: #6b1d8f;
	}
}

button, input[type="submit"], input[type="reset"], input[type="button"] {
	box-sizing: border-box;
	margin: 1px;
	padding: .25em .8em;
	background: #6b1d8f;
	border: 1px solid #6b1d8f;
	// border: none;
	border-radius: 1.5em;
	font: inherit;
	color: white;
	outline: none;
	cursor: pointer;
}

.cursor {
	position: absolute;
	left: 0;
	top: 0;
	width: 8px;
	height: 8px;
	margin: -5px 0 0 -5px;
	border: 2px solid #F00;
	border-radius: 50%;
	transform-origin: 50% 50%;
	pointer-events: none;
	overflow: hidden;
	font-size: 9px;
	line-height: 25px;
	text-indent: 15px;
	white-space: nowrap;

	&:not(.label) {
		contain: strict;
	}

	&.label {
		overflow: visible;
	}

	// &.big {
	// 	transform: scale(2);
	// 	// width: 24px;
	// 	// height: 24px;
	// 	// margin: -13px 0 0 -13px;
	// }

	.label {
		position: absolute;
		left: 0;
		top: 0;
		//transform: translateZ(0);
		// z-index: 10;
	}
}

.animation-picker {
	position: fixed;
	display: inline-block;
	right: 0;
	top: 0;
	padding: 10px;
	background: #000;
	color: #BBB;
	z-index: 1000;

	select {
		font-size: 100%;
		margin-left: 5px;
	}
}


// If the new vnode didn't have dangerouslySetInnerHTML, diff its children

keep keep keep keep replace replace keep keep keep


import { createElement } from 'react';

export default function SubComponent({ onClick }) {
	return
Lazy loaded sub component
;
}


# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: preact
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL