keep keep keep replace replace replace replace keep

L.Map.include(!L.DomUtil.TRANSITION ? {} : {
	_zoomToIfCenterInView: function(center, zoom, centerOffset) {

		if (this._animatingZoom) { return true; }
		if (!this.options.zoomAnimation) { return false; }

		var zoomDelta = zoom - this._zoom,
			scale = Math.pow(2, zoomDelta),
			offset = centerOffset.divideBy(1 - 1/scale);

		//if offset does not exceed half of the view
		if (!this._offsetIsWithinView(offset, 1)) { return false; }

		this._mapPane.className += ' leaflet-zoom-anim';

		var centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
			origin = centerPoint.add(offset);

		this._prepareTileBg();

		this._runAnimation(center, zoom, scale, origin);

		return true;
	},

	_runAnimation: function(center, zoom, scale, origin) {
		this._animatingZoom = true;

		this._animateToCenter = center;
		this._animateToZoom = zoom;

		var transform = L.DomUtil.TRANSFORM;

		//dumb FireFox hack, I have no idea why this magic zero translate fixes the scale transition problem
		if (L.Browser.gecko || window.opera) {
			this._tileBg.style[transform] += ' translate(0,0)';
		}

		var scaleStr;

		// Android doesn't like translate/scale chains, transformOrigin + scale works better but
		// it breaks touch zoom which Anroid doesn't support anyway, so that's a really ugly hack
		// TODO work around this prettier
		if (L.Browser.android) {
			this._tileBg.style[transform + 'Origin'] = origin.x + 'px ' + origin.y + 'px';
			scaleStr = 'scale(' + scale + ')';
		} else {
			scaleStr = L.DomUtil.getScaleString(scale, origin);
		}

		L.Util.falseFn(this._tileBg.offsetWidth); //hack to make sure transform is updated before running animation

		var options = {};
		options[transform] = this._tileBg.style[transform] + ' ' + scaleStr;
		this._tileBg.transition.run(options);
	},

	_prepareTileBg: function() {
		if (!this._tileBg) {
			this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
			this._tileBg.style.zIndex = 1;
		}

		var tilePane = this._tilePane,
			tileBg = this._tileBg;

		// prepare the background pane to become the main tile pane
		tileBg.innerHTML = '';
		tileBg.style[L.DomUtil.TRANSFORM] = '';
		tileBg.style.display = 'none';

		// tells tile layers to reinitialize their containers
		tileBg.empty = true;
		tilePane.empty = false;

		this._tilePane = this._panes.tilePane = tileBg;
		this._tileBg = tilePane;

		if (!this._tileBg.transition) {
			this._tileBg.transition = new L.Transition(this._tileBg, {duration: 0.3, easing: 'cubic-bezier(0.25,0.1,0.25,0.75)'});
			this._tileBg.transition.on('end', this._onZoomTransitionEnd, this);
		}

		this._removeExcessiveBgTiles();
	},

	// stops loading all tiles in the background layer and removes tiles that not in the current bbox
	_removeExcessiveBgTiles: function() {
		var tiles = [].slice.call(this._tileBg.getElementsByTagName('img')),
			mapRect = this._container.getBoundingClientRect();

		for (var i = 0, len = tiles.length; i < len; i++) {
			var tileRect = tiles[i].getBoundingClientRect();

			if (!tiles[i].complete ||
				(tileRect.right <= mapRect.left) ||
				(tileRect.left >= mapRect.right) ||
				(tileRect.top >= mapRect.bottom) ||
				(tileRect.bottom <= mapRect.top)) {

				tiles[i].src = '';
				tiles[i].parentNode.removeChild(tiles[i]);
			}
		}
	},

	_onZoomTransitionEnd: function() {
		this._restoreTileFront();

		L.Util.falseFn(this._tileBg.offsetWidth);
		this._resetView(this._animateToCenter, this._animateToZoom, true);

		//TODO clear tileBg on map layersload

		this._mapPane.className = this._mapPane.className.replace(' leaflet-zoom-anim', ''); //TODO toggleClass util
		this._animatingZoom = false;
	},

	_restoreTileFront: function() {
		this._tilePane.style.display = '';
		this._tilePane.style.zIndex = 2;
		this._tileBg.style.zIndex = 1;
	},

	_clearTileBg: function() {
		if (!this._animatingZoom) {
			this._tileBg.innerHTML = '';
		}
	}
});


<!DOCTYPE html>

	<title>Leaflet debug page</title>

	<link rel="stylesheet" href="../../dist/leaflet.css" />

	<link rel="stylesheet" href="../css/screen.css" />


describe('L#noConflict', function() {
	it('should restore the previous L value and return Leaflet namespace', function(){

		expect(L.VERSION).toBeDefined();

		var L2 = L.noConflict();

		expect(L).toEqual('test');
		expect(L2.VERSION).toBeDefined();

		this.after(function() {
			window.L = L2;
		});
	});
});


<img src="http://leaflet.cloudmade.com/docs/images/logo.png" alt="Leaflet" />

Leaflet is a modern, lightweight BSD-licensed JavaScript library for making tile-based interactive maps for both desktop and mobile web browsers, developed by [CloudMade](http://cloudmade.com) to form the core of its next generation JavaScript API.

It is built from the ground up to work efficiently and smoothly on both platforms, utilizing cutting-edge technologies included in HTML5. Its top priorities are usability, performance and small size, [A-grade](http://developer.yahoo.com/yui/articles/gbs/) browser support, flexibility and easy to use API. The OOP-based code of the library is designed to be modular, extensible and very easy to understand.

Check out the website for more information: [leaflet.cloudmade.com](http://leaflet.cloudmade.com)

## Contributing to Leaflet
Let's make the best open-source library for maps that can possibly exist! Please send your pull requests to [Vladimir Agafonkin](http://github.com/mourner) (Leaflet maintainer) - we'll be happy to accept your contributions! [List of Leaflet contributors](http://github.com/CloudMade/Leaflet/contributors)


// prepare the background pane to become the main tile pane


/*
 * L.Circle is a circle overlay (with a certain radius in meters).
 */

L.Circle = L.Path.extend({
	initialize: function(latlng, radius, options) {
		L.Path.prototype.initialize.call(this, options);

		this._latlng = latlng;
		this._mRadius = radius;
	},

	options: {
		fill: true
	},

	projectLatlngs: function() {
		var equatorLength = 40075017,
			scale = this._map.options.scaling(this._map._zoom);

		this._point = this._map.latLngToLayerPoint(this._latlng);
		this._radius = (this._mRadius / equatorLength) * scale;
	},

	getPathString: function() {
		var p = this._point,
			r = this._radius;

		if (L.Path.SVG) {
			return "M" + p.x + "," + (p.y - r) +
					"A" + r + "," + r + ",0,1,1," +
					(p.x - 0.1) + "," + (p.y - r) + " z";
		} else {
			p._round();
			r = Math.round(r);
			return "AL " + p.x + "," + p.y + " " + r + "," + r + " 0," + (65535 * 360);
		}
	}
});


body {
  font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
}

.jasmine_reporter a:visited, .jasmine_reporter a {
  color: #303;
}

.jasmine_reporter a:hover, .jasmine_reporter a:active {
  color: blue;
}

.run_spec {
  float:right;
  padding-right: 5px;
  font-size: .8em;
  text-decoration: none;
}

.jasmine_reporter {
  margin: 0 5px;
}

.banner {
  color: #303;
  background-color: #fef;
  padding: 5px;
}

.logo {
  float: left;
  font-size: 1.1em;
  padding-left: 5px;
}

.logo .version {
  font-size: .6em;
  padding-left: 1em;
}

.runner.running {
  background-color: yellow;
}

.options {
  text-align: right;
  font-size: .8em;
}

.suite {
  border: 1px outset gray;
  margin: 5px 0;
  padding-left: 1em;
}

.suite .suite {
  margin: 5px;
}

.suite.passed {
  background-color: #dfd;
}

.suite.failed {
  background-color: #fdd;
}

.spec {
  margin: 5px;
  padding-left: 1em;
  clear: both;
}

.spec.failed, .spec.passed, .spec.skipped {
  padding-bottom: 5px;
  border: 1px solid gray;
}

.spec.failed {
  background-color: #fbb;
  border-color: red;
}

.spec.passed {
  background-color: #bfb;
  border-color: green;
}

.spec.skipped {
  background-color: #bbb;
}

.messages {
  border-left: 1px dashed gray;
  padding-left: 1em;
  padding-right: 1em;
}

.passed {
  background-color: #cfc;
  display: none;
}

.failed {
  background-color: #fbb;
}

.skipped {
  color: #777;
  background-color: #eee;
  display: none;
}

/*.resultMessage {*/
  /*white-space: pre;*/
/*}*/

.resultMessage span.result {
  display: block;
  line-height: 2em;
  color: black;
}

.resultMessage .mismatch {
  color: black;
}

.stackTrace {
  white-space: pre;
  font-size: .8em;
  margin-left: 10px;
  max-height: 5em;
  overflow: auto;
  border: 1px inset red;
  padding: 1em;
  background: #eef;
}

.finished-at {
  padding-left: 1em;
  font-size: .6em;
}

.show-passed .passed,
.show-skipped .skipped {
  display: block;
}

#jasmine_content {
  position:fixed;
  right: 100%;
}

.runner {
  border: 1px solid gray;
  display: block;
  margin: 5px 0;
  padding: 2px 0 2px 10px;
}


L.Control.Attribution = L.Class.extend({
	onAdd: function(map) {
		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
		this._map = map;
		this._prefix = 'Powered by Leaflet';
		this._attributions = {};
		this._update();
	},

	getPosition: function() {
		return L.Control.Position.BOTTOM_RIGHT;
	},

	getContainer: function() {
		return this._container;
	},

	setPrefix: function(prefix) {
		this._prefix = prefix;
	},

	addAttribution: function(text) {
		if (!text) return;
		this._attributions[text] = true;
		this._update();
	},

	removeAttribution: function(text) {
		if (!text) return;
		delete this._attributions[text];
		this._update();
	},

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

		var attribs = [];

		for (var i in this._attributions) {
			if (this._attributions.hasOwnProperty(i)) {
				attribs.push(i);
			}
		}

		this._container.innerHTML = [this._prefix, attribs.join(', ')].join(' — ');
	}
});


// tells tile layers to reinitialize their containers


describe('Bounds', function() {
	var a, b;

	beforeEach(function() {
		a = new L.Bounds(
			new L.Point(14, 12),
			new L.Point(30, 40));
		b = new L.Bounds([
   			new L.Point(20, 12),
   			new L.Point(14, 20),
   			new L.Point(30, 40)
   		]);
	});

	describe('constructor', function() {
		it('should create bounds with proper min & max on (Point, Point)', function() {
			expect(a.min).toEqual(new L.Point(14, 12));
			expect(a.max).toEqual(new L.Point(30, 40));
		});
		it('should create bounds with proper min & max on (Point[])', function() {
			expect(b.min).toEqual(new L.Point(14, 12));
			expect(b.max).toEqual(new L.Point(30, 40));
		});
	});

	describe('#extend', function() {
		it('should extend the bounds to contain the given point', function() {
			a.extend(new L.Point(50, 20));
			expect(a.min).toEqual(new L.Point(14, 12));
			expect(a.max).toEqual(new L.Point(50, 40));

			b.extend(new L.Point(25, 50));
			expect(b.min).toEqual(new L.Point(14, 12));
			expect(b.max).toEqual(new L.Point(30, 50));
		});
	});

	describe('#getCenter', function() {
		it('should return the center point', function() {
			expect(a.getCenter()).toEqual(new L.Point(22, 26));
		});
	});
});


/*
 * L.Util is a namespace for various utility functions.
 */

L.Util = {
	extend: function(/*Object*/ dest) /*-> Object*/ {	// merge src properties into dest
		var sources = Array.prototype.slice.call(arguments, 1);
		for (var j = 0, len = sources.length, src; j < len; j++) {
			src = sources[j] || {};
			for (var i in src) {
				if (src.hasOwnProperty(i)) {
					dest[i] = src[i];
				}
			}
		}
		return dest;
	},

	bind: function(/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
		return function() {
			return fn.apply(obj, arguments);
		};
	},

	stamp: (function() {
		var lastId = 0, key = '_leaflet_id';
		return function(/*Object*/ obj) {
			obj[key] = obj[key] || ++lastId;
			return obj[key];
		};
	})(),

	limitExecByInterval: function(fn, time, context) {
		var lock, execOnUnlock, args;
		function exec(){
			lock = false;
			if (execOnUnlock) {
				args.callee.apply(context, args);
				execOnUnlock = false;
			}
		}
		return function() {
			args = arguments;
			if (!lock) {
				lock = true;
				setTimeout(exec, time);
				fn.apply(context, args);
			} else {
				execOnUnlock = true;
			}
		};
	},

	deferExecByInterval: function(fn, time, context) {
		var args, lock;
		function exec() {
			lock = false;
			fn.apply(context, args);
		}
		return function() {
			args = arguments;
			if (!lock) {
				lock = true;
				setTimeout(exec, time);
			}
		};
	},

	falseFn: function() { return false; },

	formatNum: function(num, digits) {
		var pow = Math.pow(10, digits || 5);
		return Math.round(num * pow) / pow;
	},

	setOptions: function(obj, options) {
		obj.options = L.Util.extend({}, obj.options, options);
	},

	getParamString: function(obj) {
		var params = [];
		for (var i in obj) {
			if (obj.hasOwnProperty(i)) {
				params.push(i + '=' + obj[i]);
			}
		}
		return '?' + params.join('&');
	}
};


this._animatingZoom = false;
},
_restoreTileFront: function() {
this._tilePane.style.display = '';
this._tilePane.style.zIndex = 2;
this._tileBg.style.zIndex = 1;
},

keep keep keep keep replace keep keep keep


L.Control = {};

L.Control.Position = {
	TOP_LEFT: 'topLeft',
	TOP_RIGHT: 'topRight',
	BOTTOM_LEFT: 'bottomLeft',
	BOTTOM_RIGHT: 'bottomRight'
};


/*
 * L.Path is a base class for rendering vector paths on a map. It's inherited by Polyline, Circle, etc.
 */

L.Path = L.Class.extend({
	includes: [L.Mixin.Events],

	statics: (function() {
		var svgns = 'http://www.w3.org/2000/svg',
			ce = 'createElementNS';

		return {
			SVG_NS: svgns,
			SVG: !!(document[ce] && document[ce](svgns, 'svg').createSVGRect),

			// how much to extend the clip area around the map view
			// (relative to its size, e.g. 0.5 is half the screen in each direction)
			CLIP_PADDING: 0.5
		};
	})(),

	options: {
		stroke: true,
		color: '#0033ff',
		weight: 5,
		opacity: 0.5,

		fill: false,
		fillColor: null, //same as color by default
		fillOpacity: 0.2,

		clickable: true,

		updateOnMoveEnd: false
	},

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

	onAdd: function(map) {
		this._map = map;

		this._initElements();
		this._initEvents();
		this.projectLatlngs();
		this._updatePath();

		map.on('viewreset', this.projectLatlngs, this);

		this._updateTrigger = this.options.updateOnMoveEnd ? 'moveend' : 'viewreset';
		map.on(this._updateTrigger, this._updatePath, this);
	},

	onRemove: function(map) {
		map._pathRoot.removeChild(this._container);
		map.off('viewreset', this._projectLatlngs, this);
		map.off(this._updateTrigger, this._updatePath, this);
	},

	projectLatlngs: function() {
		// do all projection stuff here
	},

	getPathString: function() {
		// form path string here
	},

	_initElements: function() {
		this._initRoot();
		this._initPath();
		this._initStyle();
	},

	_initRoot: function() {
		if (!this._map._pathRoot) {
			this._map._pathRoot = this._createElement('svg');
			this._map._panes.overlayPane.appendChild(this._map._pathRoot);

			this._map.on('moveend', this._updateSvgViewport, this);
			this._updateSvgViewport();
		}
	},

	_updateSvgViewport: function() {
		this._updateViewport();

		var vp = this._map._pathViewport,
			min = vp.min,
			max = vp.max,
			width = max.x - min.x,
			height = max.y - min.y,
			root = this._map._pathRoot,
			pane = this._map._panes.overlayPane;

		// Hack to make flicker on drag end on mobile webkit less irritating
		// Unfortunately I haven't found a good workaround for this yet
		if (L.Browser.mobileWebkit) { pane.removeChild(root); }

		L.DomUtil.setPosition(root, min);
		root.setAttribute('width', width);
		root.setAttribute('height', height);
		root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));

		if (L.Browser.mobileWebkit) { pane.appendChild(root); }
	},

	_updateViewport: function() {
		var p = L.Path.CLIP_PADDING,
			size = this._map.getSize(),
			//TODO this._map._getMapPanePos()
			panePos = L.DomUtil.getPosition(this._map._mapPane),
			min = panePos.multiplyBy(-1).subtract(size.multiplyBy(p)),
			max = min.add(size.multiplyBy(1 + p * 2));

		this._map._pathViewport = new L.Bounds(min, max);
	},

	_initPath: function() {
		this._container = this._createElement('g');

		this._path = this._createElement('path');
		this._container.appendChild(this._path);

		this._map._pathRoot.appendChild(this._container);
	},

	_initStyle: function() {
		if (this.options.stroke) {
			this._path.setAttribute('stroke-linejoin', 'round');
			this._path.setAttribute('stroke-linecap', 'round');
		}
		if (this.options.fill) {
			this._path.setAttribute('fill-rule', 'evenodd');
		} else {
			this._path.setAttribute('fill', 'none');
		}
		this._updateStyle();
	},

	_updateStyle: function() {
		if (this.options.stroke) {
			this._path.setAttribute('stroke', this.options.color);
			this._path.setAttribute('stroke-opacity', this.options.opacity);
			this._path.setAttribute('stroke-width', this.options.weight);
		}
		if (this.options.fill) {
			this._path.setAttribute('fill', this.options.fillColor || this.options.color);
			this._path.setAttribute('fill-opacity', this.options.fillOpacity);
		}
	},

	_updatePath: function() {
		var str = this.getPathString();
		if (!str) {
			// fix webkit empty string parsing bug
			str = 'M0 0';
		}
		this._path.setAttribute('d', str);
	},

	_createElement: function(name) {
		return document.createElementNS(L.Path.SVG_NS, name);
	},

	// TODO remove duplication with L.Map
	_initEvents: function() {
		if (this.options.clickable) {
			if (!L.Path.VML) {
				this._path.setAttribute('class', 'leaflet-clickable');
			}

			L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);

			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
			for (var i = 0; i < events.length; i++) {
				L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
			}
		}
	},

	_onMouseClick: function(e) {
		if (this._map.dragging && this._map.dragging.moved()) { return; }
		this._fireMouseEvent(e);
	},

	_fireMouseEvent: function(e) {
		if (!this.hasEventListeners(e.type)) { return; }
		this.fire(e.type, {
			latlng: this._map.mouseEventToLatLng(e),
			layerPoint: this._map.mouseEventToLayerPoint(e)
		});
		L.DomEvent.stopPropagation(e);
	}
});


this._tileBg.style.zIndex = 1;
},
_clearTileBg: function() {
if (!this._animatingZoom) {
this._tileBg.innerHTML = '';
}
}

if (!this._animatingZoom && !this.touchZoom._zooming) {


/*
 * L.PolyUtil contains utilify functions for polygons (clipping, etc.).
 */

L.PolyUtil = {};

/*
 * Sutherland-Hodgeman polygon clipping algorithm.
 * Used to avoid rendering parts of a polygon that are not currently visible.
 */
L.PolyUtil.clipPolygon = function(points, bounds) {
	var min = bounds.min,
		max = bounds.max,
		clippedPoints,
		edges = [1, 4, 2, 8],
		i, j, k,
		a, b,
		len, edge, p,
		lu = L.LineUtil;

	for (i = 0, len = points.length; i < len; i++) {
		points[i]._code = lu._getBitCode(points[i], bounds);
	}

	// for each edge (left, bottom, right, top)
	for (k = 0; k < 4; k++) {
		edge = edges[k];
		clippedPoints = [];

		for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
			a = points[i];
			b = points[j];

			// if a is inside the clip window
			if (!(a._code & edge)) {
				// if b is outside the clip window (a->b goes out of screen)
				if (b._code & edge) {
					p = lu._getEdgeIntersection(b, a, edge, bounds);
					p._code = lu._getBitCode(p, bounds);
					clippedPoints.push(p);
				}
				clippedPoints.push(a);

			// else if b is inside the clip window (a->b enters the screen)
			} else if (!(b._code & edge)) {
				p = lu._getEdgeIntersection(b, a, edge, bounds);
				p._code = lu._getBitCode(p, bounds);
				clippedPoints.push(p);
			}
		}
		points = clippedPoints;
	}

	return points;
};


#map {
	width: 800px;
	height: 600px;
	border: 1px solid #ccc;
	}


/*
 * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
 */

L.Handler.TouchZoom = L.Handler.extend({
	enable: function() {
		if (!L.Browser.mobileWebkit || this._enabled) { return; }
		L.DomEvent.addListener(this._map._container, 'touchstart', this._onTouchStart, this);
		this._enabled = true;
	},

	disable: function() {
		if (!this._enabled) { return; }
		L.DomEvent.removeListener(this._map._container, 'touchstart', this._onTouchStart, this);
		this._enabled = false;
	},

	_onTouchStart: function(e) {
		if (!e.touches || e.touches.length != 2 || this._map._animatingZoom) { return; }

		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
			p2 = this._map.mouseEventToLayerPoint(e.touches[1]),
			viewCenter = this._map.containerPointToLayerPoint(this._map.getSize().divideBy(2));

		this._startCenter = p1.add(p2).divideBy(2, true);
		this._startDist = p1.distanceTo(p2);
		//this._startTransform = this._map._mapPane.style.webkitTransform;

		this._moved = false;
		this._zooming = true;

		this._centerOffset = viewCenter.subtract(this._startCenter);

		L.DomEvent.addListener(document, 'touchmove', this._onTouchMove, this);
		L.DomEvent.addListener(document, 'touchend', this._onTouchEnd, this);

		L.DomEvent.preventDefault(e);
	},

	_onTouchMove: function(e) {
		if (!e.touches || e.touches.length != 2) { return; }

		if (!this._moved) {
			this._map._mapPane.className += ' leaflet-zoom-anim';
			this._map._prepareTileBg();
			this._moved = true;
		}

		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
			p2 = this._map.mouseEventToLayerPoint(e.touches[1]);

		this._scale = p1.distanceTo(p2) / this._startDist;
		this._delta = p1.add(p2).divideBy(2, true).subtract(this._startCenter);

		/*
		 * Used 2 translates instead of transform-origin because of a very strange bug -
		 * it didn't count the origin on the first touch-zoom but worked correctly afterwards
		 */
		this._map._tileBg.style.webkitTransform = [
            L.DomUtil.getTranslateString(this._delta),
            L.DomUtil.getScaleString(this._scale, this._startCenter)
        ].join(" ");

		L.DomEvent.preventDefault(e);
	},

	_onTouchEnd: function(e) {
		if (!this._moved || !this._zooming) { return; }
		this._zooming = false;

		var oldZoom = this._map.getZoom(),
			floatZoomDelta = Math.log(this._scale)/Math.LN2,
			roundZoomDelta = (floatZoomDelta > 0 ? Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
			zoom = this._map._limitZoom(oldZoom + roundZoomDelta),
			zoomDelta = zoom - oldZoom,
			centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
			center = this._map.unproject(centerPoint);

		L.DomEvent.removeListener(document, 'touchmove', this._onTouchMove);
		L.DomEvent.removeListener(document, 'touchend', this._onTouchEnd);

		var finalScale = Math.pow(2, zoomDelta);

		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset));
	}
});


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

/*
 * L.Map is the central class of the API - it is used to create a map.
 */

L.Map = L.Class.extend({
	includes: L.Mixin.Events,

	options: {
		// projection
		crs: L.CRS.EPSG3857 || L.CRS.EPSG4326,
		scale: function(zoom) { return 256 * (1 << zoom); },

		// state
		center: null,
		zoom: null,
		layers: [],

		// interaction
		dragging: true,
		touchZoom: L.Browser.mobileWebkit && !L.Browser.android,
		scrollWheelZoom: !L.Browser.mobileWebkit,
		doubleClickZoom: true,
		shiftDragZoom: true,

		// controls
		zoomControl: true,
		attributionControl: true,

		// animation
		fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android,
		zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android,

		// misc
		trackResize: true,
		closePopupOnClick: true
	},

	// constructor

	initialize: function(/*HTMLElement or String*/ id, /*Object*/ options) {
		L.Util.setOptions(this, options);

		this._container = L.DomUtil.get(id);

		this._initLayout();

		if (L.DomEvent) {
			this._initEvents();
			if (L.Handler) { this._initInteraction(); }
			if (L.Control) { this._initControls(); }
		}

		var center = this.options.center,
			zoom = this.options.zoom;

		if (center !== null && zoom !== null) {
			this.setView(center, zoom, true);
		}

		var layers = this.options.layers;
		layers = (layers instanceof Array ? layers : [layers]);
		this._tileLayersNum = 0;
		this._initLayers(layers);
	},

	// public methods that modify map state

	// replaced by animation-powered implementation in Map.PanAnimation.js
	setView: function(center, zoom, forceReset) {
		// reset the map view
		this._resetView(center, this._limitZoom(zoom));
		return this;
	},

	setZoom: function(/*Number*/ zoom) {
		return this.setView(this.getCenter(), zoom);
	},

	zoomIn: function() {
		return this.setZoom(this._zoom + 1);
	},

	zoomOut: function() {
		return this.setZoom(this._zoom - 1);
	},

	fitBounds: function(/*LatLngBounds*/ bounds) {
		var zoom = this.getBoundsZoom(bounds);
		return this.setView(bounds.getCenter(), zoom);
	},

	fitWorld: function() {
		var sw = new L.LatLng(-60, -170),
			ne = new L.LatLng(85, 179);
		return this.fitBounds(new L.LatLngBounds(sw, ne));
	},

	panTo: function(/*LatLng*/ center) {
		return this.setView(center, this._zoom);
	},

	panBy: function(/*Point*/ offset) {
		// replaced with animated panBy in Map.Animation.js
		this.fire('movestart');

		this._rawPanBy(offset);

		this.fire('move');
		this.fire('moveend');

		return this;
	},

	addLayer: function(layer) {
		var id = L.Util.stamp(layer);

		if (this._layers[id]) return this;

		this._layers[id] = layer;

		if (layer.options && !isNaN(layer.options.maxZoom)) {
			this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);
		}
		if (layer.options && !isNaN(layer.options.minZoom)) {
			this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);
		}
		//TODO getMaxZoom, getMinZoom in ILayer (instead of options)

		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
			this._tileLayersNum++;
			layer.on('load', this._onTileLayerLoad, this);
		}
		if (this.attributionControl && layer.getAttribution) {
			this.attributionControl.addAttribution(layer.getAttribution());
		}

		var onMapLoad = function() {
			layer.onAdd(this);
			this.fire('layeradd', {layer: layer});
		};

		if (this._loaded) {
			onMapLoad.call(this);
		} else {
			this.on('load', onMapLoad, this);
		}

		return this;
	},

	removeLayer: function(layer) {
		var id = L.Util.stamp(layer);

		if (this._layers[id]) {
			layer.onRemove(this);
			delete this._layers[id];

			if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
				this._tileLayersNum--;
			}
			if (this.attributionControl && layer.getAttribution) {
				this.attributionControl.removeAttribution(layer.getAttribution());
			}

			this.fire('layerremove', {layer: layer});
		}
		return this;
	},

	invalidateSize: function() {
		this._sizeChanged = true;

		this.fire('move');

		clearTimeout(this._sizeTimer);
		this._sizeTimer = setTimeout(L.Util.bind(function() {
			this.fire('moveend');
		}, this), 200);

		return this;
	},

	// public methods for getting map state

	getCenter: function(/*Boolean*/ unbounded) {
		var viewHalf = this.getSize().divideBy(2),
			centerPoint = this._getTopLeftPoint().add(viewHalf);
		return this.unproject(centerPoint, this._zoom, unbounded);
	},

	getZoom: function() {
		return this._zoom;
	},

	getBounds: function() {
		var bounds = this.getPixelBounds(),
			sw = this.unproject(new L.Point(bounds.min.x, bounds.max.y)),
			ne = this.unproject(new L.Point(bounds.max.x, bounds.min.y));
		return new L.LatLngBounds(sw, ne);
	},

	getMinZoom: function() {
		return isNaN(this.options.minZoom) ?  this._layersMinZoom || 0 : this.options.minZoom;
	},

	getMaxZoom: function() {
		return isNaN(this.options.maxZoom) ?  this._layersMaxZoom || Infinity : this.options.maxZoom;
	},

	getBoundsZoom: function(/*LatLngBounds*/ bounds) {
		var size = this.getSize(),
			zoom = this.getMinZoom(),
			maxZoom = this.getMaxZoom(),
			ne = bounds.getNorthEast(),
			sw = bounds.getSouthWest(),
			boundsSize,
			nePoint, swPoint;
		do {
			zoom++;
			nePoint = this.project(ne, zoom);
			swPoint = this.project(sw, zoom);
			boundsSize = new L.Point(nePoint.x - swPoint.x, swPoint.y - nePoint.y);
		} while ((boundsSize.x <= size.x) &&
				 (boundsSize.y <= size.y) && (zoom <= maxZoom));

		return zoom - 1;
	},

	getSize: function() {
		if (!this._size || this._sizeChanged) {
			this._size = new L.Point(this._container.clientWidth, this._container.clientHeight);
			this._sizeChanged = false;
		}
		return this._size;
	},

	getPixelBounds: function() {
		var topLeftPoint = this._getTopLeftPoint(),
			size = this.getSize();
		return new L.Bounds(topLeftPoint, topLeftPoint.add(size));
	},

	getPixelOrigin: function() {
		return this._initialTopLeftPoint;
	},

	getPanes: function() {
		return this._panes;
	},

	// conversion methods

	mouseEventToContainerPoint: function(/*MouseEvent*/ e) {
		return L.DomEvent.getMousePosition(e, this._container);
	},

	mouseEventToLayerPoint: function(/*MouseEvent*/ e) {
		return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
	},

	mouseEventToLatLng: function(/*MouseEvent*/ e) {
		return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
	},

	containerPointToLayerPoint: function(/*Point*/ point) {
		return point.subtract(L.DomUtil.getPosition(this._mapPane));
	},

	layerPointToContainerPoint: function(/*Point*/ point) {
		return point.add(L.DomUtil.getPosition(this._mapPane));
	},

	layerPointToLatLng: function(/*Point*/ point) {
		return this.unproject(point.add(this._initialTopLeftPoint));
	},

	latLngToLayerPoint: function(/*LatLng*/ latlng) {
		return this.project(latlng)._subtract(this._initialTopLeftPoint);
	},

	project: function(/*LatLng*/ latlng, /*(optional) Number*/ zoom)/*-> Point*/ {
		zoom = (typeof zoom == 'undefined' ? this._zoom : zoom);
		return this.options.crs.latLngToPoint(latlng, this.options.scale(zoom));
	},

	unproject: function(/*Point*/ point, /*(optional) Number*/ zoom, /*(optional) Boolean*/ unbounded)/*-> Object*/ {
		zoom = (typeof zoom == 'undefined' ? this._zoom : zoom);
		return this.options.crs.pointToLatLng(point, this.options.scale(zoom), unbounded);
	},

	// private methods that modify map state

	_initLayout: function() {
		var container = this._container;

		container.className += ' leaflet-container';

		if (this.options.fadeAnimation) {
			container.className += ' leaflet-fade-anim';
		}

		var position = L.DomUtil.getStyle(container, 'position');
		container.style.position = (position == 'absolute' ? 'absolute' : 'relative');

		this._initPanes();

		if (this._initControlPos) this._initControlPos();
	},

	_initPanes: function() {
		var panes = this._panes = {};

		this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);

		this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);
		this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);

		panes.shadowPane = this._createPane('leaflet-shadow-pane');
		panes.overlayPane = this._createPane('leaflet-overlay-pane');
		panes.markerPane = this._createPane('leaflet-marker-pane');
		panes.popupPane = this._createPane('leaflet-popup-pane');
	},

	_createPane: function(className, container) {
		return L.DomUtil.create('div', className, container || this._objectsPane);
	},

	_resetView: function(center, zoom, preserveMapOffset) {
		var zoomChanged = (this._zoom != zoom);

		this.fire('movestart');

		this._zoom = zoom;

		this._initialTopLeftPoint = this._getNewTopLeftPoint(center);

		if (!preserveMapOffset) {
			L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
		} else {
			var offset = L.DomUtil.getPosition(this._mapPane);
			this._initialTopLeftPoint._add(offset);
		}

		this._tileLayersToLoad = this._tileLayersNum;
		this.fire('viewreset');

		this.fire('move');
		if (zoomChanged) { this.fire('zoomend'); }
		this.fire('moveend');

		if (!this._loaded) {
			this._loaded = true;
			this.fire('load');
		}
	},

	_initLayers: function(layers) {
		this._layers = {};
		for (var i = 0, len = layers.length; i < len; i++) {
			this.addLayer(layers[i]);
		}
	},

	_initControls: function() {
		if (this.options.zoomControl) {
			this.addControl(new L.Control.Zoom());
		}
		if (this.options.attributionControl) {
			this.attributionControl = new L.Control.Attribution();
			this.addControl(this.attributionControl);
		}
	},

	_rawPanBy: function(offset) {
		var mapPaneOffset = L.DomUtil.getPosition(this._mapPane);
		L.DomUtil.setPosition(this._mapPane, mapPaneOffset.subtract(offset));
	},

	// map events

	_initEvents: function() {
		L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);

		var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove'];
		for (var i = 0; i < events.length; i++) {
			L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
		}

		if (this.options.trackResize) {
			L.DomEvent.addListener(window, 'resize', this.invalidateSize, this);
		}
	},

	_onMouseClick: function(e) {
		if (this.dragging && this.dragging.moved()) { return; }

		this.fire('pre' + e.type);
		this._fireMouseEvent(e);
	},

	_fireMouseEvent: function(e) {
		var type = e.type;
		type = (type == 'mouseenter' ? 'mouseover' : (type == 'mouseleave' ? 'mouseout' : type));
		if (!this.hasEventListeners(type)) { return; }
		this.fire(type, {
			latlng: this.mouseEventToLatLng(e),
			layerPoint: this.mouseEventToLayerPoint(e)
		});
	},

	_initInteraction: function() {
		var handlers = {
			dragging: L.Handler.MapDrag,
			touchZoom: L.Handler.TouchZoom,
			doubleClickZoom: L.Handler.DoubleClickZoom,
			scrollWheelZoom: L.Handler.ScrollWheelZoom,
			shiftDragZoom: L.Handler.ShiftDragZoom
		};
		for (var i in handlers) {
			if (handlers.hasOwnProperty(i) && handlers[i]) {
				this[i] = new handlers[i](this);
				if (this.options[i]) this[i].enable();
			}
		}
	},

	_onTileLayerLoad: function() {
		// clear scaled tiles after all new tiles are loaded (for performance)
		this._tileLayersToLoad--;
		if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {
			clearTimeout(this._clearTileBgTimer);
			this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500);
		}
	},

	// private methods for getting map state

	_getTopLeftPoint: function() {
		if (!this._loaded) throw new Error('Set map center and zoom first.');
		var offset = L.DomUtil.getPosition(this._mapPane);
		return this._initialTopLeftPoint.subtract(offset);
	},

	_getNewTopLeftPoint: function(center) {
		var viewHalf = this.getSize().divideBy(2);
		return this.project(center).subtract(viewHalf).round();
	},

	_limitZoom: function(zoom) {
		var min = this.getMinZoom();
		var max = this.getMaxZoom();
		return Math.max(min, Math.min(max, zoom));
	}
});


L.Polyline = L.Path.extend({
	initialize: function(latlngs, options) {
		L.Path.prototype.initialize.call(this, options);
		this._latlngs = latlngs;
	},

	options: {
		// how much to simplify the polyline on each zoom level
		// more = better performance and smoother look, less = more accurate
		smoothFactor: 1.0,
		noClip: false,

		updateOnMoveEnd: true
	},

	projectLatlngs: function() {
		this._originalPoints = [];

		for (var i = 0, len = this._latlngs.length; i < len; i++) {
			this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]);
		}
	},

	getPathString: function() {
		for (var i = 0, len = this._parts.length, str = ''; i < len; i++) {
			str += this._getPathPartStr(this._parts[i]);
		}
		return str;
	},

	getLatLngs: function() {
		return this._latlngs;
	},

	setLatLngs: function(latlngs) {
		this._latlngs = latlngs;
		this._redraw();
		return this;
	},

	addLatLng: function(latlng) {
		this._latlngs.push(latlng);
		this._redraw();
		return this;
	},

	spliceLatLngs: function(index, howMany) {
		var removed = [].splice.apply(this._latlngs, arguments);
		this._redraw();
		return removed;
	},

	_redraw: function() {
		this.projectLatlngs();
		this._updatePath();
	},

	_getPathPartStr: function(points) {
		var round = L.Path.VML;

		for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) {
			p = points[j];
			if (round) p._round();
			str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
		}
		return str;
	},

	_clipPoints: function() {
		var points = this._originalPoints,
			len = points.length,
			i, k, segment;

		if (this.options.noClip) {
			this._parts = [points];
			return;
		}

		this._parts = [];

		var parts = this._parts,
			vp = this._map._pathViewport,
			lu = L.LineUtil;

		for (i = 0, k = 0; i < len - 1; i++) {
			segment = lu.clipSegment(points[i], points[i+1], vp, i);
			if (!segment) continue;

			parts[k] = parts[k] || [];
			parts[k].push(segment[0]);

			// if segment goes out of screen, or it's the last one, it's the end of the line part
			if ((segment[1] != points[i+1]) || (i == len - 2)) {
				parts[k].push(segment[1]);
				k++;
			}
		}
	},

	// simplify each clipped part of the polyline
	_simplifyPoints: function() {
		var parts = this._parts,
			lu = L.LineUtil;

		for (var i = 0, len = parts.length; i < len; i++) {
			parts[i] = lu.simplify(parts[i], this.options.smoothFactor);
		}
	},

	_updatePath: function() {
		this._clipPoints();
		this._simplifyPoints();

		L.Path.prototype._updatePath.call(this);
	}
});


../dist/leaflet.js: Makefile
	java -jar ../lib/closure-compiler/compiler.jar \
	--js ../src/Leaflet.js \
	--js ../src/core/Util.js \
	--js ../src/core/Class.js \
	--js ../src/core/Events.js \
	--js ../src/core/Browser.js \
	--js ../src/geometry/Point.js \
	--js ../src/geometry/Bounds.js \
	--js ../src/geometry/Transformation.js \
	--js ../src/geometry/LineUtil.js \
	--js ../src/geometry/PolyUtil.js \
	--js ../src/dom/DomEvent.js \
	--js ../src/dom/DomEvent.DoubleTap.js \
	--js ../src/dom/DomUtil.js \
	--js ../src/dom/Draggable.js \
	--js ../src/dom/transition/Transition.js \
	--js ../src/dom/transition/Transition.Native.js \
	--js ../src/dom/transition/Transition.Timer.js \
	--js ../src/geo/LatLng.js \
	--js ../src/geo/LatLngBounds.js \
	--js ../src/geo/projection/Projection.js \
	--js ../src/geo/projection/Projection.SphericalMercator.js \
	--js ../src/geo/projection/Projection.LonLat.js \
	--js ../src/geo/projection/Projection.Mercator.js \
	--js ../src/geo/crs/CRS.js \
	--js ../src/geo/crs/CRS.EPSG3857.js \
	--js ../src/geo/crs/CRS.EPSG4326.js \
	--js ../src/geo/crs/CRS.EPSG3395.js \
	--js ../src/layer/tile/TileLayer.js \
	--js ../src/layer/tile/TileLayer.WMS.js \
	--js ../src/layer/tile/TileLayer.Canvas.js \
	--js ../src/layer/ImageOverlay.js \
	--js ../src/layer/Popup.js \
	--js ../src/layer/marker/Icon.js \
	--js ../src/layer/marker/Marker.js \
	--js ../src/layer/marker/Marker.Popup.js \
	--js ../src/layer/vector/Path.js \
	--js ../src/layer/vector/Path.VML.js \
	--js ../src/layer/vector/Path.Popup.js \
	--js ../src/layer/vector/Polyline.js \
	--js ../src/layer/vector/Polygon.js \
	--js ../src/layer/vector/Circle.js \
	--js ../src/layer/vector/CircleMarker.js \
	--js ../src/handler/Handler.js \
	--js ../src/handler/MapDrag.js \
	--js ../src/handler/TouchZoom.js \
	--js ../src/handler/ScrollWheelZoom.js \
	--js ../src/handler/DoubleClickZoom.js \
	--js ../src/handler/ShiftDragZoom.js \
	--js ../src/handler/MarkerDrag.js \
	--js ../src/control/Control.js \
	--js ../src/control/Control.Zoom.js \
	--js ../src/control/Control.Attribution.js \
	--js ../src/map/Map.js \
	--js ../src/map/ext/Map.Geolocation.js \
	--js ../src/map/ext/Map.Popup.js \
	--js ../src/map/ext/Map.PanAnimation.js \
	--js ../src/map/ext/Map.ZoomAnimation.js \
	--js ../src/map/ext/Map.Control.js \
	--js_output_file ../dist/leaflet.js


/*
 * L.DomUtil contains various utility functions for working with DOM
 */

L.DomUtil = {
	get: function(id) {
		return (typeof id == 'string' ? document.getElementById(id) : id);
	},

	getStyle: function(el, style) {
		var value = el.style[style];
		if ((typeof value == 'undefined') && el.currentStyle) {
			value = el.currentStyle[style];
		}
		if (typeof value == 'undefined') {
			var css = document.defaultView.getComputedStyle(el, null);
			value = css ? css[style] : null;
		}
		return (value == 'auto' ? null : value);
	},

	getCumulativeOffset: function(el) {
		var top = 0,
			left = 0;
		do {
			top += el.offsetTop || 0;
			left += el.offsetLeft || 0;
			el = el.offsetParent;
		} while (el);
		return new L.Point(left, top);
	},

	create: function(tagName, className, container) {
		var el = document.createElement(tagName);
		el.className = className;
		if (container) {
			container.appendChild(el);
		}
		return el;
	},

	disableTextSelection: function() {
		if (document.selection && document.selection.empty) {
			document.selection.empty();
		}
		if (!this._onselectstart) {
			this._onselectstart = document.onselectstart;
			document.onselectstart = L.Util.falseFn;
		}
	},

	enableTextSelection: function() {
		document.onselectstart = this._onselectstart;
		this._onselectstart = null;
	},

	CLASS_RE: /(\\s|^)'+cls+'(\\s|$)/,

	hasClass: function(el, name) {
		return (el.className.length > 0) &&
				new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);
	},

	addClass: function(el, name) {
		if (!L.DomUtil.hasClass(el, name)) {
			el.className += (el.className ? ' ' : '') + name;
		}
	},

	setOpacity: function(el, value) {
		if (L.Browser.ie) {
			el.style.filter = 'alpha(opacity=' + Math.round(value * 100) + ')';
		} else {
			el.style.opacity = value;
		}
	},

	//TODO refactor away this ugly translate/position mess

	testProp: function(props) {
		var style = document.documentElement.style;

		for (var i = 0; i < props.length; i++) {
			if (props[i] in style) {
				return props[i];
			}
		}
		return false;
	},

	getTranslateString: function(point) {
		return L.DomUtil.TRANSLATE_OPEN +
				point.x + 'px,' + point.y + 'px' +
				L.DomUtil.TRANSLATE_CLOSE;
	},

	getScaleString: function(scale, origin) {
		 return L.DomUtil.getTranslateString(origin) +
         		' scale(' + scale + ') ' +
         		L.DomUtil.getTranslateString(origin.multiplyBy(-1));
	},

	setPosition: function(el, point) {
		el._leaflet_pos = point;
		if (L.Browser.webkit) {
			el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
		} else {
			el.style.left = point.x + 'px';
			el.style.top = point.y + 'px';
		}
	},

	getPosition: function(el) {
		return el._leaflet_pos;
	}
};

L.Util.extend(L.DomUtil, {
	TRANSITION: L.DomUtil.testProp(['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']),
	TRANSFORM: L.DomUtil.testProp(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']),

	TRANSLATE_OPEN: 'translate' + (L.Browser.webkit3d ? '3d(' : '('),
	TRANSLATE_CLOSE: L.Browser.webkit3d ? ',0)' : ')'
});


<!DOCTYPE html>

	<title>Leaflet Build Helper</title>

Leaflet Build Helper

			Select All |
			Deselect All

			Run this command in the build directory:

			<input type="text" id="command" />


/*
 * L.Transition fallback implementation that powers Leaflet animation
 * in browsers that don't support CSS3 Transitions
 */

L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
	statics: {
		getTime: Date.now || function() { return +new Date(); },

		TIMER: true,

		EASINGS: {
			'ease': [0.25, 0.1, 0.25, 1.0],
			'linear': [0.0, 0.0, 1.0, 1.0],
			'ease-in': [0.42, 0, 1.0, 1.0],
			'ease-out': [0, 0, 0.58, 1.0],
			'ease-in-out': [0.42, 0, 0.58, 1.0]
		},

		CUSTOM_PROPS_GETTERS: {
			position: L.DomUtil.getPosition
		},

		//used to get units from strings like "10.5px" (->px)
		UNIT_RE: /^[\d\.]+(\D*)$/
	},

	options: {
		fps: 50
	},

	initialize: function(el, options) {
		this._el = el;
		L.Util.extend(this.options, options);

		var easings = L.Transition.EASINGS[this.options.easing] || L.Transition.EASINGS['ease'];

		this._p1 = new L.Point(0, 0);
		this._p2 = new L.Point(easings[0], easings[1]);
		this._p3 = new L.Point(easings[2], easings[3]);
		this._p4 = new L.Point(1, 1);

		this._step = L.Util.bind(this._step, this);
		this._interval = Math.round(1000 / this.options.fps);
	},

	run: function(props) {
		this._props = {};

		var getters = L.Transition.CUSTOM_PROPS_GETTERS,
			re = L.Transition.UNIT_RE;

		this.fire('start');

		for (var prop in props) {
			if (props.hasOwnProperty(prop)) {
				var p = {};
				if (prop in getters) {
					p.from = getters[prop](this._el);
				} else {
					var matches = this._el.style[prop].match(re);
					p.from = parseFloat(matches[0]);
					p.unit = matches[1];
				}
				p.to = props[prop];
				this._props[prop] = p;
			}
		}

		clearInterval(this._timer);
		this._timer = setInterval(this._step, this._interval);
		this._startTime = L.Transition.getTime();
	},

	_step: function() {
		var time = L.Transition.getTime(),
			elapsed = time - this._startTime,
			duration = this.options.duration * 1000;

		if (elapsed < duration) {
			this._runFrame(this._cubicBezier(elapsed / duration));
		} else {
			this._runFrame(1);
			this._complete();
		}
	},

	_runFrame: function(percentComplete) {
		var setters = L.Transition.CUSTOM_PROPS_SETTERS,
			prop, p, value;

		for (prop in this._props) {
			if (this._props.hasOwnProperty(prop)) {
				p = this._props[prop];
				if (prop in setters) {
					value = p.to.subtract(p.from).multiplyBy(percentComplete).add(p.from);
					setters[prop](this._el, value);
				} else {
					this._el.style[prop] =
							((p.to - p.from) * percentComplete + p.from) + p.unit;
				}
			}
		}
		this.fire('step');
	},

	_complete: function() {
		clearInterval(this._timer);
		this.fire('end');
	},

	_cubicBezier: function(t) {
		var a = Math.pow(1 - t, 3),
			b = 3 * Math.pow(1 - t, 2) * t,
			c = 3 * (1 - t) * Math.pow(t, 2),
			d = Math.pow(t, 3),
			p1 = this._p1.multiplyBy(a),
			p2 = this._p2.multiplyBy(b),
			p3 = this._p3.multiplyBy(c),
			p4 = this._p4.multiplyBy(d);

		return p1.add(p2).add(p3).add(p4).y;
	}
});


L.TileLayer.Canvas = L.TileLayer.extend({
	options: {
		async: false
	},

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

	_createTileProto: function() {
		this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');

		var tileSize = this.options.tileSize;
		this._canvasProto.width = tileSize;
		this._canvasProto.height = tileSize;
	},

	_createTile: function() {
		var tile = this._canvasProto.cloneNode(false);
		tile.onselectstart = tile.onmousemove = L.Util.falseFn;
		return tile;
	},

	_loadTile: function(tile, tilePoint, zoom) {
		tile._layer = this;

		this.drawTile(tile, tilePoint, zoom);

		if (!this.options.async) {
			this.tileDrawn();
		}
	},

	drawTile: function(tile, tilePoint, zoom) {
		// override with rendering code
	},

	tileDrawn: function(tile) {
		this._tileOnLoad.call(tile);
	}
});