Update to leaflet 1.0.2
authorTom Hughes <tom@compton.nu>
Mon, 21 Nov 2016 13:17:39 +0000 (13:17 +0000)
committerTom Hughes <tom@compton.nu>
Mon, 21 Nov 2016 13:17:39 +0000 (13:17 +0000)
Vendorfile
vendor/assets/leaflet/leaflet.css
vendor/assets/leaflet/leaflet.js

index 4fbefcf..0037f46 100644 (file)
@@ -11,13 +11,13 @@ folder 'vendor/assets' do
   end
 
   folder 'leaflet' do
-    file 'leaflet.js', 'https://unpkg.com/leaflet@1.0.1/dist/leaflet-src.js'
-    file 'leaflet.css', 'https://unpkg.com/leaflet@1.0.1/dist/leaflet.css'
+    file 'leaflet.js', 'https://unpkg.com/leaflet@1.0.2/dist/leaflet-src.js'
+    file 'leaflet.css', 'https://unpkg.com/leaflet@1.0.2/dist/leaflet.css'
 
     [ 'layers.png', 'layers-2x.png',
       'marker-icon.png', 'marker-icon-2x.png',
       'marker-shadow.png' ].each do |image|
-      file "images/#{image}", "https://unpkg.com/leaflet@1.0.1/dist/images/#{image}"
+      file "images/#{image}", "https://unpkg.com/leaflet@1.0.2/dist/images/#{image}"
     end
 
     from 'git://github.com/kajic/leaflet-locationfilter.git' do
index 82bbf8d..5453cd7 100644 (file)
@@ -5,8 +5,8 @@
 .leaflet-marker-icon,
 .leaflet-marker-shadow,
 .leaflet-tile-container,
-.leaflet-map-pane svg,
-.leaflet-map-pane canvas,
+.leaflet-pane > svg,
+.leaflet-pane > canvas,
 .leaflet-zoom-box,
 .leaflet-image-layer,
 .leaflet-layer {
@@ -43,6 +43,7 @@
 /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
 .leaflet-container .leaflet-overlay-pane svg,
 .leaflet-container .leaflet-marker-pane img,
+.leaflet-container .leaflet-shadow-pane img,
 .leaflet-container .leaflet-tile-pane img,
 .leaflet-container img.leaflet-image-layer {
        max-width: none !important;
index 32024f5..77a6b92 100644 (file)
@@ -1,10 +1,10 @@
 /*
- Leaflet 1.0.1, a JS library for interactive maps. http://leafletjs.com
+ Leaflet 1.0.2, a JS library for interactive maps. http://leafletjs.com
  (c) 2010-2016 Vladimir Agafonkin, (c) 2010-2011 CloudMade
 */
 (function (window, document, undefined) {
 var L = {
-       version: "1.0.1"
+       version: "1.0.2"
 };
 
 function expose() {
@@ -572,7 +572,7 @@ L.Evented = L.Class.extend({
        // @method fire(type: String, data?: Object, propagate?: Boolean): this
        // Fires an event of the specified type. You can optionally provide an data
        // object — the first argument of the listener function will contain its
-       // properties. The event might can optionally be propagated to event parents.
+       // properties. The event can optionally be propagated to event parents.
        fire: function (type, data, propagate) {
                if (!this.listens(type, propagate)) { return this; }
 
@@ -865,7 +865,9 @@ L.Mixin = {Events: proto};
  */
 
 L.Point = function (x, y, round) {
+       // @property x: Number; The `x` coordinate of the point
        this.x = (round ? Math.round(x) : x);
+       // @property y: Number; The `y` coordinate of the point
        this.y = (round ? Math.round(y) : y);
 };
 
@@ -1234,7 +1236,7 @@ L.Transformation = function (a, b, c, d) {
 L.Transformation.prototype = {
        // @method transform(point: Point, scale?: Number): Point
        // Returns a transformed point, optionally multiplied by the given scale.
-       // Only accepts real `L.Point` instances, not arrays.
+       // Only accepts actual `L.Point` instances, not arrays.
        transform: function (point, scale) { // (Point, Number) -> Point
                return this._transform(point.clone(), scale);
        },
@@ -1249,7 +1251,7 @@ L.Transformation.prototype = {
 
        // @method untransform(point: Point, scale?: Number): Point
        // Returns the reverse transformation of the given point, optionally divided
-       // by the given scale. Only accepts real `L.Point` instances, not arrays.
+       // by the given scale. Only accepts actual `L.Point` instances, not arrays.
        untransform: function (point, scale) {
                scale = scale || 1;
                return new L.Point(
@@ -1724,9 +1726,9 @@ L.latLng = function (a, b, c) {
  * @example
  *
  * ```js
- * var southWest = L.latLng(40.712, -74.227),
- * northEast = L.latLng(40.774, -74.125),
- * bounds = L.latLngBounds(southWest, northEast);
+ * var corner1 = L.latLng(40.712, -74.227),
+ * corner2 = L.latLng(40.774, -74.125),
+ * bounds = L.latLngBounds(corner1, corner2);
  * ```
  *
  * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
@@ -1737,12 +1739,14 @@ L.latLng = function (a, b, c) {
  *     [40.774, -74.125]
  * ]);
  * ```
+ *
+ * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.
  */
 
-L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[])
-       if (!southWest) { return; }
+L.LatLngBounds = function (corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
+       if (!corner1) { return; }
 
-       var latlngs = northEast ? [southWest, northEast] : southWest;
+       var latlngs = corner2 ? [corner1, corner2] : corner1;
 
        for (var i = 0, len = latlngs.length; i < len; i++) {
                this.extend(latlngs[i]);
@@ -1944,8 +1948,8 @@ L.LatLngBounds.prototype = {
 
 // TODO International date line?
 
-// @factory L.latLngBounds(southWest: LatLng, northEast: LatLng)
-// Creates a `LatLngBounds` object by defining south-west and north-east corners of the rectangle.
+// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
+// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
 
 // @alternative
 // @factory L.latLngBounds(latlngs: LatLng[])
@@ -2235,6 +2239,12 @@ L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, {
  * @crs L.CRS.EPSG4326
  *
  * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection.
+ *
+ * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic),
+ * which is a breaking change from 0.7.x behaviour.  If you are using a `TileLayer`
+ * with this CRS, ensure that there are two 256x256 pixel tiles covering the
+ * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90),
+ * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set.
  */
 
 L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, {
@@ -2307,6 +2317,15 @@ L.Map = L.Evented.extend({
 
 
                // @section Animation Options
+               // @option zoomAnimation: Boolean = true
+               // Whether the map zoom animation is enabled. By default it's enabled
+               // in all browsers that support CSS3 Transitions except Android.
+               zoomAnimation: true,
+
+               // @option zoomAnimationThreshold: Number = 4
+               // Won't animate zoom if the zoom difference exceeds this value.
+               zoomAnimationThreshold: 4,
+
                // @option fadeAnimation: Boolean = true
                // Whether the tile fade animation is enabled. By default it's enabled
                // in all browsers that support CSS3 Transitions except Android.
@@ -2375,6 +2394,17 @@ L.Map = L.Evented.extend({
 
                this.callInitHooks();
 
+               // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
+               this._zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera &&
+                               this.options.zoomAnimation;
+
+               // zoom transitions run with the same duration for all layers, so if one of transitionend events
+               // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
+               if (this._zoomAnimated) {
+                       this._createAnimProxy();
+                       L.DomEvent.on(this._proxy, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
+               }
+
                this._addLayers(this.options.layers);
        },
 
@@ -2384,10 +2414,36 @@ L.Map = L.Evented.extend({
        // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
        // Sets the view of the map (geographical center and zoom) with the given
        // animation options.
-       setView: function (center, zoom) {
-               // replaced by animation-powered implementation in Map.PanAnimation.js
-               zoom = zoom === undefined ? this.getZoom() : zoom;
-               this._resetView(L.latLng(center), zoom);
+       setView: function (center, zoom, options) {
+
+               zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
+               center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds);
+               options = options || {};
+
+               this._stop();
+
+               if (this._loaded && !options.reset && options !== true) {
+
+                       if (options.animate !== undefined) {
+                               options.zoom = L.extend({animate: options.animate}, options.zoom);
+                               options.pan = L.extend({animate: options.animate, duration: options.duration}, options.pan);
+                       }
+
+                       // try animating pan or zoom
+                       var moved = (this._zoom !== zoom) ?
+                               this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
+                               this._tryAnimatedPan(center, options.pan);
+
+                       if (moved) {
+                               // prevent resize handler call, the view will refresh after animation anyway
+                               clearTimeout(this._sizeTimer);
+                               return this;
+                       }
+               }
+
+               // animation didn't start, just reset the map view
+               this._resetView(center, zoom);
+
                return this;
        },
 
@@ -2486,14 +2542,135 @@ L.Map = L.Evented.extend({
 
        // @method panBy(offset: Point): this
        // Pans the map by a given number of pixels (animated).
-       panBy: function (offset) { // (Point)
-               // replaced with animated panBy in Map.PanAnimation.js
-               this.fire('movestart');
+       panBy: function (offset, options) {
+               offset = L.point(offset).round();
+               options = options || {};
+
+               if (!offset.x && !offset.y) {
+                       return this.fire('moveend');
+               }
+               // If we pan too far, Chrome gets issues with tiles
+               // and makes them disappear or appear in the wrong place (slightly offset) #2602
+               if (options.animate !== true && !this.getSize().contains(offset)) {
+                       this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
+                       return this;
+               }
 
-               this._rawPanBy(L.point(offset));
+               if (!this._panAnim) {
+                       this._panAnim = new L.PosAnimation();
 
-               this.fire('move');
-               return this.fire('moveend');
+                       this._panAnim.on({
+                               'step': this._onPanTransitionStep,
+                               'end': this._onPanTransitionEnd
+                       }, this);
+               }
+
+               // don't fire movestart if animating inertia
+               if (!options.noMoveStart) {
+                       this.fire('movestart');
+               }
+
+               // animate pan unless animate: false specified
+               if (options.animate !== false) {
+                       L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
+
+                       var newPos = this._getMapPanePos().subtract(offset).round();
+                       this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
+               } else {
+                       this._rawPanBy(offset);
+                       this.fire('move').fire('moveend');
+               }
+
+               return this;
+       },
+
+       // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
+       // Sets the view of the map (geographical center and zoom) performing a smooth
+       // pan-zoom animation.
+       flyTo: function (targetCenter, targetZoom, options) {
+
+               options = options || {};
+               if (options.animate === false || !L.Browser.any3d) {
+                       return this.setView(targetCenter, targetZoom, options);
+               }
+
+               this._stop();
+
+               var from = this.project(this.getCenter()),
+                   to = this.project(targetCenter),
+                   size = this.getSize(),
+                   startZoom = this._zoom;
+
+               targetCenter = L.latLng(targetCenter);
+               targetZoom = targetZoom === undefined ? startZoom : targetZoom;
+
+               var w0 = Math.max(size.x, size.y),
+                   w1 = w0 * this.getZoomScale(startZoom, targetZoom),
+                   u1 = (to.distanceTo(from)) || 1,
+                   rho = 1.42,
+                   rho2 = rho * rho;
+
+               function r(i) {
+                       var s1 = i ? -1 : 1,
+                           s2 = i ? w1 : w0,
+                           t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
+                           b1 = 2 * s2 * rho2 * u1,
+                           b = t1 / b1,
+                           sq = Math.sqrt(b * b + 1) - b;
+
+                           // workaround for floating point precision bug when sq = 0, log = -Infinite,
+                           // thus triggering an infinite loop in flyTo
+                           var log = sq < 0.000000001 ? -18 : Math.log(sq);
+
+                       return log;
+               }
+
+               function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
+               function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
+               function tanh(n) { return sinh(n) / cosh(n); }
+
+               var r0 = r(0);
+
+               function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
+               function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }
+
+               function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }
+
+               var start = Date.now(),
+                   S = (r(1) - r0) / rho,
+                   duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
+
+               function frame() {
+                       var t = (Date.now() - start) / duration,
+                           s = easeOut(t) * S;
+
+                       if (t <= 1) {
+                               this._flyToFrame = L.Util.requestAnimFrame(frame, this);
+
+                               this._move(
+                                       this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
+                                       this.getScaleZoom(w0 / w(s), startZoom),
+                                       {flyTo: true});
+
+                       } else {
+                               this
+                                       ._move(targetCenter, targetZoom)
+                                       ._moveEnd(true);
+                       }
+               }
+
+               this._moveStart(true);
+
+               frame.call(this);
+               return this;
+       },
+
+       // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
+       // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
+       // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
+       flyToBounds: function (bounds, options) {
+               var target = this._getBoundsCenterZoom(bounds, options);
+               return this.flyTo(target.center, target.zoom, options);
        },
 
        // @method setMaxBounds(bounds: Bounds): this
@@ -2626,61 +2803,163 @@ L.Map = L.Evented.extend({
                return this._stop();
        },
 
+       // @section Geolocation methods
+       // @method locate(options?: Locate options): this
+       // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
+       // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
+       // and optionally sets the map view to the user's location with respect to
+       // detection accuracy (or to the world view if geolocation failed).
+       // Note that, if your page doesn't use HTTPS, this method will fail in
+       // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
+       // See `Locate options` for more details.
+       locate: function (options) {
 
-       // TODO handler.addTo
-       // TODO Appropiate docs section?
-       // @section Other Methods
-       // @method addHandler(name: String, HandlerClass: Function): this
-       // Adds a new `Handler` to the map, given its name and constructor function.
-       addHandler: function (name, HandlerClass) {
-               if (!HandlerClass) { return this; }
+               options = this._locateOptions = L.extend({
+                       timeout: 10000,
+                       watch: false
+                       // setView: false
+                       // maxZoom: <Number>
+                       // maximumAge: 0
+                       // enableHighAccuracy: false
+               }, options);
 
-               var handler = this[name] = new HandlerClass(this);
+               if (!('geolocation' in navigator)) {
+                       this._handleGeolocationError({
+                               code: 0,
+                               message: 'Geolocation not supported.'
+                       });
+                       return this;
+               }
 
-               this._handlers.push(handler);
+               var onResponse = L.bind(this._handleGeolocationResponse, this),
+                   onError = L.bind(this._handleGeolocationError, this);
 
-               if (this.options[name]) {
-                       handler.enable();
+               if (options.watch) {
+                       this._locationWatchId =
+                               navigator.geolocation.watchPosition(onResponse, onError, options);
+               } else {
+                       navigator.geolocation.getCurrentPosition(onResponse, onError, options);
                }
-
                return this;
        },
 
-       // @method remove(): this
-       // Destroys the map and clears all related event listeners.
-       remove: function () {
-
-               this._initEvents(true);
-
-               if (this._containerId !== this._container._leaflet_id) {
-                       throw new Error('Map container is being reused by another instance');
+       // @method stopLocate(): this
+       // Stops watching location previously initiated by `map.locate({watch: true})`
+       // and aborts resetting the map view if map.locate was called with
+       // `{setView: true}`.
+       stopLocate: function () {
+               if (navigator.geolocation && navigator.geolocation.clearWatch) {
+                       navigator.geolocation.clearWatch(this._locationWatchId);
                }
-
-               try {
-                       // throws error in IE6-8
-                       delete this._container._leaflet_id;
-                       delete this._containerId;
-               } catch (e) {
-                       /*eslint-disable */
-                       this._container._leaflet_id = undefined;
-                       /*eslint-enable */
-                       this._containerId = undefined;
+               if (this._locateOptions) {
+                       this._locateOptions.setView = false;
                }
+               return this;
+       },
 
-               L.DomUtil.remove(this._mapPane);
+       _handleGeolocationError: function (error) {
+               var c = error.code,
+                   message = error.message ||
+                           (c === 1 ? 'permission denied' :
+                           (c === 2 ? 'position unavailable' : 'timeout'));
 
-               if (this._clearControlPos) {
-                       this._clearControlPos();
+               if (this._locateOptions.setView && !this._loaded) {
+                       this.fitWorld();
                }
 
-               this._clearHandlers();
-
-               if (this._loaded) {
-                       // @section Map state change events
-                       // @event unload: Event
-                       // Fired when the map is destroyed with [remove](#map-remove) method.
-                       this.fire('unload');
-               }
+               // @section Location events
+               // @event locationerror: ErrorEvent
+               // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
+               this.fire('locationerror', {
+                       code: c,
+                       message: 'Geolocation error: ' + message + '.'
+               });
+       },
+
+       _handleGeolocationResponse: function (pos) {
+               var lat = pos.coords.latitude,
+                   lng = pos.coords.longitude,
+                   latlng = new L.LatLng(lat, lng),
+                   bounds = latlng.toBounds(pos.coords.accuracy),
+                   options = this._locateOptions;
+
+               if (options.setView) {
+                       var zoom = this.getBoundsZoom(bounds);
+                       this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
+               }
+
+               var data = {
+                       latlng: latlng,
+                       bounds: bounds,
+                       timestamp: pos.timestamp
+               };
+
+               for (var i in pos.coords) {
+                       if (typeof pos.coords[i] === 'number') {
+                               data[i] = pos.coords[i];
+                       }
+               }
+
+               // @event locationfound: LocationEvent
+               // Fired when geolocation (using the [`locate`](#map-locate) method)
+               // went successfully.
+               this.fire('locationfound', data);
+       },
+
+       // TODO handler.addTo
+       // TODO Appropiate docs section?
+       // @section Other Methods
+       // @method addHandler(name: String, HandlerClass: Function): this
+       // Adds a new `Handler` to the map, given its name and constructor function.
+       addHandler: function (name, HandlerClass) {
+               if (!HandlerClass) { return this; }
+
+               var handler = this[name] = new HandlerClass(this);
+
+               this._handlers.push(handler);
+
+               if (this.options[name]) {
+                       handler.enable();
+               }
+
+               return this;
+       },
+
+       // @method remove(): this
+       // Destroys the map and clears all related event listeners.
+       remove: function () {
+
+               this._initEvents(true);
+
+               if (this._containerId !== this._container._leaflet_id) {
+                       throw new Error('Map container is being reused by another instance');
+               }
+
+               try {
+                       // throws error in IE6-8
+                       delete this._container._leaflet_id;
+                       delete this._containerId;
+               } catch (e) {
+                       /*eslint-disable */
+                       this._container._leaflet_id = undefined;
+                       /*eslint-enable */
+                       this._containerId = undefined;
+               }
+
+               L.DomUtil.remove(this._mapPane);
+
+               if (this._clearControlPos) {
+                       this._clearControlPos();
+               }
+
+               this._clearHandlers();
+
+               if (this._loaded) {
+                       // @section Map state change events
+                       // @event unload: Event
+                       // Fired when the map is destroyed with [remove](#map-remove) method.
+                       this.fire('unload');
+               }
 
                for (var i in this._layers) {
                        this._layers[i].remove();
@@ -3356,6 +3635,16 @@ L.Map = L.Evented.extend({
                return this.project(latlng, zoom)._subtract(topLeft);
        },
 
+       _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
+               var topLeft = this._getNewPixelOrigin(center, zoom);
+               return L.bounds([
+                       this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
+                       this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
+                       this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
+                       this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)
+               ]);
+       },
+
        // layer point of the current center
        _getCenterLayerPoint: function () {
                return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
@@ -3425,6 +3714,125 @@ L.Map = L.Evented.extend({
                        zoom = Math.round(zoom / snap) * snap;
                }
                return Math.max(min, Math.min(max, zoom));
+       },
+
+       _onPanTransitionStep: function () {
+               this.fire('move');
+       },
+
+       _onPanTransitionEnd: function () {
+               L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
+               this.fire('moveend');
+       },
+
+       _tryAnimatedPan: function (center, options) {
+               // difference between the new and current centers in pixels
+               var offset = this._getCenterOffset(center)._floor();
+
+               // don't animate too far unless animate: true specified in options
+               if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
+
+               this.panBy(offset, options);
+
+               return true;
+       },
+
+       _createAnimProxy: function () {
+
+               var proxy = this._proxy = L.DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated');
+               this._panes.mapPane.appendChild(proxy);
+
+               this.on('zoomanim', function (e) {
+                       var prop = L.DomUtil.TRANSFORM,
+                           transform = proxy.style[prop];
+
+                       L.DomUtil.setTransform(proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));
+
+                       // workaround for case when transform is the same and so transitionend event is not fired
+                       if (transform === proxy.style[prop] && this._animatingZoom) {
+                               this._onZoomTransitionEnd();
+                       }
+               }, this);
+
+               this.on('load moveend', function () {
+                       var c = this.getCenter(),
+                           z = this.getZoom();
+                       L.DomUtil.setTransform(proxy, this.project(c, z), this.getZoomScale(z, 1));
+               }, this);
+       },
+
+       _catchTransitionEnd: function (e) {
+               if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
+                       this._onZoomTransitionEnd();
+               }
+       },
+
+       _nothingToAnimate: function () {
+               return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
+       },
+
+       _tryAnimatedZoom: function (center, zoom, options) {
+
+               if (this._animatingZoom) { return true; }
+
+               options = options || {};
+
+               // don't animate if disabled, not supported or zoom difference is too large
+               if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
+                       Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
+
+               // offset is the pixel coords of the zoom origin relative to the current center
+               var scale = this.getZoomScale(zoom),
+                   offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);
+
+               // don't animate if the zoom origin isn't within one screen from the current center, unless forced
+               if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
+
+               L.Util.requestAnimFrame(function () {
+                       this
+                           ._moveStart(true)
+                           ._animateZoom(center, zoom, true);
+               }, this);
+
+               return true;
+       },
+
+       _animateZoom: function (center, zoom, startAnim, noUpdate) {
+               if (startAnim) {
+                       this._animatingZoom = true;
+
+                       // remember what center/zoom to set after animation
+                       this._animateToCenter = center;
+                       this._animateToZoom = zoom;
+
+                       L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
+               }
+
+               // @event zoomanim: ZoomAnimEvent
+               // Fired on every frame of a zoom animation
+               this.fire('zoomanim', {
+                       center: center,
+                       zoom: zoom,
+                       noUpdate: noUpdate
+               });
+
+               // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
+               setTimeout(L.bind(this._onZoomTransitionEnd, this), 250);
+       },
+
+       _onZoomTransitionEnd: function () {
+               if (!this._animatingZoom) { return; }
+
+               L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
+
+               this._animatingZoom = false;
+
+               this._move(this._animateToCenter, this._animateToZoom);
+
+               // This anim frame should prevent an obscure iOS webkit tile loading race condition.
+               L.Util.requestAnimFrame(function () {
+                       this._moveEnd(true);
+               }, this);
        }
 });
 
@@ -3477,7 +3885,11 @@ L.Layer = L.Evented.extend({
                // @option pane: String = 'overlayPane'
                // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.
                pane: 'overlayPane',
-               nonBubblingEvents: []  // Array of events that should not be bubbled to DOM parents (like the map)
+               nonBubblingEvents: [],  // Array of events that should not be bubbled to DOM parents (like the map),
+
+               // @option attribution: String = null
+               // String to be shown in the attribution control, describes the layer data, e.g. "© Mapbox".
+               attribution: null,
        },
 
        /* @section
@@ -3522,6 +3934,12 @@ L.Layer = L.Evented.extend({
                return this;
        },
 
+       // @method getAttribution: String
+       // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
+       getAttribution: function () {
+               return this.options.attribution;
+       },
+
        _layerAdd: function (e) {
                var map = e.target;
 
@@ -3696,2944 +4114,2898 @@ L.Map.include({
                if (oldZoomSpan !== this._getZoomSpan()) {
                        this.fire('zoomlevelschange');
                }
+
+               if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
+                       this.setZoom(this._layersMaxZoom);
+               }
+               if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
+                       this.setZoom(this._layersMinZoom);
+               }
        }
 });
 
 
 
 /*
- * @namespace Projection
- * @projection L.Projection.Mercator
- *
- * Elliptical Mercator projection — more complex than Spherical Mercator. Takes into account that Earth is a geoid, not a perfect sphere. Used by the EPSG:3395 CRS.
+ * @namespace DomEvent
+ * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
  */
 
-L.Projection.Mercator = {
-       R: 6378137,
-       R_MINOR: 6356752.314245179,
+// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
 
-       bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]),
 
-       project: function (latlng) {
-               var d = Math.PI / 180,
-                   r = this.R,
-                   y = latlng.lat * d,
-                   tmp = this.R_MINOR / r,
-                   e = Math.sqrt(1 - tmp * tmp),
-                   con = e * Math.sin(y);
 
-               var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2);
-               y = -r * Math.log(Math.max(ts, 1E-10));
+var eventsKey = '_leaflet_events';
 
-               return new L.Point(latlng.lng * d * r, y);
-       },
+L.DomEvent = {
 
-       unproject: function (point) {
-               var d = 180 / Math.PI,
-                   r = this.R,
-                   tmp = this.R_MINOR / r,
-                   e = Math.sqrt(1 - tmp * tmp),
-                   ts = Math.exp(-point.y / r),
-                   phi = Math.PI / 2 - 2 * Math.atan(ts);
+       // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
+       // Adds a listener function (`fn`) to a particular DOM event type of the
+       // element `el`. You can optionally specify the context of the listener
+       // (object the `this` keyword will point to). You can also pass several
+       // space-separated types (e.g. `'click dblclick'`).
 
-               for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) {
-                       con = e * Math.sin(phi);
-                       con = Math.pow((1 - con) / (1 + con), e / 2);
-                       dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi;
-                       phi += dphi;
+       // @alternative
+       // @function on(el: HTMLElement, eventMap: Object, context?: Object): this
+       // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+       on: function (obj, types, fn, context) {
+
+               if (typeof types === 'object') {
+                       for (var type in types) {
+                               this._on(obj, type, types[type], fn);
+                       }
+               } else {
+                       types = L.Util.splitWords(types);
+
+                       for (var i = 0, len = types.length; i < len; i++) {
+                               this._on(obj, types[i], fn, context);
+                       }
                }
 
-               return new L.LatLng(phi * d, point.x * d / r);
-       }
-};
+               return this;
+       },
 
+       // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
+       // Removes a previously added listener function. If no function is specified,
+       // it will remove all the listeners of that particular DOM event from the element.
+       // Note that if you passed a custom context to on, you must pass the same
+       // context to `off` in order to remove the listener.
 
+       // @alternative
+       // @function off(el: HTMLElement, eventMap: Object, context?: Object): this
+       // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+       off: function (obj, types, fn, context) {
 
-/*
- * @namespace CRS
- * @crs L.CRS.EPSG3395
- *
- * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection.
- */
+               if (typeof types === 'object') {
+                       for (var type in types) {
+                               this._off(obj, type, types[type], fn);
+                       }
+               } else {
+                       types = L.Util.splitWords(types);
 
-L.CRS.EPSG3395 = L.extend({}, L.CRS.Earth, {
-       code: 'EPSG:3395',
-       projection: L.Projection.Mercator,
+                       for (var i = 0, len = types.length; i < len; i++) {
+                               this._off(obj, types[i], fn, context);
+                       }
+               }
 
-       transformation: (function () {
-               var scale = 0.5 / (Math.PI * L.Projection.Mercator.R);
-               return new L.Transformation(scale, 0.5, -scale, 0.5);
-       }())
-});
+               return this;
+       },
 
+       _on: function (obj, type, fn, context) {
+               var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : '');
 
+               if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
 
-/*
- * @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
- */
+               var handler = function (e) {
+                       return fn.call(context || obj, e || window.event);
+               };
 
+               var originalHandler = handler;
 
-L.GridLayer = L.Layer.extend({
+               if (L.Browser.pointer && type.indexOf('touch') === 0) {
+                       this.addPointerListener(obj, type, handler, id);
 
-       // @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,
+               } else if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
+                       this.addDoubleTapListener(obj, handler, id);
 
-               // @option opacity: Number = 1.0
-               // Opacity of the tiles. Can be used in the `createTile()` function.
-               opacity: 1,
+               } else if ('addEventListener' in obj) {
 
-               // @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: L.Browser.mobile,
+                       if (type === 'mousewheel') {
+                               obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
 
-               // @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,
+                       } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
+                               handler = function (e) {
+                                       e = e || window.event;
+                                       if (L.DomEvent._isExternalTarget(obj, e)) {
+                                               originalHandler(e);
+                                       }
+                               };
+                               obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false);
 
-               // @option updateInterval: Number = 200
-               // Tiles will not update more than once every `updateInterval` milliseconds when panning.
-               updateInterval: 200,
+                       } else {
+                               if (type === 'click' && L.Browser.android) {
+                                       handler = function (e) {
+                                               return L.DomEvent._filterClick(e, originalHandler);
+                                       };
+                               }
+                               obj.addEventListener(type, handler, false);
+                       }
 
-               // @option attribution: String = null
-               // String to be shown in the attribution control, describes the layer data, e.g. "© Mapbox".
-               attribution: null,
+               } else if ('attachEvent' in obj) {
+                       obj.attachEvent('on' + type, handler);
+               }
 
-               // @option zIndex: Number = 1
-               // The explicit zIndex of the tile layer.
-               zIndex: 1,
+               obj[eventsKey] = obj[eventsKey] || {};
+               obj[eventsKey][id] = handler;
 
-               // @option bounds: LatLngBounds = undefined
-               // If set, tiles will only be loaded inside the set `LatLngBounds`.
-               bounds: null,
+               return this;
+       },
 
-               // @option minZoom: Number = 0
-               // The minimum zoom level that tiles will be loaded at. By default the entire map.
-               minZoom: 0,
+       _off: function (obj, type, fn, context) {
 
-               // @option maxZoom: Number = undefined
-               // The maximum zoom level that tiles will be loaded at.
-               maxZoom: undefined,
+               var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''),
+                   handler = obj[eventsKey] && obj[eventsKey][id];
 
-               // @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.
-               noWrap: false,
+               if (!handler) { return this; }
 
-               // @option pane: String = 'tilePane'
-               // `Map pane` where the grid layer will be added.
-               pane: 'tilePane',
+               if (L.Browser.pointer && type.indexOf('touch') === 0) {
+                       this.removePointerListener(obj, type, id);
 
-               // @option className: String = ''
-               // A custom class name to assign to the tile layer. Empty by default.
-               className: '',
+               } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
+                       this.removeDoubleTapListener(obj, id);
 
-               // @option keepBuffer: Number = 2
-               // When panning the map, keep this many rows and columns of tiles before unloading them.
-               keepBuffer: 2
-       },
+               } else if ('removeEventListener' in obj) {
 
-       initialize: function (options) {
-               L.setOptions(this, options);
-       },
+                       if (type === 'mousewheel') {
+                               obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
 
-       onAdd: function () {
-               this._initContainer();
+                       } else {
+                               obj.removeEventListener(
+                                       type === 'mouseenter' ? 'mouseover' :
+                                       type === 'mouseleave' ? 'mouseout' : type, handler, false);
+                       }
 
-               this._levels = {};
-               this._tiles = {};
+               } else if ('detachEvent' in obj) {
+                       obj.detachEvent('on' + type, handler);
+               }
 
-               this._resetView();
-               this._update();
-       },
+               obj[eventsKey][id] = null;
 
-       beforeAdd: function (map) {
-               map._addZoomLimit(this);
+               return this;
        },
 
-       onRemove: function (map) {
-               this._removeAllTiles();
-               L.DomUtil.remove(this._container);
-               map._removeZoomLimit(this);
-               this._container = null;
-               this._tileZoom = null;
-       },
+       // @function stopPropagation(ev: DOMEvent): this
+       // Stop the given event from propagation to parent elements. Used inside the listener functions:
+       // ```js
+       // L.DomEvent.on(div, 'click', function (ev) {
+       //      L.DomEvent.stopPropagation(ev);
+       // });
+       // ```
+       stopPropagation: function (e) {
 
-       // @method bringToFront: this
-       // Brings the tile layer to the top of all tile layers.
-       bringToFront: function () {
-               if (this._map) {
-                       L.DomUtil.toFront(this._container);
-                       this._setAutoZIndex(Math.max);
+               if (e.stopPropagation) {
+                       e.stopPropagation();
+               } else if (e.originalEvent) {  // In case of Leaflet event.
+                       e.originalEvent._stopped = true;
+               } else {
+                       e.cancelBubble = true;
                }
-               return this;
-       },
+               L.DomEvent._skipped(e);
 
-       // @method bringToBack: this
-       // Brings the tile layer to the bottom of all tile layers.
-       bringToBack: function () {
-               if (this._map) {
-                       L.DomUtil.toBack(this._container);
-                       this._setAutoZIndex(Math.min);
-               }
                return this;
        },
 
-       // @method getAttribution: String
-       // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
-       getAttribution: function () {
-               return this.options.attribution;
-       },
-
-       // @method getContainer: HTMLElement
-       // Returns the HTML element that contains the tiles for this layer.
-       getContainer: function () {
-               return this._container;
+       // @function disableScrollPropagation(el: HTMLElement): this
+       // Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants).
+       disableScrollPropagation: function (el) {
+               return L.DomEvent.on(el, 'mousewheel', L.DomEvent.stopPropagation);
        },
 
-       // @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;
-       },
+       // @function disableClickPropagation(el: HTMLElement): this
+       // Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`,
+       // `'mousedown'` and `'touchstart'` events (plus browser variants).
+       disableClickPropagation: function (el) {
+               var stop = L.DomEvent.stopPropagation;
 
-       // @method setZIndex(zIndex: Number): this
-       // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
-       setZIndex: function (zIndex) {
-               this.options.zIndex = zIndex;
-               this._updateZIndex();
+               L.DomEvent.on(el, L.Draggable.START.join(' '), stop);
 
-               return this;
+               return L.DomEvent.on(el, {
+                       click: L.DomEvent._fakeStop,
+                       dblclick: stop
+               });
        },
 
-       // @method isLoading: Boolean
-       // Returns `true` if any tile in the grid layer has not finished loading.
-       isLoading: function () {
-               return this._loading;
-       },
+       // @function preventDefault(ev: DOMEvent): this
+       // Prevents the default action of the DOM Event `ev` from happening (such as
+       // following a link in the href of the a element, or doing a POST request
+       // with page reload when a `<form>` is submitted).
+       // Use it inside listener functions.
+       preventDefault: function (e) {
 
-       // @method redraw: this
-       // Causes the layer to clear all the tiles and request them again.
-       redraw: function () {
-               if (this._map) {
-                       this._removeAllTiles();
-                       this._update();
+               if (e.preventDefault) {
+                       e.preventDefault();
+               } else {
+                       e.returnValue = false;
                }
                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 = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
-                       }
+       // @function stop(ev): this
+       // Does `stopPropagation` and `preventDefault` at the same time.
+       stop: function (e) {
+               return L.DomEvent
+                       .preventDefault(e)
+                       .stopPropagation(e);
+       },
 
-                       events.move = this._onMove;
+       // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
+       // Gets normalized mouse position from a DOM event relative to the
+       // `container` or to the whole page if not specified.
+       getMousePosition: function (e, container) {
+               if (!container) {
+                       return new L.Point(e.clientX, e.clientY);
                }
 
-               if (this._zoomAnimated) {
-                       events.zoomanim = this._animateZoom;
-               }
+               var rect = container.getBoundingClientRect();
 
-               return events;
+               return new L.Point(
+                       e.clientX - rect.left - container.clientLeft,
+                       e.clientY - rect.top - container.clientTop);
        },
 
-       // @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');
-       },
+       // Chrome on Win scrolls double the pixels as in other platforms (see #4538),
+       // and Firefox scrolls device pixels, not CSS pixels
+       _wheelPxFactor: (L.Browser.win && L.Browser.chrome) ? 2 :
+                       L.Browser.gecko ? window.devicePixelRatio :
+                       1,
 
-       // @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 L.Point ? s : new L.Point(s, s);
+       // @function getWheelDelta(ev: DOMEvent): Number
+       // Gets normalized wheel delta from a mousewheel DOM event, in vertical
+       // pixels scrolled (negative if scrolling down).
+       // Events from pointing devices without precise scrolling are mapped to
+       // a best guess of 60 pixels.
+       getWheelDelta: function (e) {
+               return (L.Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
+                      (e.deltaY && e.deltaMode === 0) ? -e.deltaY / L.DomEvent._wheelPxFactor : // Pixels
+                      (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
+                      (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
+                      (e.deltaX || e.deltaZ) ? 0 :     // Skip horizontal/depth wheel events
+                      e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
+                      (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
+                      e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
+                      0;
        },
 
-       _updateZIndex: function () {
-               if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
-                       this._container.style.zIndex = this.options.zIndex;
-               }
+       _skipEvents: {},
+
+       _fakeStop: function (e) {
+               // fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e)
+               L.DomEvent._skipEvents[e.type] = true;
        },
 
-       _setAutoZIndex: function (compare) {
-               // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
+       _skipped: function (e) {
+               var skipped = this._skipEvents[e.type];
+               // reset when checking, as it's only used in map container and propagates outside of the map
+               this._skipEvents[e.type] = false;
+               return skipped;
+       },
 
-               var layers = this.getPane().children,
-                   edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
+       // check if element really left/entered the event target (for mouseenter/mouseleave)
+       _isExternalTarget: function (el, e) {
 
-               for (var i = 0, len = layers.length, zIndex; i < len; i++) {
+               var related = e.relatedTarget;
 
-                       zIndex = layers[i].style.zIndex;
+               if (!related) { return true; }
 
-                       if (layers[i] !== this._container && zIndex) {
-                               edgeZIndex = compare(edgeZIndex, +zIndex);
+               try {
+                       while (related && (related !== el)) {
+                               related = related.parentNode;
                        }
+               } catch (err) {
+                       return false;
                }
-
-               if (isFinite(edgeZIndex)) {
-                       this.options.zIndex = edgeZIndex + compare(-1, 1);
-                       this._updateZIndex();
-               }
+               return (related !== el);
        },
 
-       _updateOpacity: function () {
-               if (!this._map) { return; }
-
-               // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
-               if (L.Browser.ielt9) { return; }
-
-               L.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; }
+       // this is a horrible workaround for a bug in Android where a single touch triggers two click events
+       _filterClick: function (e, handler) {
+               var timeStamp = (e.timeStamp || (e.originalEvent && e.originalEvent.timeStamp)),
+                   elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick);
 
-                       var fade = Math.min(1, (now - tile.loaded) / 200);
+               // are they closer together than 500ms yet more than 100ms?
+               // Android typically triggers them ~300ms apart while multiple listeners
+               // on the same event should be triggered far faster;
+               // or check if click is simulated on the element, and if it is, reject any non-simulated events
 
-                       L.DomUtil.setOpacity(tile.el, fade);
-                       if (fade < 1) {
-                               nextFrame = true;
-                       } else {
-                               if (tile.active) { willPrune = true; }
-                               tile.active = true;
-                       }
+               if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
+                       L.DomEvent.stop(e);
+                       return;
                }
+               L.DomEvent._lastClick = timeStamp;
 
-               if (willPrune && !this._noPrune) { this._pruneTiles(); }
+               handler(e);
+       }
+};
 
-               if (nextFrame) {
-                       L.Util.cancelAnimFrame(this._fadeFrame);
-                       this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
-               }
-       },
+// @function addListener(…): this
+// Alias to [`L.DomEvent.on`](#domevent-on)
+L.DomEvent.addListener = L.DomEvent.on;
 
-       _initContainer: function () {
-               if (this._container) { return; }
+// @function removeListener(…): this
+// Alias to [`L.DomEvent.off`](#domevent-off)
+L.DomEvent.removeListener = L.DomEvent.off;
 
-               this._container = L.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 {
-                               L.DomUtil.remove(this._levels[z].el);
-                               this._removeTilesAtZoom(z);
-                               delete this._levels[z];
-                       }
-               }
+/*
+ * @class PosAnimation
+ * @aka L.PosAnimation
+ * @inherits Evented
+ * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9.
+ *
+ * @example
+ * ```js
+ * var fx = new L.PosAnimation();
+ * fx.run(el, [300, 500], 0.5);
+ * ```
+ *
+ * @constructor L.PosAnimation()
+ * Creates a `PosAnimation` object.
+ *
+ */
 
-               var level = this._levels[zoom],
-                   map = this._map;
+L.PosAnimation = L.Evented.extend({
 
-               if (!level) {
-                       level = this._levels[zoom] = {};
+       // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number)
+       // Run an animation of a given element to a new position, optionally setting
+       // duration in seconds (`0.25` by default) and easing linearity factor (3rd
+       // argument of the [cubic bezier curve](http://cubic-bezier.com/#0,0,.5,1),
+       // `0.5` by default).
+       run: function (el, newPos, duration, easeLinearity) {
+               this.stop();
 
-                       level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
-                       level.el.style.zIndex = maxZoom;
+               this._el = el;
+               this._inProgress = true;
+               this._duration = duration || 0.25;
+               this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2);
 
-                       level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
-                       level.zoom = zoom;
+               this._startPos = L.DomUtil.getPosition(el);
+               this._offset = newPos.subtract(this._startPos);
+               this._startTime = +new Date();
 
-                       this._setZoomTransform(level, map.getCenter(), map.getZoom());
+               // @event start: Event
+               // Fired when the animation starts
+               this.fire('start');
 
-                       // force the browser to consider the newly added element for transition
-                       L.Util.falseFn(level.el.offsetWidth);
-               }
+               this._animate();
+       },
 
-               this._level = level;
+       // @method stop()
+       // Stops the animation (if currently running).
+       stop: function () {
+               if (!this._inProgress) { return; }
 
-               return level;
+               this._step(true);
+               this._complete();
        },
 
-       _pruneTiles: function () {
-               if (!this._map) {
-                       return;
-               }
-
-               var key, tile;
+       _animate: function () {
+               // animation loop
+               this._animId = L.Util.requestAnimFrame(this._animate, this);
+               this._step();
+       },
 
-               var zoom = this._map.getZoom();
-               if (zoom > this.options.maxZoom ||
-                       zoom < this.options.minZoom) {
-                       this._removeAllTiles();
-                       return;
-               }
+       _step: function (round) {
+               var elapsed = (+new Date()) - this._startTime,
+                   duration = this._duration * 1000;
 
-               for (key in this._tiles) {
-                       tile = this._tiles[key];
-                       tile.retain = tile.current;
+               if (elapsed < duration) {
+                       this._runFrame(this._easeOut(elapsed / duration), round);
+               } else {
+                       this._runFrame(1);
+                       this._complete();
                }
+       },
 
-               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);
-                               }
-                       }
+       _runFrame: function (progress, round) {
+               var pos = this._startPos.add(this._offset.multiplyBy(progress));
+               if (round) {
+                       pos._round();
                }
+               L.DomUtil.setPosition(this._el, pos);
 
-               for (key in this._tiles) {
-                       if (!this._tiles[key].retain) {
-                               this._removeTile(key);
-                       }
-               }
+               // @event step: Event
+               // Fired continuously during the animation.
+               this.fire('step');
        },
 
-       _removeTilesAtZoom: function (zoom) {
-               for (var key in this._tiles) {
-                       if (this._tiles[key].coords.z !== zoom) {
-                               continue;
-                       }
-                       this._removeTile(key);
-               }
-       },
+       _complete: function () {
+               L.Util.cancelAnimFrame(this._animId);
 
-       _removeAllTiles: function () {
-               for (var key in this._tiles) {
-                       this._removeTile(key);
-               }
+               this._inProgress = false;
+               // @event end: Event
+               // Fired when the animation ends.
+               this.fire('end');
        },
 
-       _invalidateAll: function () {
-               for (var z in this._levels) {
-                       L.DomUtil.remove(this._levels[z].el);
-                       delete this._levels[z];
-               }
-               this._removeAllTiles();
+       _easeOut: function (t) {
+               return 1 - Math.pow(1 - t, this._easeOutPower);
+       }
+});
 
-               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 L.Point(+x2, +y2);
-               coords2.z = +z2;
 
-               var key = this._tileCoordsToKey(coords2),
-                   tile = this._tiles[key];
+/*
+ * @namespace Projection
+ * @projection L.Projection.Mercator
+ *
+ * Elliptical Mercator projection — more complex than Spherical Mercator. Takes into account that Earth is a geoid, not a perfect sphere. Used by the EPSG:3395 CRS.
+ */
 
-               if (tile && tile.active) {
-                       tile.retain = true;
-                       return true;
+L.Projection.Mercator = {
+       R: 6378137,
+       R_MINOR: 6356752.314245179,
 
-               } else if (tile && tile.loaded) {
-                       tile.retain = true;
-               }
+       bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]),
 
-               if (z2 > minZoom) {
-                       return this._retainParent(x2, y2, z2, minZoom);
-               }
+       project: function (latlng) {
+               var d = Math.PI / 180,
+                   r = this.R,
+                   y = latlng.lat * d,
+                   tmp = this.R_MINOR / r,
+                   e = Math.sqrt(1 - tmp * tmp),
+                   con = e * Math.sin(y);
 
-               return false;
+               var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2);
+               y = -r * Math.log(Math.max(ts, 1E-10));
+
+               return new L.Point(latlng.lng * d * r, y);
        },
 
-       _retainChildren: function (x, y, z, maxZoom) {
+       unproject: function (point) {
+               var d = 180 / Math.PI,
+                   r = this.R,
+                   tmp = this.R_MINOR / r,
+                   e = Math.sqrt(1 - tmp * tmp),
+                   ts = Math.exp(-point.y / r),
+                   phi = Math.PI / 2 - 2 * Math.atan(ts);
 
-               for (var i = 2 * x; i < 2 * x + 2; i++) {
-                       for (var j = 2 * y; j < 2 * y + 2; j++) {
+               for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) {
+                       con = e * Math.sin(phi);
+                       con = Math.pow((1 - con) / (1 + con), e / 2);
+                       dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi;
+                       phi += dphi;
+               }
 
-                               var coords = new L.Point(i, j);
-                               coords.z = z + 1;
+               return new L.LatLng(phi * d, point.x * d / r);
+       }
+};
 
-                               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;
-                               }
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG3395
+ *
+ * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection.
+ */
 
-                               if (z + 1 < maxZoom) {
-                                       this._retainChildren(i, j, z + 1, maxZoom);
-                               }
-                       }
-               }
-       },
+L.CRS.EPSG3395 = L.extend({}, L.CRS.Earth, {
+       code: 'EPSG:3395',
+       projection: L.Projection.Mercator,
 
-       _resetView: function (e) {
-               var animating = e && (e.pinch || e.flyTo);
-               this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
-       },
+       transformation: (function () {
+               var scale = 0.5 / (Math.PI * L.Projection.Mercator.R);
+               return new L.Transformation(scale, 0.5, -scale, 0.5);
+       }())
+});
 
-       _animateZoom: function (e) {
-               this._setView(e.center, e.zoom, true, e.noUpdate);
-       },
 
-       _setView: function (center, zoom, noPrune, noUpdate) {
-               var tileZoom = 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);
+/*
+ * @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
+ */
 
-               if (!noUpdate || tileZoomChanged) {
 
-                       this._tileZoom = tileZoom;
+L.GridLayer = L.Layer.extend({
 
-                       if (this._abortLoading) {
-                               this._abortLoading();
-                       }
+       // @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,
 
-                       this._updateLevels();
-                       this._resetGrid();
+               // @option opacity: Number = 1.0
+               // Opacity of the tiles. Can be used in the `createTile()` function.
+               opacity: 1,
 
-                       if (tileZoom !== undefined) {
-                               this._update(center);
-                       }
+               // @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: L.Browser.mobile,
 
-                       if (!noPrune) {
-                               this._pruneTiles();
-                       }
+               // @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,
 
-                       // Flag to prevent _updateOpacity from pruning tiles during
-                       // a zoom anim or a pinch gesture
-                       this._noPrune = !!noPrune;
-               }
+               // @option updateInterval: Number = 200
+               // Tiles will not update more than once every `updateInterval` milliseconds when panning.
+               updateInterval: 200,
 
-               this._setZoomTransforms(center, zoom);
-       },
+               // @option zIndex: Number = 1
+               // The explicit zIndex of the tile layer.
+               zIndex: 1,
 
-       _setZoomTransforms: function (center, zoom) {
-               for (var i in this._levels) {
-                       this._setZoomTransform(this._levels[i], center, zoom);
-               }
-       },
+               // @option bounds: LatLngBounds = undefined
+               // If set, tiles will only be loaded inside the set `LatLngBounds`.
+               bounds: null,
 
-       _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();
+               // @option minZoom: Number = 0
+               // The minimum zoom level that tiles will be loaded at. By default the entire map.
+               minZoom: 0,
 
-               if (L.Browser.any3d) {
-                       L.DomUtil.setTransform(level.el, translate, scale);
-               } else {
-                       L.DomUtil.setPosition(level.el, translate);
-               }
-       },
+               // @option maxZoom: Number = undefined
+               // The maximum zoom level that tiles will be loaded at.
+               maxZoom: undefined,
 
-       _resetGrid: function () {
-               var map = this._map,
-                   crs = map.options.crs,
-                   tileSize = this._tileSize = this.getTileSize(),
-                   tileZoom = this._tileZoom;
+               // @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.
+               noWrap: false,
 
-               var bounds = this._map.getPixelWorldBounds(this._tileZoom);
-               if (bounds) {
-                       this._globalTileRange = this._pxBoundsToTileRange(bounds);
-               }
+               // @option pane: String = 'tilePane'
+               // `Map pane` where the grid layer will be added.
+               pane: 'tilePane',
 
-               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)
-               ];
-       },
+               // @option className: String = ''
+               // A custom class name to assign to the tile layer. Empty by default.
+               className: '',
 
-       _onMoveEnd: function () {
-               if (!this._map || this._map._animatingZoom) { return; }
+               // @option keepBuffer: Number = 2
+               // When panning the map, keep this many rows and columns of tiles before unloading them.
+               keepBuffer: 2
+       },
 
-               this._update();
+       initialize: function (options) {
+               L.setOptions(this, options);
        },
 
-       _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);
+       onAdd: function () {
+               this._initContainer();
 
-               return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
+               this._levels = {};
+               this._tiles = {};
+
+               this._resetView();
+               this._update();
        },
 
-       // 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 = map.getZoom();
+       beforeAdd: function (map) {
+               map._addZoomLimit(this);
+       },
 
-               if (center === undefined) { center = map.getCenter(); }
-               if (this._tileZoom === undefined) { return; }   // if out of minzoom/maxzoom
+       onRemove: function (map) {
+               this._removeAllTiles();
+               L.DomUtil.remove(this._container);
+               map._removeZoomLimit(this);
+               this._container = null;
+               this._tileZoom = null;
+       },
 
-               var pixelBounds = this._getTiledPixelBounds(center),
-                   tileRange = this._pxBoundsToTileRange(pixelBounds),
-                   tileCenter = tileRange.getCenter(),
-                   queue = [],
-                   margin = this.options.keepBuffer,
-                   noPruneRange = new L.Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
-                                             tileRange.getTopRight().add([margin, -margin]));
+       // @method bringToFront: this
+       // Brings the tile layer to the top of all tile layers.
+       bringToFront: function () {
+               if (this._map) {
+                       L.DomUtil.toFront(this._container);
+                       this._setAutoZIndex(Math.max);
+               }
+               return this;
+       },
 
-               for (var key in this._tiles) {
-                       var c = this._tiles[key].coords;
-                       if (c.z !== this._tileZoom || !noPruneRange.contains(L.point(c.x, c.y))) {
-                               this._tiles[key].current = false;
-                       }
+       // @method bringToBack: this
+       // Brings the tile layer to the bottom of all tile layers.
+       bringToBack: function () {
+               if (this._map) {
+                       L.DomUtil.toBack(this._container);
+                       this._setAutoZIndex(Math.min);
                }
+               return this;
+       },
 
-               // _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; }
+       // @method getContainer: HTMLElement
+       // Returns the HTML element that contains the tiles for this layer.
+       getContainer: function () {
+               return this._container;
+       },
 
-               // 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 L.Point(i, j);
-                               coords.z = this._tileZoom;
+       // @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;
+       },
 
-                               if (!this._isValidTile(coords)) { continue; }
+       // @method setZIndex(zIndex: Number): this
+       // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
+       setZIndex: function (zIndex) {
+               this.options.zIndex = zIndex;
+               this._updateZIndex();
 
-                               var tile = this._tiles[this._tileCoordsToKey(coords)];
-                               if (tile) {
-                                       tile.current = true;
-                               } else {
-                                       queue.push(coords);
-                               }
-                       }
-               }
+               return this;
+       },
 
-               // 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);
-               });
+       // @method isLoading: Boolean
+       // Returns `true` if any tile in the grid layer has not finished loading.
+       isLoading: function () {
+               return this._loading;
+       },
 
-               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');
-                       }
+       // @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;
+       },
 
-                       // create DOM fragment to append tiles in one batch
-                       var fragment = document.createDocumentFragment();
+       getEvents: function () {
+               var events = {
+                       viewprereset: this._invalidateAll,
+                       viewreset: this._resetView,
+                       zoom: this._resetView,
+                       moveend: this._onMoveEnd
+               };
 
-                       for (i = 0; i < queue.length; i++) {
-                               this._addTile(queue[i], fragment);
+               if (!this.options.updateWhenIdle) {
+                       // update tiles on move, but not more often than once per given interval
+                       if (!this._onMove) {
+                               this._onMove = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
                        }
 
-                       this._level.el.appendChild(fragment);
+                       events.move = this._onMove;
                }
-       },
-
-       _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._zoomAnimated) {
+                       events.zoomanim = this._animateZoom;
                }
 
-               if (!this.options.bounds) { return true; }
+               return events;
+       },
 
-               // don't load tile if it doesn't intersect the bounds in options
-               var tileBounds = this._tileCoordsToBounds(coords);
-               return L.latLngBounds(this.options.bounds).overlaps(tileBounds);
+       // @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');
        },
 
-       _keyToBounds: function (key) {
-               return this._tileCoordsToBounds(this._keyToTileCoords(key));
+       // @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 L.Point ? s : new L.Point(s, s);
        },
 
-       // converts tile coordinates to its geographical bounds
-       _tileCoordsToBounds: function (coords) {
+       _updateZIndex: function () {
+               if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
+                       this._container.style.zIndex = this.options.zIndex;
+               }
+       },
 
-               var map = this._map,
-                   tileSize = this.getTileSize(),
+       _setAutoZIndex: function (compare) {
+               // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
 
-                   nwPoint = coords.scaleBy(tileSize),
-                   sePoint = nwPoint.add(tileSize),
+               var layers = this.getPane().children,
+                   edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
 
-                   nw = map.unproject(nwPoint, coords.z),
-                   se = map.unproject(sePoint, coords.z);
+               for (var i = 0, len = layers.length, zIndex; i < len; i++) {
 
-               if (!this.options.noWrap) {
-                       nw = map.wrapLatLng(nw);
-                       se = map.wrapLatLng(se);
-               }
+                       zIndex = layers[i].style.zIndex;
 
-               return new L.LatLngBounds(nw, se);
-       },
+                       if (layers[i] !== this._container && zIndex) {
+                               edgeZIndex = compare(edgeZIndex, +zIndex);
+                       }
+               }
 
-       // converts tile coordinates to key for the tile cache
-       _tileCoordsToKey: function (coords) {
-               return coords.x + ':' + coords.y + ':' + coords.z;
+               if (isFinite(edgeZIndex)) {
+                       this.options.zIndex = edgeZIndex + compare(-1, 1);
+                       this._updateZIndex();
+               }
        },
 
-       // converts tile cache key to coordinates
-       _keyToTileCoords: function (key) {
-               var k = key.split(':'),
-                   coords = new L.Point(+k[0], +k[1]);
-               coords.z = +k[2];
-               return coords;
-       },
+       _updateOpacity: function () {
+               if (!this._map) { return; }
 
-       _removeTile: function (key) {
-               var tile = this._tiles[key];
-               if (!tile) { return; }
+               // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
+               if (L.Browser.ielt9) { return; }
 
-               L.DomUtil.remove(tile.el);
+               L.DomUtil.setOpacity(this._container, this.options.opacity);
 
-               delete this._tiles[key];
+               var now = +new Date(),
+                   nextFrame = false,
+                   willPrune = false;
 
-               // @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)
-               });
-       },
+               for (var key in this._tiles) {
+                       var tile = this._tiles[key];
+                       if (!tile.current || !tile.loaded) { continue; }
 
-       _initTile: function (tile) {
-               L.DomUtil.addClass(tile, 'leaflet-tile');
+                       var fade = Math.min(1, (now - tile.loaded) / 200);
 
-               var tileSize = this.getTileSize();
-               tile.style.width = tileSize.x + 'px';
-               tile.style.height = tileSize.y + 'px';
+                       L.DomUtil.setOpacity(tile.el, fade);
+                       if (fade < 1) {
+                               nextFrame = true;
+                       } else {
+                               if (tile.active) { willPrune = true; }
+                               tile.active = true;
+                       }
+               }
 
-               tile.onselectstart = L.Util.falseFn;
-               tile.onmousemove = L.Util.falseFn;
+               if (willPrune && !this._noPrune) { this._pruneTiles(); }
 
-               // update opacity on tiles in IE7-8 because of filter inheritance problems
-               if (L.Browser.ielt9 && this.options.opacity < 1) {
-                       L.DomUtil.setOpacity(tile, this.options.opacity);
+               if (nextFrame) {
+                       L.Util.cancelAnimFrame(this._fadeFrame);
+                       this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
                }
+       },
 
-               // without this hack, tiles disappear after zoom on Chrome for Android
-               // https://github.com/Leaflet/Leaflet/issues/2078
-               if (L.Browser.android && !L.Browser.android23) {
-                       tile.style.WebkitBackfaceVisibility = 'hidden';
+       _initContainer: function () {
+               if (this._container) { return; }
+
+               this._container = L.DomUtil.create('div', 'leaflet-layer ' + (this.options.className || ''));
+               this._updateZIndex();
+
+               if (this.options.opacity < 1) {
+                       this._updateOpacity();
                }
+
+               this.getPane().appendChild(this._container);
        },
 
-       _addTile: function (coords, container) {
-               var tilePos = this._getTilePos(coords),
-                   key = this._tileCoordsToKey(coords);
+       _updateLevels: function () {
 
-               var tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords));
+               var zoom = this._tileZoom,
+                   maxZoom = this.options.maxZoom;
 
-               this._initTile(tile);
+               if (zoom === undefined) { return undefined; }
 
-               // 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
-                       L.Util.requestAnimFrame(L.bind(this._tileReady, this, coords, null, tile));
+               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 {
+                               L.DomUtil.remove(this._levels[z].el);
+                               this._removeTilesAtZoom(z);
+                               delete this._levels[z];
+                       }
                }
 
-               L.DomUtil.setPosition(tile, tilePos);
+               var level = this._levels[zoom],
+                   map = this._map;
 
-               // save tile in cache
-               this._tiles[key] = {
-                       el: tile,
-                       coords: coords,
-                       current: true
-               };
+               if (!level) {
+                       level = this._levels[zoom] = {};
 
-               container.appendChild(tile);
-               // @event tileloadstart: TileEvent
-               // Fired when a tile is requested and starts loading.
-               this.fire('tileloadstart', {
-                       tile: tile,
-                       coords: coords
-               });
-       },
+                       level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
+                       level.el.style.zIndex = maxZoom;
 
-       _tileReady: function (coords, err, tile) {
-               if (!this._map) { return; }
+                       level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
+                       level.zoom = zoom;
 
-               if (err) {
-                       // @event tileerror: TileErrorEvent
-                       // Fired when there is an error loading a tile.
-                       this.fire('tileerror', {
-                               error: err,
-                               tile: tile,
-                               coords: coords
-                       });
+                       this._setZoomTransform(level, map.getCenter(), map.getZoom());
+
+                       // force the browser to consider the newly added element for transition
+                       L.Util.falseFn(level.el.offsetWidth);
                }
 
-               var key = this._tileCoordsToKey(coords);
+               this._level = level;
 
-               tile = this._tiles[key];
-               if (!tile) { return; }
+               return level;
+       },
 
-               tile.loaded = +new Date();
-               if (this._map._fadeAnimated) {
-                       L.DomUtil.setOpacity(tile.el, 0);
-                       L.Util.cancelAnimFrame(this._fadeFrame);
-                       this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
-               } else {
-                       tile.active = true;
-                       this._pruneTiles();
+       _pruneTiles: function () {
+               if (!this._map) {
+                       return;
                }
 
-               if (!err) {
-                       L.DomUtil.addClass(tile.el, 'leaflet-tile-loaded');
+               var key, tile;
 
-                       // @event tileload: TileEvent
-                       // Fired when a tile loads.
-                       this.fire('tileload', {
-                               tile: tile.el,
-                               coords: coords
-                       });
+               var zoom = this._map.getZoom();
+               if (zoom > this.options.maxZoom ||
+                       zoom < this.options.minZoom) {
+                       this._removeAllTiles();
+                       return;
                }
 
-               if (this._noTilesToLoad()) {
-                       this._loading = false;
-                       // @event load: Event
-                       // Fired when the grid layer loaded all visible tiles.
-                       this.fire('load');
+               for (key in this._tiles) {
+                       tile = this._tiles[key];
+                       tile.retain = tile.current;
+               }
 
-                       if (L.Browser.ielt9 || !this._map._fadeAnimated) {
-                               L.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(L.bind(this._pruneTiles, this), 250);
+               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);
+                               }
                        }
                }
-       },
 
-       _getTilePos: function (coords) {
-               return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
+               for (key in this._tiles) {
+                       if (!this._tiles[key].retain) {
+                               this._removeTile(key);
+                       }
+               }
        },
 
-       _wrapCoords: function (coords) {
-               var newCoords = new L.Point(
-                       this._wrapX ? L.Util.wrapNum(coords.x, this._wrapX) : coords.x,
-                       this._wrapY ? L.Util.wrapNum(coords.y, this._wrapY) : coords.y);
-               newCoords.z = coords.z;
-               return newCoords;
+       _removeTilesAtZoom: function (zoom) {
+               for (var key in this._tiles) {
+                       if (this._tiles[key].coords.z !== zoom) {
+                               continue;
+                       }
+                       this._removeTile(key);
+               }
        },
 
-       _pxBoundsToTileRange: function (bounds) {
-               var tileSize = this.getTileSize();
-               return new L.Bounds(
-                       bounds.min.unscaleBy(tileSize).floor(),
-                       bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
+       _removeAllTiles: function () {
+               for (var key in this._tiles) {
+                       this._removeTile(key);
+               }
        },
 
-       _noTilesToLoad: function () {
-               for (var key in this._tiles) {
-                       if (!this._tiles[key].loaded) { return false; }
+       _invalidateAll: function () {
+               for (var z in this._levels) {
+                       L.DomUtil.remove(this._levels[z].el);
+                       delete this._levels[z];
                }
-               return true;
-       }
-});
+               this._removeAllTiles();
 
-// @factory L.gridLayer(options?: GridLayer options)
-// Creates a new instance of GridLayer with the supplied options.
-L.gridLayer = function (options) {
-       return new L.GridLayer(options);
-};
+               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 L.Point(+x2, +y2);
+               coords2.z = +z2;
 
+               var key = this._tileCoordsToKey(coords2),
+                   tile = this._tiles[key];
 
-/*
- * @class TileLayer
- * @inherits GridLayer
- * @aka L.TileLayer
- * Used to load and display tile layers on the map. Extends `GridLayer`.
- *
- * @example
- *
- * ```js
- * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar'}).addTo(map);
- * ```
- *
- * @section URL template
- * @example
- *
- * A string of the following form:
- *
- * ```
- * 'http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
- * ```
- *
- * `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add @2x to the URL to load retina tiles.
- *
- * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
- *
- * ```
- * L.tileLayer('http://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
- * ```
- */
+               if (tile && tile.active) {
+                       tile.retain = true;
+                       return true;
 
+               } else if (tile && tile.loaded) {
+                       tile.retain = true;
+               }
 
-L.TileLayer = L.GridLayer.extend({
+               if (z2 > minZoom) {
+                       return this._retainParent(x2, y2, z2, minZoom);
+               }
 
-       // @section
-       // @aka TileLayer options
-       options: {
-               // @option minZoom: Number = 0
-               // Minimum zoom number.
-               minZoom: 0,
+               return false;
+       },
 
-               // @option maxZoom: Number = 18
-               // Maximum zoom number.
-               maxZoom: 18,
+       _retainChildren: function (x, y, z, maxZoom) {
 
-               // @option maxNativeZoom: Number = null
-               // 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: null,
+               for (var i = 2 * x; i < 2 * x + 2; i++) {
+                       for (var j = 2 * y; j < 2 * y + 2; j++) {
 
-               // @option subdomains: String|String[] = 'abc'
-               // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings.
-               subdomains: 'abc',
+                               var coords = new L.Point(i, j);
+                               coords.z = z + 1;
 
-               // @option errorTileUrl: String = ''
-               // URL to the tile image to show in place of the tile that failed to load.
-               errorTileUrl: '',
+                               var key = this._tileCoordsToKey(coords),
+                                   tile = this._tiles[key];
 
-               // @option zoomOffset: Number = 0
-               // The zoom number used in tile URLs will be offset with this value.
-               zoomOffset: 0,
+                               if (tile && tile.active) {
+                                       tile.retain = true;
+                                       continue;
 
-               // @option tms: Boolean = false
-               // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
-               tms: false,
+                               } else if (tile && tile.loaded) {
+                                       tile.retain = true;
+                               }
 
-               // @option zoomReverse: Boolean = false
-               // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
-               zoomReverse: false,
+                               if (z + 1 < maxZoom) {
+                                       this._retainChildren(i, j, z + 1, maxZoom);
+                               }
+                       }
+               }
+       },
 
-               // @option detectRetina: Boolean = false
-               // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.
-               detectRetina: false,
+       _resetView: function (e) {
+               var animating = e && (e.pinch || e.flyTo);
+               this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
+       },
 
-               // @option crossOrigin: Boolean = false
-               // If true, all tiles will have their crossOrigin attribute set to ''. This is needed if you want to access tile pixel data.
-               crossOrigin: false
+       _animateZoom: function (e) {
+               this._setView(e.center, e.zoom, true, e.noUpdate);
        },
 
-       initialize: function (url, options) {
+       _setView: function (center, zoom, noPrune, noUpdate) {
+               var tileZoom = Math.round(zoom);
+               if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
+                   (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
+                       tileZoom = undefined;
+               }
 
-               this._url = url;
+               var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);
 
-               options = L.setOptions(this, options);
+               if (!noUpdate || tileZoomChanged) {
 
-               // detecting retina displays, adjusting tileSize and zoom levels
-               if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
+                       this._tileZoom = tileZoom;
 
-                       options.tileSize = Math.floor(options.tileSize / 2);
+                       if (this._abortLoading) {
+                               this._abortLoading();
+                       }
 
-                       if (!options.zoomReverse) {
-                               options.zoomOffset++;
-                               options.maxZoom--;
-                       } else {
-                               options.zoomOffset--;
-                               options.minZoom++;
+                       this._updateLevels();
+                       this._resetGrid();
+
+                       if (tileZoom !== undefined) {
+                               this._update(center);
                        }
 
-                       options.minZoom = Math.max(0, options.minZoom);
-               }
+                       if (!noPrune) {
+                               this._pruneTiles();
+                       }
 
-               if (typeof options.subdomains === 'string') {
-                       options.subdomains = options.subdomains.split('');
+                       // Flag to prevent _updateOpacity from pruning tiles during
+                       // a zoom anim or a pinch gesture
+                       this._noPrune = !!noPrune;
                }
 
-               // for https://github.com/Leaflet/Leaflet/issues/137
-               if (!L.Browser.android) {
-                       this.on('tileunload', this._onTileRemove);
-               }
+               this._setZoomTransforms(center, zoom);
        },
 
-       // @method setUrl(url: String, noRedraw?: Boolean): this
-       // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
-       setUrl: function (url, noRedraw) {
-               this._url = url;
-
-               if (!noRedraw) {
-                       this.redraw();
+       _setZoomTransforms: function (center, zoom) {
+               for (var i in this._levels) {
+                       this._setZoomTransform(this._levels[i], center, zoom);
                }
-               return this;
        },
 
-       // @method createTile(coords: Object, done?: Function): HTMLElement
-       // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
-       // to return an `<img>` HTML element with the appropiate image URL given `coords`. The `done`
-       // callback is called when the tile has been loaded.
-       createTile: function (coords, done) {
-               var tile = document.createElement('img');
-
-               L.DomEvent.on(tile, 'load', L.bind(this._tileOnLoad, this, done, tile));
-               L.DomEvent.on(tile, 'error', L.bind(this._tileOnError, this, done, tile));
+       _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 (this.options.crossOrigin) {
-                       tile.crossOrigin = '';
+               if (L.Browser.any3d) {
+                       L.DomUtil.setTransform(level.el, translate, scale);
+               } else {
+                       L.DomUtil.setPosition(level.el, translate);
                }
-
-               /*
-                Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons
-                http://www.w3.org/TR/WCAG20-TECHS/H67
-               */
-               tile.alt = '';
-
-               tile.src = this.getTileUrl(coords);
-
-               return tile;
        },
 
-       // @section Extension methods
-       // @uninheritable
-       // Layers extending `TileLayer` might reimplement the following method.
-       // @method getTileUrl(coords: Object): String
-       // Called only internally, returns the URL for a tile given its coordinates.
-       // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
-       getTileUrl: function (coords) {
-               var data = {
-                       r: L.Browser.retina ? '@2x' : '',
-                       s: this._getSubdomain(coords),
-                       x: coords.x,
-                       y: coords.y,
-                       z: this._getZoomForUrl()
-               };
-               if (this._map && !this._map.options.crs.infinite) {
-                       var invertedY = this._globalTileRange.max.y - coords.y;
-                       if (this.options.tms) {
-                               data['y'] = invertedY;
-                       }
-                       data['-y'] = invertedY;
+       _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);
                }
 
-               return L.Util.template(this._url, L.extend(data, this.options));
+               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)
+               ];
        },
 
-       _tileOnLoad: function (done, tile) {
-               // For https://github.com/Leaflet/Leaflet/issues/3332
-               if (L.Browser.ielt9) {
-                       setTimeout(L.bind(done, this, null, tile), 0);
-               } else {
-                       done(null, tile);
-               }
-       },
+       _onMoveEnd: function () {
+               if (!this._map || this._map._animatingZoom) { return; }
 
-       _tileOnError: function (done, tile, e) {
-               var errorUrl = this.options.errorTileUrl;
-               if (errorUrl) {
-                       tile.src = errorUrl;
-               }
-               done(e, tile);
+               this._update();
        },
 
-       getTileSize: function () {
+       _getTiledPixelBounds: function (center) {
                var map = this._map,
-                   tileSize = L.GridLayer.prototype.getTileSize.call(this),
-                   zoom = this._tileZoom + this.options.zoomOffset,
-                   zoomN = this.options.maxNativeZoom;
+                   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);
 
-               // increase tile size when overscaling
-               return zoomN !== null && zoom > zoomN ?
-                               tileSize.divideBy(map.getZoomScale(zoomN, zoom)).round() :
-                               tileSize;
+               return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
        },
 
-       _onTileRemove: function (e) {
-               e.tile.onload = null;
-       },
+       // 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 = map.getZoom();
 
-       _getZoomForUrl: function () {
+               if (center === undefined) { center = map.getCenter(); }
+               if (this._tileZoom === undefined) { return; }   // if out of minzoom/maxzoom
 
-               var options = this.options,
-                   zoom = this._tileZoom;
+               var pixelBounds = this._getTiledPixelBounds(center),
+                   tileRange = this._pxBoundsToTileRange(pixelBounds),
+                   tileCenter = tileRange.getCenter(),
+                   queue = [],
+                   margin = this.options.keepBuffer,
+                   noPruneRange = new L.Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
+                                             tileRange.getTopRight().add([margin, -margin]));
 
-               if (options.zoomReverse) {
-                       zoom = options.maxZoom - zoom;
+               for (var key in this._tiles) {
+                       var c = this._tiles[key].coords;
+                       if (c.z !== this._tileZoom || !noPruneRange.contains(L.point(c.x, c.y))) {
+                               this._tiles[key].current = false;
+                       }
                }
 
-               zoom += options.zoomOffset;
-
-               return options.maxNativeZoom !== null ? Math.min(zoom, options.maxNativeZoom) : zoom;
-       },
-
-       _getSubdomain: function (tilePoint) {
-               var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
-               return this.options.subdomains[index];
-       },
+               // _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; }
 
-       // stops loading all tiles in the background layer
-       _abortLoading: function () {
-               var i, tile;
-               for (i in this._tiles) {
-                       if (this._tiles[i].coords.z !== this._tileZoom) {
-                               tile = this._tiles[i].el;
+               // 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 L.Point(i, j);
+                               coords.z = this._tileZoom;
 
-                               tile.onload = L.Util.falseFn;
-                               tile.onerror = L.Util.falseFn;
+                               if (!this._isValidTile(coords)) { continue; }
 
-                               if (!tile.complete) {
-                                       tile.src = L.Util.emptyImageUrl;
-                                       L.DomUtil.remove(tile);
+                               var tile = this._tiles[this._tileCoordsToKey(coords)];
+                               if (tile) {
+                                       tile.current = true;
+                               } else {
+                                       queue.push(coords);
                                }
                        }
                }
-       }
-});
-
-
-// @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
-// Instantiates a tile layer object given a `URL template` and optionally an options object.
-
-L.tileLayer = function (url, options) {
-       return new L.TileLayer(url, options);
-};
 
+               // 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');
+                       }
 
-/*
- * @class TileLayer.WMS
- * @inherits TileLayer
- * @aka L.TileLayer.WMS
- * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
- *
- * @example
- *
- * ```js
- * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
- *     layers: 'nexrad-n0r-900913',
- *     format: 'image/png',
- *     transparent: true,
- *     attribution: "Weather data © 2012 IEM Nexrad"
- * });
- * ```
- */
+                       // create DOM fragment to append tiles in one batch
+                       var fragment = document.createDocumentFragment();
 
-L.TileLayer.WMS = L.TileLayer.extend({
+                       for (i = 0; i < queue.length; i++) {
+                               this._addTile(queue[i], fragment);
+                       }
 
-       // @section
-       // @aka TileLayer.WMS options
-       // If any custom options not documented here are used, they will be sent to the
-       // WMS server as extra parameters in each request URL. This can be useful for
-       // [non-standard vendor WMS parameters](http://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
-       defaultWmsParams: {
-               service: 'WMS',
-               request: 'GetMap',
+                       this._level.el.appendChild(fragment);
+               }
+       },
 
-               // @option layers: String = ''
-               // **(required)** Comma-separated list of WMS layers to show.
-               layers: '',
+       _isValidTile: function (coords) {
+               var crs = this._map.options.crs;
 
-               // @option styles: String = ''
-               // Comma-separated list of WMS styles.
-               styles: '',
+               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; }
+               }
 
-               // @option format: String = 'image/jpeg'
-               // WMS image format (use `'image/png'` for layers with transparency).
-               format: 'image/jpeg',
+               if (!this.options.bounds) { return true; }
 
-               // @option transparent: Boolean = false
-               // If `true`, the WMS service will return images with transparency.
-               transparent: false,
+               // don't load tile if it doesn't intersect the bounds in options
+               var tileBounds = this._tileCoordsToBounds(coords);
+               return L.latLngBounds(this.options.bounds).overlaps(tileBounds);
+       },
 
-               // @option version: String = '1.1.1'
-               // Version of the WMS service to use
-               version: '1.1.1'
+       _keyToBounds: function (key) {
+               return this._tileCoordsToBounds(this._keyToTileCoords(key));
        },
 
-       options: {
-               // @option crs: CRS = null
-               // Coordinate Reference System to use for the WMS requests, defaults to
-               // map CRS. Don't change this if you're not sure what it means.
-               crs: null,
+       // converts tile coordinates to its geographical bounds
+       _tileCoordsToBounds: function (coords) {
 
-               // @option uppercase: Boolean = false
-               // If `true`, WMS request parameter keys will be uppercase.
-               uppercase: false
-       },
+               var map = this._map,
+                   tileSize = this.getTileSize(),
 
-       initialize: function (url, options) {
+                   nwPoint = coords.scaleBy(tileSize),
+                   sePoint = nwPoint.add(tileSize),
 
-               this._url = url;
+                   nw = map.unproject(nwPoint, coords.z),
+                   se = map.unproject(sePoint, coords.z);
 
-               var wmsParams = L.extend({}, this.defaultWmsParams);
+               if (!this.options.noWrap) {
+                       nw = map.wrapLatLng(nw);
+                       se = map.wrapLatLng(se);
+               }
 
-               // all keys that are not TileLayer options go to WMS params
-               for (var i in options) {
-                       if (!(i in this.options)) {
-                               wmsParams[i] = options[i];
-                       }
-               }
+               return new L.LatLngBounds(nw, se);
+       },
 
-               options = L.setOptions(this, options);
+       // converts tile coordinates to key for the tile cache
+       _tileCoordsToKey: function (coords) {
+               return coords.x + ':' + coords.y + ':' + coords.z;
+       },
 
-               wmsParams.width = wmsParams.height = options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1);
+       // converts tile cache key to coordinates
+       _keyToTileCoords: function (key) {
+               var k = key.split(':'),
+                   coords = new L.Point(+k[0], +k[1]);
+               coords.z = +k[2];
+               return coords;
+       },
 
-               this.wmsParams = wmsParams;
+       _removeTile: function (key) {
+               var tile = this._tiles[key];
+               if (!tile) { return; }
+
+               L.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)
+               });
        },
 
-       onAdd: function (map) {
+       _initTile: function (tile) {
+               L.DomUtil.addClass(tile, 'leaflet-tile');
 
-               this._crs = this.options.crs || map.options.crs;
-               this._wmsVersion = parseFloat(this.wmsParams.version);
+               var tileSize = this.getTileSize();
+               tile.style.width = tileSize.x + 'px';
+               tile.style.height = tileSize.y + 'px';
 
-               var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
-               this.wmsParams[projectionKey] = this._crs.code;
+               tile.onselectstart = L.Util.falseFn;
+               tile.onmousemove = L.Util.falseFn;
 
-               L.TileLayer.prototype.onAdd.call(this, map);
+               // update opacity on tiles in IE7-8 because of filter inheritance problems
+               if (L.Browser.ielt9 && this.options.opacity < 1) {
+                       L.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 (L.Browser.android && !L.Browser.android23) {
+                       tile.style.WebkitBackfaceVisibility = 'hidden';
+               }
        },
 
-       getTileUrl: function (coords) {
+       _addTile: function (coords, container) {
+               var tilePos = this._getTilePos(coords),
+                   key = this._tileCoordsToKey(coords);
 
-               var tileBounds = this._tileCoordsToBounds(coords),
-                   nw = this._crs.project(tileBounds.getNorthWest()),
-                   se = this._crs.project(tileBounds.getSouthEast()),
+               var tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords));
 
-                   bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ?
-                           [se.y, nw.x, nw.y, se.x] :
-                           [nw.x, se.y, se.x, nw.y]).join(','),
+               this._initTile(tile);
 
-                   url = L.TileLayer.prototype.getTileUrl.call(this, coords);
+               // 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
+                       L.Util.requestAnimFrame(L.bind(this._tileReady, this, coords, null, tile));
+               }
 
-               return url +
-                       L.Util.getParamString(this.wmsParams, url, this.options.uppercase) +
-                       (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
+               L.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
+               });
        },
 
-       // @method setParams(params: Object, noRedraw?: Boolean): this
-       // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
-       setParams: function (params, noRedraw) {
+       _tileReady: function (coords, err, tile) {
+               if (!this._map) { return; }
 
-               L.extend(this.wmsParams, params);
+               if (err) {
+                       // @event tileerror: TileErrorEvent
+                       // Fired when there is an error loading a tile.
+                       this.fire('tileerror', {
+                               error: err,
+                               tile: tile,
+                               coords: coords
+                       });
+               }
 
-               if (!noRedraw) {
-                       this.redraw();
+               var key = this._tileCoordsToKey(coords);
+
+               tile = this._tiles[key];
+               if (!tile) { return; }
+
+               tile.loaded = +new Date();
+               if (this._map._fadeAnimated) {
+                       L.DomUtil.setOpacity(tile.el, 0);
+                       L.Util.cancelAnimFrame(this._fadeFrame);
+                       this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
+               } else {
+                       tile.active = true;
+                       this._pruneTiles();
                }
 
-               return this;
+               if (!err) {
+                       L.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 (L.Browser.ielt9 || !this._map._fadeAnimated) {
+                               L.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(L.bind(this._pruneTiles, this), 250);
+                       }
+               }
+       },
+
+       _getTilePos: function (coords) {
+               return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
+       },
+
+       _wrapCoords: function (coords) {
+               var newCoords = new L.Point(
+                       this._wrapX ? L.Util.wrapNum(coords.x, this._wrapX) : coords.x,
+                       this._wrapY ? L.Util.wrapNum(coords.y, this._wrapY) : coords.y);
+               newCoords.z = coords.z;
+               return newCoords;
+       },
+
+       _pxBoundsToTileRange: function (bounds) {
+               var tileSize = this.getTileSize();
+               return new L.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.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
-// Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
-L.tileLayer.wms = function (url, options) {
-       return new L.TileLayer.WMS(url, options);
+// @factory L.gridLayer(options?: GridLayer options)
+// Creates a new instance of GridLayer with the supplied options.
+L.gridLayer = function (options) {
+       return new L.GridLayer(options);
 };
 
 
 
 /*
- * @class ImageOverlay
- * @aka L.ImageOverlay
- * @inherits Interactive layer
- *
- * Used to load and display a single image over specific bounds of the map. Extends `Layer`.
+ * @class TileLayer
+ * @inherits GridLayer
+ * @aka L.TileLayer
+ * Used to load and display tile layers on the map. Extends `GridLayer`.
  *
  * @example
  *
  * ```js
- * var imageUrl = 'http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
- *     imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
- * L.imageOverlay(imageUrl, imageBounds).addTo(map);
+ * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar'}).addTo(map);
+ * ```
+ *
+ * @section URL template
+ * @example
+ *
+ * A string of the following form:
+ *
+ * ```
+ * 'http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
+ * ```
+ *
+ * `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add @2x to the URL to load retina tiles.
+ *
+ * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
+ *
+ * ```
+ * L.tileLayer('http://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
  * ```
  */
 
-L.ImageOverlay = L.Layer.extend({
+
+L.TileLayer = L.GridLayer.extend({
 
        // @section
-       // @aka ImageOverlay options
+       // @aka TileLayer options
        options: {
-               // @option opacity: Number = 1.0
-               // The opacity of the image overlay.
-               opacity: 1,
-
-               // @option alt: String = ''
-               // Text for the `alt` attribute of the image (useful for accessibility).
-               alt: '',
+               // @option minZoom: Number = 0
+               // Minimum zoom number.
+               minZoom: 0,
 
-               // @option interactive: Boolean = false
-               // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
-               interactive: false,
+               // @option maxZoom: Number = 18
+               // Maximum zoom number.
+               maxZoom: 18,
 
-               // @option attribution: String = null
-               // An optional string containing HTML to be shown on the `Attribution control`
-               attribution: null,
+               // @option maxNativeZoom: Number = null
+               // 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: null,
 
-               // @option crossOrigin: Boolean = false
-               // If true, the image will have its crossOrigin attribute set to ''. This is needed if you want to access image pixel data.
-               crossOrigin: false
-       },
+               // @option minNativeZoom: Number = null
+               // 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: null,
 
-       initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
-               this._url = url;
-               this._bounds = L.latLngBounds(bounds);
+               // @option subdomains: String|String[] = 'abc'
+               // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings.
+               subdomains: 'abc',
 
-               L.setOptions(this, options);
+               // @option errorTileUrl: String = ''
+               // URL to the tile image to show in place of the tile that failed to load.
+               errorTileUrl: '',
+
+               // @option zoomOffset: Number = 0
+               // The zoom number used in tile URLs will be offset with this value.
+               zoomOffset: 0,
+
+               // @option tms: Boolean = false
+               // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
+               tms: false,
+
+               // @option zoomReverse: Boolean = false
+               // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
+               zoomReverse: false,
+
+               // @option detectRetina: Boolean = false
+               // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.
+               detectRetina: false,
+
+               // @option crossOrigin: Boolean = false
+               // If true, all tiles will have their crossOrigin attribute set to ''. This is needed if you want to access tile pixel data.
+               crossOrigin: false
        },
 
-       onAdd: function () {
-               if (!this._image) {
-                       this._initImage();
+       initialize: function (url, options) {
 
-                       if (this.options.opacity < 1) {
-                               this._updateOpacity();
+               this._url = url;
+
+               options = L.setOptions(this, options);
+
+               // detecting retina displays, adjusting tileSize and zoom levels
+               if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
+
+                       options.tileSize = Math.floor(options.tileSize / 2);
+
+                       if (!options.zoomReverse) {
+                               options.zoomOffset++;
+                               options.maxZoom--;
+                       } else {
+                               options.zoomOffset--;
+                               options.minZoom++;
                        }
-               }
 
-               if (this.options.interactive) {
-                       L.DomUtil.addClass(this._image, 'leaflet-interactive');
-                       this.addInteractiveTarget(this._image);
+                       options.minZoom = Math.max(0, options.minZoom);
                }
 
-               this.getPane().appendChild(this._image);
-               this._reset();
-       },
+               if (typeof options.subdomains === 'string') {
+                       options.subdomains = options.subdomains.split('');
+               }
 
-       onRemove: function () {
-               L.DomUtil.remove(this._image);
-               if (this.options.interactive) {
-                       this.removeInteractiveTarget(this._image);
+               // for https://github.com/Leaflet/Leaflet/issues/137
+               if (!L.Browser.android) {
+                       this.on('tileunload', this._onTileRemove);
                }
        },
 
-       // @method setOpacity(opacity: Number): this
-       // Sets the opacity of the overlay.
-       setOpacity: function (opacity) {
-               this.options.opacity = opacity;
+       // @method setUrl(url: String, noRedraw?: Boolean): this
+       // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
+       setUrl: function (url, noRedraw) {
+               this._url = url;
 
-               if (this._image) {
-                       this._updateOpacity();
+               if (!noRedraw) {
+                       this.redraw();
                }
                return this;
        },
 
-       setStyle: function (styleOpts) {
-               if (styleOpts.opacity) {
-                       this.setOpacity(styleOpts.opacity);
-               }
-               return this;
-       },
+       // @method createTile(coords: Object, done?: Function): HTMLElement
+       // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
+       // to return an `<img>` HTML element with the appropiate image URL given `coords`. The `done`
+       // callback is called when the tile has been loaded.
+       createTile: function (coords, done) {
+               var tile = document.createElement('img');
 
-       // @method bringToFront(): this
-       // Brings the layer to the top of all overlays.
-       bringToFront: function () {
-               if (this._map) {
-                       L.DomUtil.toFront(this._image);
+               L.DomEvent.on(tile, 'load', L.bind(this._tileOnLoad, this, done, tile));
+               L.DomEvent.on(tile, 'error', L.bind(this._tileOnError, this, done, tile));
+
+               if (this.options.crossOrigin) {
+                       tile.crossOrigin = '';
                }
-               return this;
+
+               /*
+                Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons
+                http://www.w3.org/TR/WCAG20-TECHS/H67
+               */
+               tile.alt = '';
+
+               /*
+                Set role="presentation" to force screen readers to ignore this
+                https://www.w3.org/TR/wai-aria/roles#textalternativecomputation
+               */
+               tile.setAttribute('role', 'presentation');
+
+               tile.src = this.getTileUrl(coords);
+
+               return tile;
        },
 
-       // @method bringToBack(): this
-       // Brings the layer to the bottom of all overlays.
-       bringToBack: function () {
-               if (this._map) {
-                       L.DomUtil.toBack(this._image);
+       // @section Extension methods
+       // @uninheritable
+       // Layers extending `TileLayer` might reimplement the following method.
+       // @method getTileUrl(coords: Object): String
+       // Called only internally, returns the URL for a tile given its coordinates.
+       // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
+       getTileUrl: function (coords) {
+               var data = {
+                       r: L.Browser.retina ? '@2x' : '',
+                       s: this._getSubdomain(coords),
+                       x: coords.x,
+                       y: coords.y,
+                       z: this._getZoomForUrl()
+               };
+               if (this._map && !this._map.options.crs.infinite) {
+                       var invertedY = this._globalTileRange.max.y - coords.y;
+                       if (this.options.tms) {
+                               data['y'] = invertedY;
+                       }
+                       data['-y'] = invertedY;
                }
-               return this;
-       },
 
-       // @method setUrl(url: String): this
-       // Changes the URL of the image.
-       setUrl: function (url) {
-               this._url = url;
+               return L.Util.template(this._url, L.extend(data, this.options));
+       },
 
-               if (this._image) {
-                       this._image.src = url;
+       _tileOnLoad: function (done, tile) {
+               // For https://github.com/Leaflet/Leaflet/issues/3332
+               if (L.Browser.ielt9) {
+                       setTimeout(L.bind(done, this, null, tile), 0);
+               } else {
+                       done(null, tile);
                }
-               return this;
        },
 
-       setBounds: function (bounds) {
-               this._bounds = bounds;
-
-               if (this._map) {
-                       this._reset();
+       _tileOnError: function (done, tile, e) {
+               var errorUrl = this.options.errorTileUrl;
+               if (errorUrl) {
+                       tile.src = errorUrl;
                }
-               return this;
+               done(e, tile);
        },
 
-       getAttribution: function () {
-               return this.options.attribution;
-       },
+       getTileSize: function () {
+               var map = this._map,
+               tileSize = L.GridLayer.prototype.getTileSize.call(this),
+               zoom = this._tileZoom + this.options.zoomOffset,
+               minNativeZoom = this.options.minNativeZoom,
+               maxNativeZoom = this.options.maxNativeZoom;
 
-       getEvents: function () {
-               var events = {
-                       zoom: this._reset,
-                       viewreset: this._reset
-               };
+               // decrease tile size when scaling below minNativeZoom
+               if (minNativeZoom !== null && zoom < minNativeZoom) {
+                       return tileSize.divideBy(map.getZoomScale(minNativeZoom, zoom)).round();
+               }
 
-               if (this._zoomAnimated) {
-                       events.zoomanim = this._animateZoom;
+               // increase tile size when scaling above maxNativeZoom
+               if (maxNativeZoom !== null && zoom > maxNativeZoom) {
+                       return tileSize.divideBy(map.getZoomScale(maxNativeZoom, zoom)).round();
                }
 
-               return events;
+               return tileSize;
        },
 
-       getBounds: function () {
-               return this._bounds;
+       _onTileRemove: function (e) {
+               e.tile.onload = null;
        },
 
-       getElement: function () {
-               return this._image;
-       },
+       _getZoomForUrl: function () {
+               var zoom = this._tileZoom,
+               maxZoom = this.options.maxZoom,
+               zoomReverse = this.options.zoomReverse,
+               zoomOffset = this.options.zoomOffset,
+               minNativeZoom = this.options.minNativeZoom,
+               maxNativeZoom = this.options.maxNativeZoom;
 
-       _initImage: function () {
-               var img = this._image = L.DomUtil.create('img',
-                               'leaflet-image-layer ' + (this._zoomAnimated ? 'leaflet-zoom-animated' : ''));
+               if (zoomReverse) {
+                       zoom = maxZoom - zoom;
+               }
 
-               img.onselectstart = L.Util.falseFn;
-               img.onmousemove = L.Util.falseFn;
+               zoom += zoomOffset;
 
-               img.onload = L.bind(this.fire, this, 'load');
+               if (minNativeZoom !== null && zoom < minNativeZoom) {
+                       return minNativeZoom;
+               }
 
-               if (this.options.crossOrigin) {
-                       img.crossOrigin = '';
+               if (maxNativeZoom !== null && zoom > maxNativeZoom) {
+                       return maxNativeZoom;
                }
 
-               img.src = this._url;
-               img.alt = this.options.alt;
+               return zoom;
        },
 
-       _animateZoom: function (e) {
-               var scale = this._map.getZoomScale(e.zoom),
-                   offset = this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(), e.zoom, e.center);
-
-               L.DomUtil.setTransform(this._image, offset, scale);
+       _getSubdomain: function (tilePoint) {
+               var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
+               return this.options.subdomains[index];
        },
 
-       _reset: function () {
-               var image = this._image,
-                   bounds = new L.Bounds(
-                       this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
-                       this._map.latLngToLayerPoint(this._bounds.getSouthEast())),
-                   size = bounds.getSize();
-
-               L.DomUtil.setPosition(image, bounds.min);
+       // stops loading all tiles in the background layer
+       _abortLoading: function () {
+               var i, tile;
+               for (i in this._tiles) {
+                       if (this._tiles[i].coords.z !== this._tileZoom) {
+                               tile = this._tiles[i].el;
 
-               image.style.width  = size.x + 'px';
-               image.style.height = size.y + 'px';
-       },
+                               tile.onload = L.Util.falseFn;
+                               tile.onerror = L.Util.falseFn;
 
-       _updateOpacity: function () {
-               L.DomUtil.setOpacity(this._image, this.options.opacity);
+                               if (!tile.complete) {
+                                       tile.src = L.Util.emptyImageUrl;
+                                       L.DomUtil.remove(tile);
+                               }
+                       }
+               }
        }
 });
 
-// @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
-// Instantiates an image overlay object given the URL of the image and the
-// geographical bounds it is tied to.
-L.imageOverlay = function (url, bounds, options) {
-       return new L.ImageOverlay(url, bounds, options);
+
+// @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
+// Instantiates a tile layer object given a `URL template` and optionally an options object.
+
+L.tileLayer = function (url, options) {
+       return new L.TileLayer(url, options);
 };
 
 
 
 /*
- * @class Icon
- * @aka L.Icon
- * @inherits Layer
- *
- * Represents an icon to provide when creating a marker.
+ * @class TileLayer.WMS
+ * @inherits TileLayer
+ * @aka L.TileLayer.WMS
+ * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
  *
  * @example
  *
  * ```js
- * var myIcon = L.icon({
- *     iconUrl: 'my-icon.png',
- *     iconRetinaUrl: 'my-icon@2x.png',
- *     iconSize: [38, 95],
- *     iconAnchor: [22, 94],
- *     popupAnchor: [-3, -76],
- *     shadowUrl: 'my-icon-shadow.png',
- *     shadowRetinaUrl: 'my-icon-shadow@2x.png',
- *     shadowSize: [68, 95],
- *     shadowAnchor: [22, 94]
+ * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
+ *     layers: 'nexrad-n0r-900913',
+ *     format: 'image/png',
+ *     transparent: true,
+ *     attribution: "Weather data © 2012 IEM Nexrad"
  * });
- *
- * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
  * ```
- *
- * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
- *
  */
 
-L.Icon = L.Class.extend({
+L.TileLayer.WMS = L.TileLayer.extend({
 
-       /* @section
-        * @aka Icon options
-        *
-        * @option iconUrl: String = null
-        * **(required)** The URL to the icon image (absolute or relative to your script path).
-        *
-        * @option iconRetinaUrl: String = null
-        * The URL to a retina sized version of the icon image (absolute or relative to your
-        * script path). Used for Retina screen devices.
-        *
-        * @option iconSize: Point = null
-        * Size of the icon image in pixels.
-        *
-        * @option iconAnchor: Point = null
-        * The coordinates of the "tip" of the icon (relative to its top left corner). The icon
-        * will be aligned so that this point is at the marker's geographical location. Centered
-        * by default if size is specified, also can be set in CSS with negative margins.
-        *
-        * @option popupAnchor: Point = null
-        * The coordinates of the point from which popups will "open", relative to the icon anchor.
-        *
-        * @option shadowUrl: String = null
-        * The URL to the icon shadow image. If not specified, no shadow image will be created.
-        *
-        * @option shadowRetinaUrl: String = null
-        *
-        * @option shadowSize: Point = null
-        * Size of the shadow image in pixels.
-        *
-        * @option shadowAnchor: Point = null
-        * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
-        * as iconAnchor if not specified).
-        *
-        * @option className: String = ''
-        * A custom class name to assign to both icon and shadow images. Empty by default.
-        */
+       // @section
+       // @aka TileLayer.WMS options
+       // If any custom options not documented here are used, they will be sent to the
+       // WMS server as extra parameters in each request URL. This can be useful for
+       // [non-standard vendor WMS parameters](http://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
+       defaultWmsParams: {
+               service: 'WMS',
+               request: 'GetMap',
 
-       initialize: function (options) {
-               L.setOptions(this, options);
-       },
+               // @option layers: String = ''
+               // **(required)** Comma-separated list of WMS layers to show.
+               layers: '',
 
-       // @method createIcon(oldIcon?: HTMLElement): HTMLElement
-       // Called internally when the icon has to be shown, returns a `<img>` HTML element
-       // styled according to the options.
-       createIcon: function (oldIcon) {
-               return this._createIcon('icon', oldIcon);
-       },
+               // @option styles: String = ''
+               // Comma-separated list of WMS styles.
+               styles: '',
 
-       // @method createShadow(oldIcon?: HTMLElement): HTMLElement
-       // As `createIcon`, but for the shadow beneath it.
-       createShadow: function (oldIcon) {
-               return this._createIcon('shadow', oldIcon);
-       },
+               // @option format: String = 'image/jpeg'
+               // WMS image format (use `'image/png'` for layers with transparency).
+               format: 'image/jpeg',
 
-       _createIcon: function (name, oldIcon) {
-               var src = this._getIconUrl(name);
+               // @option transparent: Boolean = false
+               // If `true`, the WMS service will return images with transparency.
+               transparent: false,
 
-               if (!src) {
-                       if (name === 'icon') {
-                               throw new Error('iconUrl not set in Icon options (see the docs).');
-                       }
-                       return null;
-               }
+               // @option version: String = '1.1.1'
+               // Version of the WMS service to use
+               version: '1.1.1'
+       },
 
-               var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
-               this._setIconStyles(img, name);
+       options: {
+               // @option crs: CRS = null
+               // Coordinate Reference System to use for the WMS requests, defaults to
+               // map CRS. Don't change this if you're not sure what it means.
+               crs: null,
 
-               return img;
+               // @option uppercase: Boolean = false
+               // If `true`, WMS request parameter keys will be uppercase.
+               uppercase: false
        },
 
-       _setIconStyles: function (img, name) {
-               var options = this.options;
-               var sizeOption = options[name + 'Size'];
-
-               if (typeof sizeOption === 'number') {
-                       sizeOption = [sizeOption, sizeOption];
-               }
+       initialize: function (url, options) {
 
-               var size = L.point(sizeOption),
-                   anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
-                           size && size.divideBy(2, true));
+               this._url = url;
 
-               img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
+               var wmsParams = L.extend({}, this.defaultWmsParams);
 
-               if (anchor) {
-                       img.style.marginLeft = (-anchor.x) + 'px';
-                       img.style.marginTop  = (-anchor.y) + 'px';
+               // all keys that are not TileLayer options go to WMS params
+               for (var i in options) {
+                       if (!(i in this.options)) {
+                               wmsParams[i] = options[i];
+                       }
                }
 
-               if (size) {
-                       img.style.width  = size.x + 'px';
-                       img.style.height = size.y + 'px';
-               }
-       },
+               options = L.setOptions(this, options);
 
-       _createImg: function (src, el) {
-               el = el || document.createElement('img');
-               el.src = src;
-               return el;
-       },
+               wmsParams.width = wmsParams.height = options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1);
 
-       _getIconUrl: function (name) {
-               return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
-       }
-});
+               this.wmsParams = wmsParams;
+       },
 
+       onAdd: function (map) {
 
-// @factory L.icon(options: Icon options)
-// Creates an icon instance with the given options.
-L.icon = function (options) {
-       return new L.Icon(options);
-};
+               this._crs = this.options.crs || map.options.crs;
+               this._wmsVersion = parseFloat(this.wmsParams.version);
 
+               var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
+               this.wmsParams[projectionKey] = this._crs.code;
 
+               L.TileLayer.prototype.onAdd.call(this, map);
+       },
 
-/*
- * @miniclass Icon.Default (Icon)
- * @aka L.Icon.Default
- * @section
- *
- * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
- * no icon is specified. Points to the blue marker image distributed with Leaflet
- * releases.
- *
- * In order to change the default icon, just change the properties of `L.Icon.Default.prototype.options`
- * (which is a set of `Icon options`).
- */
+       getTileUrl: function (coords) {
 
-L.Icon.Default = L.Icon.extend({
+               var tileBounds = this._tileCoordsToBounds(coords),
+                   nw = this._crs.project(tileBounds.getNorthWest()),
+                   se = this._crs.project(tileBounds.getSouthEast()),
 
-       options: {
-               iconUrl:       'marker-icon.png',
-               iconRetinaUrl: 'marker-icon-2x.png',
-               shadowUrl:     'marker-shadow.png',
-               iconSize:    [25, 41],
-               iconAnchor:  [12, 41],
-               popupAnchor: [1, -34],
-               tooltipAnchor: [16, -28],
-               shadowSize:  [41, 41]
-       },
+                   bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ?
+                           [se.y, nw.x, nw.y, se.x] :
+                           [nw.x, se.y, se.x, nw.y]).join(','),
 
-       _getIconUrl: function (name) {
-               if (!L.Icon.Default.imagePath) {        // Deprecated, backwards-compatibility only
-                       L.Icon.Default.imagePath = this._detectIconPath();
-               }
+                   url = L.TileLayer.prototype.getTileUrl.call(this, coords);
 
-               // @option imagePath: String
-               // `L.Icon.Default` will try to auto-detect the absolute location of the
-               // blue icon images. If you are placing these images in a non-standard
-               // way, set this option to point to the right absolute path.
-               return (this.options.imagePath || L.Icon.Default.imagePath) + L.Icon.prototype._getIconUrl.call(this, name);
+               return url +
+                       L.Util.getParamString(this.wmsParams, url, this.options.uppercase) +
+                       (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
        },
 
-       _detectIconPath: function () {
-               var el = L.DomUtil.create('div',  'leaflet-default-icon-path', document.body);
-               var path = L.DomUtil.getStyle(el, 'background-image') ||
-                          L.DomUtil.getStyle(el, 'backgroundImage');   // IE8
+       // @method setParams(params: Object, noRedraw?: Boolean): this
+       // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
+       setParams: function (params, noRedraw) {
 
-               document.body.removeChild(el);
+               L.extend(this.wmsParams, params);
 
-               return path.indexOf('url') === 0 ?
-                       path.replace(/^url\([\"\']?/, '').replace(/marker-icon\.png[\"\']?\)$/, '') : '';
+               if (!noRedraw) {
+                       this.redraw();
+               }
+
+               return this;
        }
 });
 
 
+// @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
+// Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
+L.tileLayer.wms = function (url, options) {
+       return new L.TileLayer.WMS(url, options);
+};
+
+
 
 /*
- * @class Marker
+ * @class ImageOverlay
+ * @aka L.ImageOverlay
  * @inherits Interactive layer
- * @aka L.Marker
- * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
+ *
+ * Used to load and display a single image over specific bounds of the map. Extends `Layer`.
  *
  * @example
  *
  * ```js
- * L.marker([50.5, 30.5]).addTo(map);
+ * var imageUrl = 'http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
+ *     imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
+ * L.imageOverlay(imageUrl, imageBounds).addTo(map);
  * ```
  */
 
-L.Marker = L.Layer.extend({
+L.ImageOverlay = L.Layer.extend({
 
        // @section
-       // @aka Marker options
+       // @aka ImageOverlay options
        options: {
-               // @option icon: Icon = *
-               // Icon class to use for rendering the marker. See [Icon documentation](#L.Icon) for details on how to customize the marker icon. If not specified, a new `L.Icon.Default` is used.
-               icon: new L.Icon.Default(),
+               // @option opacity: Number = 1.0
+               // The opacity of the image overlay.
+               opacity: 1,
 
-               // Option inherited from "Interactive layer" abstract class
-               interactive: true,
+               // @option alt: String = ''
+               // Text for the `alt` attribute of the image (useful for accessibility).
+               alt: '',
 
-               // @option draggable: Boolean = false
-               // Whether the marker is draggable with mouse/touch or not.
-               draggable: false,
-
-               // @option keyboard: Boolean = true
-               // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
-               keyboard: true,
-
-               // @option title: String = ''
-               // Text for the browser tooltip that appear on marker hover (no tooltip by default).
-               title: '',
-
-               // @option alt: String = ''
-               // Text for the `alt` attribute of the icon image (useful for accessibility).
-               alt: '',
-
-               // @option zIndexOffset: Number = 0
-               // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
-               zIndexOffset: 0,
-
-               // @option opacity: Number = 1.0
-               // The opacity of the marker.
-               opacity: 1,
-
-               // @option riseOnHover: Boolean = false
-               // If `true`, the marker will get on top of others when you hover the mouse over it.
-               riseOnHover: false,
-
-               // @option riseOffset: Number = 250
-               // The z-index offset used for the `riseOnHover` feature.
-               riseOffset: 250,
-
-               // @option pane: String = 'markerPane'
-               // `Map pane` where the markers icon will be added.
-               pane: 'markerPane',
+               // @option interactive: Boolean = false
+               // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
+               interactive: false,
 
-               // FIXME: shadowPane is no longer a valid option
-               nonBubblingEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu']
+               // @option crossOrigin: Boolean = false
+               // If true, the image will have its crossOrigin attribute set to ''. This is needed if you want to access image pixel data.
+               crossOrigin: false
        },
 
-       /* @section
-        *
-        * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
-        */
+       initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
+               this._url = url;
+               this._bounds = L.latLngBounds(bounds);
 
-       initialize: function (latlng, options) {
                L.setOptions(this, options);
-               this._latlng = L.latLng(latlng);
        },
 
-       onAdd: function (map) {
-               this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
-
-               if (this._zoomAnimated) {
-                       map.on('zoomanim', this._animateZoom, this);
-               }
-
-               this._initIcon();
-               this.update();
-       },
+       onAdd: function () {
+               if (!this._image) {
+                       this._initImage();
 
-       onRemove: function (map) {
-               if (this.dragging && this.dragging.enabled()) {
-                       this.options.draggable = true;
-                       this.dragging.removeHooks();
+                       if (this.options.opacity < 1) {
+                               this._updateOpacity();
+                       }
                }
 
-               if (this._zoomAnimated) {
-                       map.off('zoomanim', this._animateZoom, this);
+               if (this.options.interactive) {
+                       L.DomUtil.addClass(this._image, 'leaflet-interactive');
+                       this.addInteractiveTarget(this._image);
                }
 
-               this._removeIcon();
-               this._removeShadow();
-       },
-
-       getEvents: function () {
-               return {
-                       zoom: this.update,
-                       viewreset: this.update
-               };
+               this.getPane().appendChild(this._image);
+               this._reset();
        },
 
-       // @method getLatLng: LatLng
-       // Returns the current geographical position of the marker.
-       getLatLng: function () {
-               return this._latlng;
+       onRemove: function () {
+               L.DomUtil.remove(this._image);
+               if (this.options.interactive) {
+                       this.removeInteractiveTarget(this._image);
+               }
        },
 
-       // @method setLatLng(latlng: LatLng): this
-       // Changes the marker position to the given point.
-       setLatLng: function (latlng) {
-               var oldLatLng = this._latlng;
-               this._latlng = L.latLng(latlng);
-               this.update();
+       // @method setOpacity(opacity: Number): this
+       // Sets the opacity of the overlay.
+       setOpacity: function (opacity) {
+               this.options.opacity = opacity;
 
-               // @event move: Event
-               // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
-               return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
+               if (this._image) {
+                       this._updateOpacity();
+               }
+               return this;
        },
 
-       // @method setZIndexOffset(offset: Number): this
-       // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
-       setZIndexOffset: function (offset) {
-               this.options.zIndexOffset = offset;
-               return this.update();
+       setStyle: function (styleOpts) {
+               if (styleOpts.opacity) {
+                       this.setOpacity(styleOpts.opacity);
+               }
+               return this;
        },
 
-       // @method setIcon(icon: Icon): this
-       // Changes the marker icon.
-       setIcon: function (icon) {
-
-               this.options.icon = icon;
-
+       // @method bringToFront(): this
+       // Brings the layer to the top of all overlays.
+       bringToFront: function () {
                if (this._map) {
-                       this._initIcon();
-                       this.update();
-               }
-
-               if (this._popup) {
-                       this.bindPopup(this._popup, this._popup.options);
+                       L.DomUtil.toFront(this._image);
                }
-
                return this;
        },
 
-       getElement: function () {
-               return this._icon;
+       // @method bringToBack(): this
+       // Brings the layer to the bottom of all overlays.
+       bringToBack: function () {
+               if (this._map) {
+                       L.DomUtil.toBack(this._image);
+               }
+               return this;
        },
 
-       update: function () {
+       // @method setUrl(url: String): this
+       // Changes the URL of the image.
+       setUrl: function (url) {
+               this._url = url;
 
-               if (this._icon) {
-                       var pos = this._map.latLngToLayerPoint(this._latlng).round();
-                       this._setPos(pos);
+               if (this._image) {
+                       this._image.src = url;
                }
-
                return this;
        },
 
-       _initIcon: function () {
-               var options = this.options,
-                   classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
-
-               var icon = options.icon.createIcon(this._icon),
-                   addIcon = false;
-
-               // if we're not reusing the icon, remove the old one and init new one
-               if (icon !== this._icon) {
-                       if (this._icon) {
-                               this._removeIcon();
-                       }
-                       addIcon = true;
+       setBounds: function (bounds) {
+               this._bounds = bounds;
 
-                       if (options.title) {
-                               icon.title = options.title;
-                       }
-                       if (options.alt) {
-                               icon.alt = options.alt;
-                       }
+               if (this._map) {
+                       this._reset();
                }
+               return this;
+       },
 
-               L.DomUtil.addClass(icon, classToAdd);
+       getEvents: function () {
+               var events = {
+                       zoom: this._reset,
+                       viewreset: this._reset
+               };
 
-               if (options.keyboard) {
-                       icon.tabIndex = '0';
+               if (this._zoomAnimated) {
+                       events.zoomanim = this._animateZoom;
                }
 
-               this._icon = icon;
+               return events;
+       },
 
-               if (options.riseOnHover) {
-                       this.on({
-                               mouseover: this._bringToFront,
-                               mouseout: this._resetZIndex
-                       });
-               }
+       getBounds: function () {
+               return this._bounds;
+       },
 
-               var newShadow = options.icon.createShadow(this._shadow),
-                   addShadow = false;
+       getElement: function () {
+               return this._image;
+       },
 
-               if (newShadow !== this._shadow) {
-                       this._removeShadow();
-                       addShadow = true;
-               }
+       _initImage: function () {
+               var img = this._image = L.DomUtil.create('img',
+                               'leaflet-image-layer ' + (this._zoomAnimated ? 'leaflet-zoom-animated' : ''));
 
-               if (newShadow) {
-                       L.DomUtil.addClass(newShadow, classToAdd);
-               }
-               this._shadow = newShadow;
+               img.onselectstart = L.Util.falseFn;
+               img.onmousemove = L.Util.falseFn;
 
+               img.onload = L.bind(this.fire, this, 'load');
 
-               if (options.opacity < 1) {
-                       this._updateOpacity();
+               if (this.options.crossOrigin) {
+                       img.crossOrigin = '';
                }
 
-
-               if (addIcon) {
-                       this.getPane().appendChild(this._icon);
-               }
-               this._initInteraction();
-               if (newShadow && addShadow) {
-                       this.getPane('shadowPane').appendChild(this._shadow);
-               }
+               img.src = this._url;
+               img.alt = this.options.alt;
        },
 
-       _removeIcon: function () {
-               if (this.options.riseOnHover) {
-                       this.off({
-                               mouseover: this._bringToFront,
-                               mouseout: this._resetZIndex
-                       });
-               }
-
-               L.DomUtil.remove(this._icon);
-               this.removeInteractiveTarget(this._icon);
+       _animateZoom: function (e) {
+               var scale = this._map.getZoomScale(e.zoom),
+                   offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min;
 
-               this._icon = null;
+               L.DomUtil.setTransform(this._image, offset, scale);
        },
 
-       _removeShadow: function () {
-               if (this._shadow) {
-                       L.DomUtil.remove(this._shadow);
-               }
-               this._shadow = null;
-       },
+       _reset: function () {
+               var image = this._image,
+                   bounds = new L.Bounds(
+                       this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
+                       this._map.latLngToLayerPoint(this._bounds.getSouthEast())),
+                   size = bounds.getSize();
 
-       _setPos: function (pos) {
-               L.DomUtil.setPosition(this._icon, pos);
+               L.DomUtil.setPosition(image, bounds.min);
 
-               if (this._shadow) {
-                       L.DomUtil.setPosition(this._shadow, pos);
-               }
+               image.style.width  = size.x + 'px';
+               image.style.height = size.y + 'px';
+       },
 
-               this._zIndex = pos.y + this.options.zIndexOffset;
+       _updateOpacity: function () {
+               L.DomUtil.setOpacity(this._image, this.options.opacity);
+       }
+});
 
-               this._resetZIndex();
-       },
+// @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
+// Instantiates an image overlay object given the URL of the image and the
+// geographical bounds it is tied to.
+L.imageOverlay = function (url, bounds, options) {
+       return new L.ImageOverlay(url, bounds, options);
+};
 
-       _updateZIndex: function (offset) {
-               this._icon.style.zIndex = this._zIndex + offset;
-       },
 
-       _animateZoom: function (opt) {
-               var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
 
-               this._setPos(pos);
-       },
+/*
+ * @class Icon
+ * @aka L.Icon
+ * @inherits Layer
+ *
+ * Represents an icon to provide when creating a marker.
+ *
+ * @example
+ *
+ * ```js
+ * var myIcon = L.icon({
+ *     iconUrl: 'my-icon.png',
+ *     iconRetinaUrl: 'my-icon@2x.png',
+ *     iconSize: [38, 95],
+ *     iconAnchor: [22, 94],
+ *     popupAnchor: [-3, -76],
+ *     shadowUrl: 'my-icon-shadow.png',
+ *     shadowRetinaUrl: 'my-icon-shadow@2x.png',
+ *     shadowSize: [68, 95],
+ *     shadowAnchor: [22, 94]
+ * });
+ *
+ * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
+ * ```
+ *
+ * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
+ *
+ */
 
-       _initInteraction: function () {
+L.Icon = L.Class.extend({
 
-               if (!this.options.interactive) { return; }
+       /* @section
+        * @aka Icon options
+        *
+        * @option iconUrl: String = null
+        * **(required)** The URL to the icon image (absolute or relative to your script path).
+        *
+        * @option iconRetinaUrl: String = null
+        * The URL to a retina sized version of the icon image (absolute or relative to your
+        * script path). Used for Retina screen devices.
+        *
+        * @option iconSize: Point = null
+        * Size of the icon image in pixels.
+        *
+        * @option iconAnchor: Point = null
+        * The coordinates of the "tip" of the icon (relative to its top left corner). The icon
+        * will be aligned so that this point is at the marker's geographical location. Centered
+        * by default if size is specified, also can be set in CSS with negative margins.
+        *
+        * @option popupAnchor: Point = null
+        * The coordinates of the point from which popups will "open", relative to the icon anchor.
+        *
+        * @option shadowUrl: String = null
+        * The URL to the icon shadow image. If not specified, no shadow image will be created.
+        *
+        * @option shadowRetinaUrl: String = null
+        *
+        * @option shadowSize: Point = null
+        * Size of the shadow image in pixels.
+        *
+        * @option shadowAnchor: Point = null
+        * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
+        * as iconAnchor if not specified).
+        *
+        * @option className: String = ''
+        * A custom class name to assign to both icon and shadow images. Empty by default.
+        */
 
-               L.DomUtil.addClass(this._icon, 'leaflet-interactive');
+       initialize: function (options) {
+               L.setOptions(this, options);
+       },
 
-               this.addInteractiveTarget(this._icon);
+       // @method createIcon(oldIcon?: HTMLElement): HTMLElement
+       // Called internally when the icon has to be shown, returns a `<img>` HTML element
+       // styled according to the options.
+       createIcon: function (oldIcon) {
+               return this._createIcon('icon', oldIcon);
+       },
 
-               if (L.Handler.MarkerDrag) {
-                       var draggable = this.options.draggable;
-                       if (this.dragging) {
-                               draggable = this.dragging.enabled();
-                               this.dragging.disable();
-                       }
+       // @method createShadow(oldIcon?: HTMLElement): HTMLElement
+       // As `createIcon`, but for the shadow beneath it.
+       createShadow: function (oldIcon) {
+               return this._createIcon('shadow', oldIcon);
+       },
 
-                       this.dragging = new L.Handler.MarkerDrag(this);
+       _createIcon: function (name, oldIcon) {
+               var src = this._getIconUrl(name);
 
-                       if (draggable) {
-                               this.dragging.enable();
+               if (!src) {
+                       if (name === 'icon') {
+                               throw new Error('iconUrl not set in Icon options (see the docs).');
                        }
+                       return null;
                }
+
+               var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
+               this._setIconStyles(img, name);
+
+               return img;
        },
 
-       // @method setOpacity(opacity: Number): this
-       // Changes the opacity of the marker.
-       setOpacity: function (opacity) {
-               this.options.opacity = opacity;
-               if (this._map) {
-                       this._updateOpacity();
+       _setIconStyles: function (img, name) {
+               var options = this.options;
+               var sizeOption = options[name + 'Size'];
+
+               if (typeof sizeOption === 'number') {
+                       sizeOption = [sizeOption, sizeOption];
                }
 
-               return this;
-       },
+               var size = L.point(sizeOption),
+                   anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
+                           size && size.divideBy(2, true));
 
-       _updateOpacity: function () {
-               var opacity = this.options.opacity;
+               img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
 
-               L.DomUtil.setOpacity(this._icon, opacity);
+               if (anchor) {
+                       img.style.marginLeft = (-anchor.x) + 'px';
+                       img.style.marginTop  = (-anchor.y) + 'px';
+               }
 
-               if (this._shadow) {
-                       L.DomUtil.setOpacity(this._shadow, opacity);
+               if (size) {
+                       img.style.width  = size.x + 'px';
+                       img.style.height = size.y + 'px';
                }
        },
 
-       _bringToFront: function () {
-               this._updateZIndex(this.options.riseOffset);
+       _createImg: function (src, el) {
+               el = el || document.createElement('img');
+               el.src = src;
+               return el;
        },
 
-       _resetZIndex: function () {
-               this._updateZIndex(0);
+       _getIconUrl: function (name) {
+               return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
        }
 });
 
 
-// factory L.marker(latlng: LatLng, options? : Marker options)
-
-// @factory L.marker(latlng: LatLng, options? : Marker options)
-// Instantiates a Marker object given a geographical point and optionally an options object.
-L.marker = function (latlng, options) {
-       return new L.Marker(latlng, options);
+// @factory L.icon(options: Icon options)
+// Creates an icon instance with the given options.
+L.icon = function (options) {
+       return new L.Icon(options);
 };
 
 
 
 /*
- * @class DivIcon
- * @aka L.DivIcon
- * @inherits Icon
- *
- * Represents a lightweight icon for markers that uses a simple `<div>`
- * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
+ * @miniclass Icon.Default (Icon)
+ * @aka L.Icon.Default
+ * @section
  *
- * @example
- * ```js
- * var myIcon = L.divIcon({className: 'my-div-icon'});
- * // you can set .my-div-icon styles in CSS
+ * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
+ * no icon is specified. Points to the blue marker image distributed with Leaflet
+ * releases.
  *
- * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
- * ```
+ * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options`
+ * (which is a set of `Icon options`).
  *
- * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
+ * If you want to _completely_ replace the default icon, override the
+ * `L.Marker.prototype.options.icon` with your own icon instead.
  */
 
-L.DivIcon = L.Icon.extend({
-       options: {
-               // @section
-               // @aka DivIcon options
-               iconSize: [12, 12], // also can be set through CSS
-
-               // iconAnchor: (Point),
-               // popupAnchor: (Point),
+L.Icon.Default = L.Icon.extend({
 
-               // @option html: String = ''
-               // Custom HTML code to put inside the div element, empty by default.
-               html: false,
+       options: {
+               iconUrl:       'marker-icon.png',
+               iconRetinaUrl: 'marker-icon-2x.png',
+               shadowUrl:     'marker-shadow.png',
+               iconSize:    [25, 41],
+               iconAnchor:  [12, 41],
+               popupAnchor: [1, -34],
+               tooltipAnchor: [16, -28],
+               shadowSize:  [41, 41]
+       },
 
-               // @option bgPos: Point = [0, 0]
-               // Optional relative position of the background, in pixels
-               bgPos: null,
+       _getIconUrl: function (name) {
+               if (!L.Icon.Default.imagePath) {        // Deprecated, backwards-compatibility only
+                       L.Icon.Default.imagePath = this._detectIconPath();
+               }
 
-               className: 'leaflet-div-icon'
+               // @option imagePath: String
+               // `L.Icon.Default` will try to auto-detect the absolute location of the
+               // blue icon images. If you are placing these images in a non-standard
+               // way, set this option to point to the right absolute path.
+               return (this.options.imagePath || L.Icon.Default.imagePath) + L.Icon.prototype._getIconUrl.call(this, name);
        },
 
-       createIcon: function (oldIcon) {
-               var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
-                   options = this.options;
+       _detectIconPath: function () {
+               var el = L.DomUtil.create('div',  'leaflet-default-icon-path', document.body);
+               var path = L.DomUtil.getStyle(el, 'background-image') ||
+                          L.DomUtil.getStyle(el, 'backgroundImage');   // IE8
 
-               div.innerHTML = options.html !== false ? options.html : '';
+               document.body.removeChild(el);
 
-               if (options.bgPos) {
-                       var bgPos = L.point(options.bgPos);
-                       div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
-               }
-               this._setIconStyles(div, 'icon');
-
-               return div;
-       },
-
-       createShadow: function () {
-               return null;
+               return path.indexOf('url') === 0 ?
+                       path.replace(/^url\([\"\']?/, '').replace(/marker-icon\.png[\"\']?\)$/, '') : '';
        }
 });
 
-// @factory L.divIcon(options: DivIcon options)
-// Creates a `DivIcon` instance with the given options.
-L.divIcon = function (options) {
-       return new L.DivIcon(options);
-};
-
 
 
 /*
- * @class DivOverlay
- * @inherits Layer
- * @aka L.DivOverlay
- * Base model for L.Popup and L.Tooltip. Inherit from it for custom popup like plugins.
+ * @class Marker
+ * @inherits Interactive layer
+ * @aka L.Marker
+ * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
+ *
+ * @example
+ *
+ * ```js
+ * L.marker([50.5, 30.5]).addTo(map);
+ * ```
  */
 
-// @namespace DivOverlay
-L.DivOverlay = L.Layer.extend({
+L.Marker = L.Layer.extend({
 
        // @section
-       // @aka DivOverlay options
+       // @aka Marker options
        options: {
-               // @option offset: Point = Point(0, 7)
-               // The offset of the popup position. Useful to control the anchor
-               // of the popup when opening it on some overlays.
-               offset: [0, 7],
+               // @option icon: Icon = *
+               // Icon class to use for rendering the marker. See [Icon documentation](#L.Icon) for details on how to customize the marker icon. If not specified, a new `L.Icon.Default` is used.
+               icon: new L.Icon.Default(),
 
-               // @option className: String = ''
-               // A custom CSS class name to assign to the popup.
-               className: '',
+               // Option inherited from "Interactive layer" abstract class
+               interactive: true,
 
-               // @option pane: String = 'popupPane'
-               // `Map pane` where the popup will be added.
-               pane: 'popupPane'
+               // @option draggable: Boolean = false
+               // Whether the marker is draggable with mouse/touch or not.
+               draggable: false,
+
+               // @option keyboard: Boolean = true
+               // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
+               keyboard: true,
+
+               // @option title: String = ''
+               // Text for the browser tooltip that appear on marker hover (no tooltip by default).
+               title: '',
+
+               // @option alt: String = ''
+               // Text for the `alt` attribute of the icon image (useful for accessibility).
+               alt: '',
+
+               // @option zIndexOffset: Number = 0
+               // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
+               zIndexOffset: 0,
+
+               // @option opacity: Number = 1.0
+               // The opacity of the marker.
+               opacity: 1,
+
+               // @option riseOnHover: Boolean = false
+               // If `true`, the marker will get on top of others when you hover the mouse over it.
+               riseOnHover: false,
+
+               // @option riseOffset: Number = 250
+               // The z-index offset used for the `riseOnHover` feature.
+               riseOffset: 250,
+
+               // @option pane: String = 'markerPane'
+               // `Map pane` where the markers icon will be added.
+               pane: 'markerPane',
+
+               // FIXME: shadowPane is no longer a valid option
+               nonBubblingEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu']
        },
 
-       initialize: function (options, source) {
-               L.setOptions(this, options);
+       /* @section
+        *
+        * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
+        */
 
-               this._source = source;
+       initialize: function (latlng, options) {
+               L.setOptions(this, options);
+               this._latlng = L.latLng(latlng);
        },
 
        onAdd: function (map) {
-               this._zoomAnimated = map._zoomAnimated;
-
-               if (!this._container) {
-                       this._initLayout();
-               }
+               this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
 
-               if (map._fadeAnimated) {
-                       L.DomUtil.setOpacity(this._container, 0);
+               if (this._zoomAnimated) {
+                       map.on('zoomanim', this._animateZoom, this);
                }
 
-               clearTimeout(this._removeTimeout);
-               this.getPane().appendChild(this._container);
+               this._initIcon();
                this.update();
+       },
 
-               if (map._fadeAnimated) {
-                       L.DomUtil.setOpacity(this._container, 1);
+       onRemove: function (map) {
+               if (this.dragging && this.dragging.enabled()) {
+                       this.options.draggable = true;
+                       this.dragging.removeHooks();
                }
 
-               this.bringToFront();
+               if (this._zoomAnimated) {
+                       map.off('zoomanim', this._animateZoom, this);
+               }
+
+               this._removeIcon();
+               this._removeShadow();
        },
 
-       onRemove: function (map) {
-               if (map._fadeAnimated) {
-                       L.DomUtil.setOpacity(this._container, 0);
-                       this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200);
-               } else {
-                       L.DomUtil.remove(this._container);
-               }
+       getEvents: function () {
+               return {
+                       zoom: this.update,
+                       viewreset: this.update
+               };
        },
 
-       // @namespace Popup
        // @method getLatLng: LatLng
-       // Returns the geographical point of popup.
+       // Returns the current geographical position of the marker.
        getLatLng: function () {
                return this._latlng;
        },
 
        // @method setLatLng(latlng: LatLng): this
-       // Sets the geographical point where the popup will open.
+       // Changes the marker position to the given point.
        setLatLng: function (latlng) {
+               var oldLatLng = this._latlng;
                this._latlng = L.latLng(latlng);
-               if (this._map) {
-                       this._updatePosition();
-                       this._adjustPan();
-               }
-               return this;
-       },
-
-       // @method getContent: String|HTMLElement
-       // Returns the content of the popup.
-       getContent: function () {
-               return this._content;
-       },
-
-       // @method setContent(htmlContent: String|HTMLElement|Function): this
-       // Sets the HTML content of the popup. If a function is passed the source layer will be passed to the function. The function should return a `String` or `HTMLElement` to be used in the popup.
-       setContent: function (content) {
-               this._content = content;
                this.update();
-               return this;
+
+               // @event move: Event
+               // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
+               return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
        },
 
-       // @method getElement: String|HTMLElement
-       // Alias for [getContent()](#popup-getcontent)
-       getElement: function () {
-               return this._container;
+       // @method setZIndexOffset(offset: Number): this
+       // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
+       setZIndexOffset: function (offset) {
+               this.options.zIndexOffset = offset;
+               return this.update();
        },
 
-       // @method update: null
-       // Updates the popup content, layout and position. Useful for updating the popup after something inside changed, e.g. image loaded.
-       update: function () {
-               if (!this._map) { return; }
+       // @method setIcon(icon: Icon): this
+       // Changes the marker icon.
+       setIcon: function (icon) {
 
-               this._container.style.visibility = 'hidden';
+               this.options.icon = icon;
 
-               this._updateContent();
-               this._updateLayout();
-               this._updatePosition();
+               if (this._map) {
+                       this._initIcon();
+                       this.update();
+               }
 
-               this._container.style.visibility = '';
+               if (this._popup) {
+                       this.bindPopup(this._popup, this._popup.options);
+               }
 
-               this._adjustPan();
+               return this;
        },
 
-       getEvents: function () {
-               var events = {
-                       zoom: this._updatePosition,
-                       viewreset: this._updatePosition
-               };
-
-               if (this._zoomAnimated) {
-                       events.zoomanim = this._animateZoom;
-               }
-               return events;
+       getElement: function () {
+               return this._icon;
        },
 
-       // @method isOpen: Boolean
-       // Returns `true` when the popup is visible on the map.
-       isOpen: function () {
-               return !!this._map && this._map.hasLayer(this);
-       },
+       update: function () {
 
-       // @method bringToFront: this
-       // Brings this popup in front of other popups (in the same map pane).
-       bringToFront: function () {
-               if (this._map) {
-                       L.DomUtil.toFront(this._container);
+               if (this._icon) {
+                       var pos = this._map.latLngToLayerPoint(this._latlng).round();
+                       this._setPos(pos);
                }
-               return this;
-       },
 
-       // @method bringToBack: this
-       // Brings this popup to the back of other popups (in the same map pane).
-       bringToBack: function () {
-               if (this._map) {
-                       L.DomUtil.toBack(this._container);
-               }
                return this;
        },
 
-       _updateContent: function () {
-               if (!this._content) { return; }
+       _initIcon: function () {
+               var options = this.options,
+                   classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
 
-               var node = this._contentNode;
-               var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
+               var icon = options.icon.createIcon(this._icon),
+                   addIcon = false;
 
-               if (typeof content === 'string') {
-                       node.innerHTML = content;
-               } else {
-                       while (node.hasChildNodes()) {
-                               node.removeChild(node.firstChild);
+               // if we're not reusing the icon, remove the old one and init new one
+               if (icon !== this._icon) {
+                       if (this._icon) {
+                               this._removeIcon();
                        }
-                       node.appendChild(content);
-               }
-               this.fire('contentupdate');
-       },
+                       addIcon = true;
 
-       _updatePosition: function () {
-               if (!this._map) { return; }
+                       if (options.title) {
+                               icon.title = options.title;
+                       }
+                       if (options.alt) {
+                               icon.alt = options.alt;
+                       }
+               }
 
-               var pos = this._map.latLngToLayerPoint(this._latlng),
-                   offset = L.point(this.options.offset),
-                   anchor = this._getAnchor();
+               L.DomUtil.addClass(icon, classToAdd);
 
-               if (this._zoomAnimated) {
-                       L.DomUtil.setPosition(this._container, pos.add(anchor));
-               } else {
-                       offset = offset.add(pos).add(anchor);
+               if (options.keyboard) {
+                       icon.tabIndex = '0';
                }
 
-               var bottom = this._containerBottom = -offset.y,
-                   left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x;
+               this._icon = icon;
 
-               // bottom position the popup in case the height of the popup changes (images loading etc)
-               this._container.style.bottom = bottom + 'px';
-               this._container.style.left = left + 'px';
-       },
+               if (options.riseOnHover) {
+                       this.on({
+                               mouseover: this._bringToFront,
+                               mouseout: this._resetZIndex
+                       });
+               }
 
-       _getAnchor: function () {
-               return [0, 0];
-       }
+               var newShadow = options.icon.createShadow(this._shadow),
+                   addShadow = false;
 
-});
+               if (newShadow !== this._shadow) {
+                       this._removeShadow();
+                       addShadow = true;
+               }
 
+               if (newShadow) {
+                       L.DomUtil.addClass(newShadow, classToAdd);
+               }
+               this._shadow = newShadow;
 
 
-/*
- * @class Popup
- * @inherits DivOverlay
- * @aka L.Popup
- * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
- * open popups while making sure that only one popup is open at one time
- * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
- *
- * @example
- *
- * If you want to just bind a popup to marker click and then open it, it's really easy:
- *
- * ```js
- * marker.bindPopup(popupContent).openPopup();
- * ```
- * Path overlays like polylines also have a `bindPopup` method.
- * Here's a more complicated way to open a popup on a map:
- *
- * ```js
- * var popup = L.popup()
- *     .setLatLng(latlng)
- *     .setContent('<p>Hello world!<br />This is a nice popup.</p>')
- *     .openOn(map);
- * ```
- */
+               if (options.opacity < 1) {
+                       this._updateOpacity();
+               }
 
 
-// @namespace Popup
-L.Popup = L.DivOverlay.extend({
+               if (addIcon) {
+                       this.getPane().appendChild(this._icon);
+               }
+               this._initInteraction();
+               if (newShadow && addShadow) {
+                       this.getPane('shadowPane').appendChild(this._shadow);
+               }
+       },
 
-       // @section
-       // @aka Popup options
-       options: {
-               // @option maxWidth: Number = 300
-               // Max width of the popup, in pixels.
-               maxWidth: 300,
+       _removeIcon: function () {
+               if (this.options.riseOnHover) {
+                       this.off({
+                               mouseover: this._bringToFront,
+                               mouseout: this._resetZIndex
+                       });
+               }
 
-               // @option minWidth: Number = 50
-               // Min width of the popup, in pixels.
-               minWidth: 50,
+               L.DomUtil.remove(this._icon);
+               this.removeInteractiveTarget(this._icon);
 
-               // @option maxHeight: Number = null
-               // If set, creates a scrollable container of the given height
-               // inside a popup if its content exceeds it.
-               maxHeight: null,
+               this._icon = null;
+       },
 
-               // @option autoPan: Boolean = true
-               // Set it to `false` if you don't want the map to do panning animation
-               // to fit the opened popup.
-               autoPan: true,
+       _removeShadow: function () {
+               if (this._shadow) {
+                       L.DomUtil.remove(this._shadow);
+               }
+               this._shadow = null;
+       },
 
-               // @option autoPanPaddingTopLeft: Point = null
-               // The margin between the popup and the top left corner of the map
-               // view after autopanning was performed.
-               autoPanPaddingTopLeft: null,
+       _setPos: function (pos) {
+               L.DomUtil.setPosition(this._icon, pos);
 
-               // @option autoPanPaddingBottomRight: Point = null
-               // The margin between the popup and the bottom right corner of the map
-               // view after autopanning was performed.
-               autoPanPaddingBottomRight: null,
+               if (this._shadow) {
+                       L.DomUtil.setPosition(this._shadow, pos);
+               }
 
-               // @option autoPanPadding: Point = Point(5, 5)
-               // Equivalent of setting both top left and bottom right autopan padding to the same value.
-               autoPanPadding: [5, 5],
+               this._zIndex = pos.y + this.options.zIndexOffset;
 
-               // @option keepInView: Boolean = false
-               // Set it to `true` if you want to prevent users from panning the popup
-               // off of the screen while it is open.
-               keepInView: false,
+               this._resetZIndex();
+       },
 
-               // @option closeButton: Boolean = true
-               // Controls the presence of a close button in the popup.
-               closeButton: true,
+       _updateZIndex: function (offset) {
+               this._icon.style.zIndex = this._zIndex + offset;
+       },
 
-               // @option autoClose: Boolean = true
-               // Set it to `false` if you want to override the default behavior of
-               // the popup closing when user clicks the map (set globally by
-               // the Map's [closePopupOnClick](#map-closepopuponclick) option).
-               autoClose: true,
+       _animateZoom: function (opt) {
+               var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
 
-               // @option className: String = ''
-               // A custom CSS class name to assign to the popup.
-               className: ''
+               this._setPos(pos);
        },
 
-       // @namespace Popup
-       // @method openOn(map: Map): this
-       // Adds the popup to the map and closes the previous one. The same as `map.openPopup(popup)`.
-       openOn: function (map) {
-               map.openPopup(this);
-               return this;
-       },
+       _initInteraction: function () {
 
-       onAdd: function (map) {
-               L.DivOverlay.prototype.onAdd.call(this, map);
+               if (!this.options.interactive) { return; }
 
-               // @namespace Map
-               // @section Popup events
-               // @event popupopen: PopupEvent
-               // Fired when a popup is opened in the map
-               map.fire('popupopen', {popup: this});
+               L.DomUtil.addClass(this._icon, 'leaflet-interactive');
 
-               if (this._source) {
-                       // @namespace Layer
-                       // @section Popup events
-                       // @event popupopen: PopupEvent
-                       // Fired when a popup bound to this layer is opened
-                       this._source.fire('popupopen', {popup: this}, true);
-                       // For non-path layers, we toggle the popup when clicking
-                       // again the layer, so prevent the map to reopen it.
-                       if (!(this._source instanceof L.Path)) {
-                               this._source.on('preclick', L.DomEvent.stopPropagation);
-                       }
-               }
-       },
+               this.addInteractiveTarget(this._icon);
 
-       onRemove: function (map) {
-               L.DivOverlay.prototype.onRemove.call(this, map);
+               if (L.Handler.MarkerDrag) {
+                       var draggable = this.options.draggable;
+                       if (this.dragging) {
+                               draggable = this.dragging.enabled();
+                               this.dragging.disable();
+                       }
 
-               // @namespace Map
-               // @section Popup events
-               // @event popupclose: PopupEvent
-               // Fired when a popup in the map is closed
-               map.fire('popupclose', {popup: this});
+                       this.dragging = new L.Handler.MarkerDrag(this);
 
-               if (this._source) {
-                       // @namespace Layer
-                       // @section Popup events
-                       // @event popupclose: PopupEvent
-                       // Fired when a popup bound to this layer is closed
-                       this._source.fire('popupclose', {popup: this}, true);
-                       if (!(this._source instanceof L.Path)) {
-                               this._source.off('preclick', L.DomEvent.stopPropagation);
+                       if (draggable) {
+                               this.dragging.enable();
                        }
                }
        },
 
-       getEvents: function () {
-               var events = L.DivOverlay.prototype.getEvents.call(this);
-
-               if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
-                       events.preclick = this._close;
-               }
-
-               if (this.options.keepInView) {
-                       events.moveend = this._adjustPan;
+       // @method setOpacity(opacity: Number): this
+       // Changes the opacity of the marker.
+       setOpacity: function (opacity) {
+               this.options.opacity = opacity;
+               if (this._map) {
+                       this._updateOpacity();
                }
 
-               return events;
+               return this;
        },
 
-       _close: function () {
-               if (this._map) {
-                       this._map.closePopup(this);
+       _updateOpacity: function () {
+               var opacity = this.options.opacity;
+
+               L.DomUtil.setOpacity(this._icon, opacity);
+
+               if (this._shadow) {
+                       L.DomUtil.setOpacity(this._shadow, opacity);
                }
        },
 
-       _initLayout: function () {
-               var prefix = 'leaflet-popup',
-                   container = this._container = L.DomUtil.create('div',
-                       prefix + ' ' + (this.options.className || '') +
-                       ' leaflet-zoom-animated');
+       _bringToFront: function () {
+               this._updateZIndex(this.options.riseOffset);
+       },
 
-               if (this.options.closeButton) {
-                       var closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container);
-                       closeButton.href = '#close';
-                       closeButton.innerHTML = '&#215;';
+       _resetZIndex: function () {
+               this._updateZIndex(0);
+       },
 
-                       L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this);
-               }
-
-               var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container);
-               this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
-
-               L.DomEvent
-                       .disableClickPropagation(wrapper)
-                       .disableScrollPropagation(this._contentNode)
-                       .on(wrapper, 'contextmenu', L.DomEvent.stopPropagation);
-
-               this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
-               this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
+       _getPopupAnchor: function () {
+               return this.options.icon.options.popupAnchor || [0, 0];
        },
 
-       _updateLayout: function () {
-               var container = this._contentNode,
-                   style = container.style;
+       _getTooltipAnchor: function () {
+               return this.options.icon.options.tooltipAnchor || [0, 0];
+       }
+});
 
-               style.width = '';
-               style.whiteSpace = 'nowrap';
 
-               var width = container.offsetWidth;
-               width = Math.min(width, this.options.maxWidth);
-               width = Math.max(width, this.options.minWidth);
+// factory L.marker(latlng: LatLng, options? : Marker options)
 
-               style.width = (width + 1) + 'px';
-               style.whiteSpace = '';
+// @factory L.marker(latlng: LatLng, options? : Marker options)
+// Instantiates a Marker object given a geographical point and optionally an options object.
+L.marker = function (latlng, options) {
+       return new L.Marker(latlng, options);
+};
 
-               style.height = '';
 
-               var height = container.offsetHeight,
-                   maxHeight = this.options.maxHeight,
-                   scrolledClass = 'leaflet-popup-scrolled';
 
-               if (maxHeight && height > maxHeight) {
-                       style.height = maxHeight + 'px';
-                       L.DomUtil.addClass(container, scrolledClass);
-               } else {
-                       L.DomUtil.removeClass(container, scrolledClass);
-               }
+/*
+ * @class DivIcon
+ * @aka L.DivIcon
+ * @inherits Icon
+ *
+ * Represents a lightweight icon for markers that uses a simple `<div>`
+ * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
+ *
+ * @example
+ * ```js
+ * var myIcon = L.divIcon({className: 'my-div-icon'});
+ * // you can set .my-div-icon styles in CSS
+ *
+ * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
+ * ```
+ *
+ * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
+ */
 
-               this._containerWidth = this._container.offsetWidth;
-       },
+L.DivIcon = L.Icon.extend({
+       options: {
+               // @section
+               // @aka DivIcon options
+               iconSize: [12, 12], // also can be set through CSS
 
-       _animateZoom: function (e) {
-               var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
-                   anchor = this._getAnchor();
-               L.DomUtil.setPosition(this._container, pos.add(anchor));
-       },
+               // iconAnchor: (Point),
+               // popupAnchor: (Point),
 
-       _adjustPan: function () {
-               if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; }
+               // @option html: String = ''
+               // Custom HTML code to put inside the div element, empty by default.
+               html: false,
 
-               var map = this._map,
-                   marginBottom = parseInt(L.DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0,
-                   containerHeight = this._container.offsetHeight + marginBottom,
-                   containerWidth = this._containerWidth,
-                   layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
+               // @option bgPos: Point = [0, 0]
+               // Optional relative position of the background, in pixels
+               bgPos: null,
 
-               layerPos._add(L.DomUtil.getPosition(this._container));
+               className: 'leaflet-div-icon'
+       },
 
-               var containerPos = map.layerPointToContainerPoint(layerPos),
-                   padding = L.point(this.options.autoPanPadding),
-                   paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding),
-                   paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding),
-                   size = map.getSize(),
-                   dx = 0,
-                   dy = 0;
+       createIcon: function (oldIcon) {
+               var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
+                   options = this.options;
 
-               if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
-                       dx = containerPos.x + containerWidth - size.x + paddingBR.x;
-               }
-               if (containerPos.x - dx - paddingTL.x < 0) { // left
-                       dx = containerPos.x - paddingTL.x;
-               }
-               if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
-                       dy = containerPos.y + containerHeight - size.y + paddingBR.y;
-               }
-               if (containerPos.y - dy - paddingTL.y < 0) { // top
-                       dy = containerPos.y - paddingTL.y;
-               }
+               div.innerHTML = options.html !== false ? options.html : '';
 
-               // @namespace Map
-               // @section Popup events
-               // @event autopanstart: Event
-               // Fired when the map starts autopanning when opening a popup.
-               if (dx || dy) {
-                       map
-                           .fire('autopanstart')
-                           .panBy([dx, dy]);
+               if (options.bgPos) {
+                       var bgPos = L.point(options.bgPos);
+                       div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
                }
-       },
+               this._setIconStyles(div, 'icon');
 
-       _onCloseButtonClick: function (e) {
-               this._close();
-               L.DomEvent.stop(e);
+               return div;
        },
 
-       _getAnchor: function () {
-               // Where should we anchor the popup on the source layer?
-               return L.point(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
+       createShadow: function () {
+               return null;
        }
-
 });
 
-// @namespace Popup
-// @factory L.popup(options?: Popup options, source?: Layer)
-// Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers.
-L.popup = function (options, source) {
-       return new L.Popup(options, source);
+// @factory L.divIcon(options: DivIcon options)
+// Creates a `DivIcon` instance with the given options.
+L.divIcon = function (options) {
+       return new L.DivIcon(options);
 };
 
 
-/* @namespace Map
- * @section Interaction Options
- * @option closePopupOnClick: Boolean = true
- * Set it to `false` if you don't want popups to close when user clicks the map.
+
+/*
+ * @class DivOverlay
+ * @inherits Layer
+ * @aka L.DivOverlay
+ * Base model for L.Popup and L.Tooltip. Inherit from it for custom popup like plugins.
  */
-L.Map.mergeOptions({
-       closePopupOnClick: true
-});
 
+// @namespace DivOverlay
+L.DivOverlay = L.Layer.extend({
 
-// @namespace Map
-// @section Methods for Layers and Controls
-L.Map.include({
-       // @method openPopup(popup: Popup): this
-       // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
-       // @alternative
-       // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
-       // Creates a popup with the specified content and options and opens it in the given point on a map.
-       openPopup: function (popup, latlng, options) {
-               if (!(popup instanceof L.Popup)) {
-                       popup = new L.Popup(options).setContent(popup);
-               }
+       // @section
+       // @aka DivOverlay options
+       options: {
+               // @option offset: Point = Point(0, 7)
+               // The offset of the popup position. Useful to control the anchor
+               // of the popup when opening it on some overlays.
+               offset: [0, 7],
 
-               if (latlng) {
-                       popup.setLatLng(latlng);
+               // @option className: String = ''
+               // A custom CSS class name to assign to the popup.
+               className: '',
+
+               // @option pane: String = 'popupPane'
+               // `Map pane` where the popup will be added.
+               pane: 'popupPane'
+       },
+
+       initialize: function (options, source) {
+               L.setOptions(this, options);
+
+               this._source = source;
+       },
+
+       onAdd: function (map) {
+               this._zoomAnimated = map._zoomAnimated;
+
+               if (!this._container) {
+                       this._initLayout();
                }
 
-               if (this.hasLayer(popup)) {
-                       return this;
+               if (map._fadeAnimated) {
+                       L.DomUtil.setOpacity(this._container, 0);
                }
 
-               if (this._popup && this._popup.options.autoClose) {
-                       this.closePopup();
+               clearTimeout(this._removeTimeout);
+               this.getPane().appendChild(this._container);
+               this.update();
+
+               if (map._fadeAnimated) {
+                       L.DomUtil.setOpacity(this._container, 1);
                }
 
-               this._popup = popup;
-               return this.addLayer(popup);
+               this.bringToFront();
        },
 
-       // @method closePopup(popup?: Popup): this
-       // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
-       closePopup: function (popup) {
-               if (!popup || popup === this._popup) {
-                       popup = this._popup;
-                       this._popup = null;
+       onRemove: function (map) {
+               if (map._fadeAnimated) {
+                       L.DomUtil.setOpacity(this._container, 0);
+                       this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200);
+               } else {
+                       L.DomUtil.remove(this._container);
                }
-               if (popup) {
-                       this.removeLayer(popup);
+       },
+
+       // @namespace Popup
+       // @method getLatLng: LatLng
+       // Returns the geographical point of popup.
+       getLatLng: function () {
+               return this._latlng;
+       },
+
+       // @method setLatLng(latlng: LatLng): this
+       // Sets the geographical point where the popup will open.
+       setLatLng: function (latlng) {
+               this._latlng = L.latLng(latlng);
+               if (this._map) {
+                       this._updatePosition();
+                       this._adjustPan();
                }
                return this;
-       }
-});
+       },
 
+       // @method getContent: String|HTMLElement
+       // Returns the content of the popup.
+       getContent: function () {
+               return this._content;
+       },
 
-
-/*
- * @namespace Layer
- * @section Popup methods example
- *
- * All layers share a set of methods convenient for binding popups to it.
- *
- * ```js
- * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
- * layer.openPopup();
- * layer.closePopup();
- * ```
- *
- * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened.
- */
-
-// @section Popup methods
-L.Layer.include({
-
-       // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
-       // Binds a popup to the layer with the passed `content` and sets up the
-       // neccessary event listeners. If a `Function` is passed it will receive
-       // the layer as the first argument and should return a `String` or `HTMLElement`.
-       bindPopup: function (content, options) {
-
-               if (content instanceof L.Popup) {
-                       L.setOptions(content, options);
-                       this._popup = content;
-                       content._source = this;
-               } else {
-                       if (!this._popup || options) {
-                               this._popup = new L.Popup(options, this);
-                       }
-                       this._popup.setContent(content);
-               }
-
-               if (!this._popupHandlersAdded) {
-                       this.on({
-                               click: this._openPopup,
-                               remove: this.closePopup,
-                               move: this._movePopup
-                       });
-                       this._popupHandlersAdded = true;
-               }
-
+       // @method setContent(htmlContent: String|HTMLElement|Function): this
+       // Sets the HTML content of the popup. If a function is passed the source layer will be passed to the function. The function should return a `String` or `HTMLElement` to be used in the popup.
+       setContent: function (content) {
+               this._content = content;
+               this.update();
                return this;
        },
 
-       // @method unbindPopup(): this
-       // Removes the popup previously bound with `bindPopup`.
-       unbindPopup: function () {
-               if (this._popup) {
-                       this.off({
-                               click: this._openPopup,
-                               remove: this.closePopup,
-                               move: this._movePopup
-                       });
-                       this._popupHandlersAdded = false;
-                       this._popup = null;
-               }
-               return this;
+       // @method getElement: String|HTMLElement
+       // Alias for [getContent()](#popup-getcontent)
+       getElement: function () {
+               return this._container;
        },
 
-       // @method openPopup(latlng?: LatLng): this
-       // Opens the bound popup at the specificed `latlng` or at the default popup anchor if no `latlng` is passed.
-       openPopup: function (layer, latlng) {
-               if (!(layer instanceof L.Layer)) {
-                       latlng = layer;
-                       layer = this;
-               }
-
-               if (layer instanceof L.FeatureGroup) {
-                       for (var id in this._layers) {
-                               layer = this._layers[id];
-                               break;
-                       }
-               }
-
-               if (!latlng) {
-                       latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
-               }
+       // @method update: null
+       // Updates the popup content, layout and position. Useful for updating the popup after something inside changed, e.g. image loaded.
+       update: function () {
+               if (!this._map) { return; }
 
-               if (this._popup && this._map) {
-                       // set popup source to this layer
-                       this._popup._source = layer;
+               this._container.style.visibility = 'hidden';
 
-                       // update the popup (content, layout, ect...)
-                       this._popup.update();
+               this._updateContent();
+               this._updateLayout();
+               this._updatePosition();
 
-                       // open the popup on the map
-                       this._map.openPopup(this._popup, latlng);
-               }
+               this._container.style.visibility = '';
 
-               return this;
+               this._adjustPan();
        },
 
-       // @method closePopup(): this
-       // Closes the popup bound to this layer if it is open.
-       closePopup: function () {
-               if (this._popup) {
-                       this._popup._close();
-               }
-               return this;
-       },
+       getEvents: function () {
+               var events = {
+                       zoom: this._updatePosition,
+                       viewreset: this._updatePosition
+               };
 
-       // @method togglePopup(): this
-       // Opens or closes the popup bound to this layer depending on its current state.
-       togglePopup: function (target) {
-               if (this._popup) {
-                       if (this._popup._map) {
-                               this.closePopup();
-                       } else {
-                               this.openPopup(target);
-                       }
+               if (this._zoomAnimated) {
+                       events.zoomanim = this._animateZoom;
                }
-               return this;
+               return events;
        },
 
-       // @method isPopupOpen(): boolean
-       // Returns `true` if the popup bound to this layer is currently open.
-       isPopupOpen: function () {
-               return this._popup.isOpen();
+       // @method isOpen: Boolean
+       // Returns `true` when the popup is visible on the map.
+       isOpen: function () {
+               return !!this._map && this._map.hasLayer(this);
        },
 
-       // @method setPopupContent(content: String|HTMLElement|Popup): this
-       // Sets the content of the popup bound to this layer.
-       setPopupContent: function (content) {
-               if (this._popup) {
-                       this._popup.setContent(content);
+       // @method bringToFront: this
+       // Brings this popup in front of other popups (in the same map pane).
+       bringToFront: function () {
+               if (this._map) {
+                       L.DomUtil.toFront(this._container);
                }
                return this;
        },
 
-       // @method getPopup(): Popup
-       // Returns the popup bound to this layer.
-       getPopup: function () {
-               return this._popup;
+       // @method bringToBack: this
+       // Brings this popup to the back of other popups (in the same map pane).
+       bringToBack: function () {
+               if (this._map) {
+                       L.DomUtil.toBack(this._container);
+               }
+               return this;
        },
 
-       _openPopup: function (e) {
-               var layer = e.layer || e.target;
+       _updateContent: function () {
+               if (!this._content) { return; }
 
-               if (!this._popup) {
-                       return;
-               }
+               var node = this._contentNode;
+               var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
 
-               if (!this._map) {
-                       return;
+               if (typeof content === 'string') {
+                       node.innerHTML = content;
+               } else {
+                       while (node.hasChildNodes()) {
+                               node.removeChild(node.firstChild);
+                       }
+                       node.appendChild(content);
                }
+               this.fire('contentupdate');
+       },
 
-               // prevent map click
-               L.DomEvent.stop(e);
+       _updatePosition: function () {
+               if (!this._map) { return; }
 
-               // if this inherits from Path its a vector and we can just
-               // open the popup at the new location
-               if (layer instanceof L.Path) {
-                       this.openPopup(e.layer || e.target, e.latlng);
-                       return;
-               }
+               var pos = this._map.latLngToLayerPoint(this._latlng),
+                   offset = L.point(this.options.offset),
+                   anchor = this._getAnchor();
 
-               // otherwise treat it like a marker and figure out
-               // if we should toggle it open/closed
-               if (this._map.hasLayer(this._popup) && this._popup._source === layer) {
-                       this.closePopup();
+               if (this._zoomAnimated) {
+                       L.DomUtil.setPosition(this._container, pos.add(anchor));
                } else {
-                       this.openPopup(layer, e.latlng);
+                       offset = offset.add(pos).add(anchor);
                }
-       },
-
-       _movePopup: function (e) {
-               this._popup.setLatLng(e.latlng);
-       }
-});
-
 
+               var bottom = this._containerBottom = -offset.y,
+                   left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x;
 
-/*
- * Popup extension to L.Marker, adding popup-related methods.
- */
+               // bottom position the popup in case the height of the popup changes (images loading etc)
+               this._container.style.bottom = bottom + 'px';
+               this._container.style.left = left + 'px';
+       },
 
-L.Marker.include({
-       _getPopupAnchor: function () {
-               return this.options.icon.options.popupAnchor || [0, 0];
+       _getAnchor: function () {
+               return [0, 0];
        }
+
 });
 
 
 
 /*
- * @class Tooltip
+ * @class Popup
  * @inherits DivOverlay
- * @aka L.Tooltip
- * Used to display small texts on top of map layers.
+ * @aka L.Popup
+ * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
+ * open popups while making sure that only one popup is open at one time
+ * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
  *
  * @example
  *
+ * If you want to just bind a popup to marker click and then open it, it's really easy:
+ *
  * ```js
- * marker.bindTooltip("my tooltip text").openTooltip();
+ * marker.bindPopup(popupContent).openPopup();
+ * ```
+ * Path overlays like polylines also have a `bindPopup` method.
+ * Here's a more complicated way to open a popup on a map:
+ *
+ * ```js
+ * var popup = L.popup()
+ *     .setLatLng(latlng)
+ *     .setContent('<p>Hello world!<br />This is a nice popup.</p>')
+ *     .openOn(map);
  * ```
- * Note about tooltip offset. Leaflet takes two options in consideration
- * for computing tooltip offseting:
- * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
- *   Add a positive x offset to move the tooltip to the right, and a positive y offset to
- *   move it to the bottom. Negatives will move to the left and top.
- * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
- *   should adapt this value if you use a custom icon.
  */
 
 
-// @namespace Tooltip
-L.Tooltip = L.DivOverlay.extend({
+// @namespace Popup
+L.Popup = L.DivOverlay.extend({
 
        // @section
-       // @aka Tooltip options
+       // @aka Popup options
        options: {
-               // @option pane: String = 'tooltipPane'
-               // `Map pane` where the tooltip will be added.
-               pane: 'tooltipPane',
+               // @option maxWidth: Number = 300
+               // Max width of the popup, in pixels.
+               maxWidth: 300,
 
-               // @option offset: Point = Point(0, 0)
-               // Optional offset of the tooltip position.
-               offset: [0, 0],
+               // @option minWidth: Number = 50
+               // Min width of the popup, in pixels.
+               minWidth: 50,
 
-               // @option direction: String = 'auto'
-               // Direction where to open the tooltip. Possible values are: `right`, `left`,
-               // `top`, `bottom`, `center`, `auto`.
-               // `auto` will dynamicaly switch between `right` and `left` according to the tooltip
-               // position on the map.
-               direction: 'auto',
+               // @option maxHeight: Number = null
+               // If set, creates a scrollable container of the given height
+               // inside a popup if its content exceeds it.
+               maxHeight: null,
 
-               // @option permanent: Boolean = false
-               // Whether to open the tooltip permanently or only on mouseover.
-               permanent: false,
+               // @option autoPan: Boolean = true
+               // Set it to `false` if you don't want the map to do panning animation
+               // to fit the opened popup.
+               autoPan: true,
 
-               // @option sticky: Boolean = false
-               // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
-               sticky: false,
+               // @option autoPanPaddingTopLeft: Point = null
+               // The margin between the popup and the top left corner of the map
+               // view after autopanning was performed.
+               autoPanPaddingTopLeft: null,
 
-               // @option interactive: Boolean = false
-               // If true, the tooltip will listen to the feature events.
-               interactive: false,
+               // @option autoPanPaddingBottomRight: Point = null
+               // The margin between the popup and the bottom right corner of the map
+               // view after autopanning was performed.
+               autoPanPaddingBottomRight: null,
 
-               // @option opacity: Number = 0.9
-               // Tooltip container opacity.
-               opacity: 0.9
+               // @option autoPanPadding: Point = Point(5, 5)
+               // Equivalent of setting both top left and bottom right autopan padding to the same value.
+               autoPanPadding: [5, 5],
+
+               // @option keepInView: Boolean = false
+               // Set it to `true` if you want to prevent users from panning the popup
+               // off of the screen while it is open.
+               keepInView: false,
+
+               // @option closeButton: Boolean = true
+               // Controls the presence of a close button in the popup.
+               closeButton: true,
+
+               // @option autoClose: Boolean = true
+               // Set it to `false` if you want to override the default behavior of
+               // the popup closing when user clicks the map (set globally by
+               // the Map's [closePopupOnClick](#map-closepopuponclick) option).
+               autoClose: true,
+
+               // @option className: String = ''
+               // A custom CSS class name to assign to the popup.
+               className: ''
+       },
+
+       // @namespace Popup
+       // @method openOn(map: Map): this
+       // Adds the popup to the map and closes the previous one. The same as `map.openPopup(popup)`.
+       openOn: function (map) {
+               map.openPopup(this);
+               return this;
        },
 
        onAdd: function (map) {
                L.DivOverlay.prototype.onAdd.call(this, map);
-               this.setOpacity(this.options.opacity);
 
                // @namespace Map
-               // @section Tooltip events
-               // @event tooltipopen: TooltipEvent
-               // Fired when a tooltip is opened in the map.
-               map.fire('tooltipopen', {tooltip: this});
+               // @section Popup events
+               // @event popupopen: PopupEvent
+               // Fired when a popup is opened in the map
+               map.fire('popupopen', {popup: this});
 
                if (this._source) {
                        // @namespace Layer
-                       // @section Tooltip events
-                       // @event tooltipopen: TooltipEvent
-                       // Fired when a tooltip bound to this layer is opened.
-                       this._source.fire('tooltipopen', {tooltip: this}, true);
+                       // @section Popup events
+                       // @event popupopen: PopupEvent
+                       // Fired when a popup bound to this layer is opened
+                       this._source.fire('popupopen', {popup: this}, true);
+                       // For non-path layers, we toggle the popup when clicking
+                       // again the layer, so prevent the map to reopen it.
+                       if (!(this._source instanceof L.Path)) {
+                               this._source.on('preclick', L.DomEvent.stopPropagation);
+                       }
                }
        },
 
@@ -6641,230 +7013,286 @@ L.Tooltip = L.DivOverlay.extend({
                L.DivOverlay.prototype.onRemove.call(this, map);
 
                // @namespace Map
-               // @section Tooltip events
-               // @event tooltipclose: TooltipEvent
-               // Fired when a tooltip in the map is closed.
-               map.fire('tooltipclose', {tooltip: this});
+               // @section Popup events
+               // @event popupclose: PopupEvent
+               // Fired when a popup in the map is closed
+               map.fire('popupclose', {popup: this});
 
                if (this._source) {
                        // @namespace Layer
-                       // @section Tooltip events
-                       // @event tooltipclose: TooltipEvent
-                       // Fired when a tooltip bound to this layer is closed.
-                       this._source.fire('tooltipclose', {tooltip: this}, true);
+                       // @section Popup events
+                       // @event popupclose: PopupEvent
+                       // Fired when a popup bound to this layer is closed
+                       this._source.fire('popupclose', {popup: this}, true);
+                       if (!(this._source instanceof L.Path)) {
+                               this._source.off('preclick', L.DomEvent.stopPropagation);
+                       }
                }
        },
 
        getEvents: function () {
                var events = L.DivOverlay.prototype.getEvents.call(this);
 
-               if (L.Browser.touch && !this.options.permanent) {
+               if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
                        events.preclick = this._close;
                }
 
+               if (this.options.keepInView) {
+                       events.moveend = this._adjustPan;
+               }
+
                return events;
        },
 
        _close: function () {
                if (this._map) {
-                       this._map.closeTooltip(this);
+                       this._map.closePopup(this);
                }
        },
 
        _initLayout: function () {
-               var prefix = 'leaflet-tooltip',
-                   className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
+               var prefix = 'leaflet-popup',
+                   container = this._container = L.DomUtil.create('div',
+                       prefix + ' ' + (this.options.className || '') +
+                       ' leaflet-zoom-animated');
 
-               this._contentNode = this._container = L.DomUtil.create('div', className);
+               if (this.options.closeButton) {
+                       var closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container);
+                       closeButton.href = '#close';
+                       closeButton.innerHTML = '&#215;';
+
+                       L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this);
+               }
+
+               var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container);
+               this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
+
+               L.DomEvent
+                       .disableClickPropagation(wrapper)
+                       .disableScrollPropagation(this._contentNode)
+                       .on(wrapper, 'contextmenu', L.DomEvent.stopPropagation);
+
+               this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
+               this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
        },
 
-       _updateLayout: function () {},
+       _updateLayout: function () {
+               var container = this._contentNode,
+                   style = container.style;
 
-       _adjustPan: function () {},
+               style.width = '';
+               style.whiteSpace = 'nowrap';
 
-       _setPosition: function (pos) {
-               var map = this._map,
-                   container = this._container,
-                   centerPoint = map.latLngToContainerPoint(map.getCenter()),
-                   tooltipPoint = map.layerPointToContainerPoint(pos),
-                   direction = this.options.direction,
-                   tooltipWidth = container.offsetWidth,
-                   tooltipHeight = container.offsetHeight,
-                   offset = L.point(this.options.offset),
-                   anchor = this._getAnchor();
+               var width = container.offsetWidth;
+               width = Math.min(width, this.options.maxWidth);
+               width = Math.max(width, this.options.minWidth);
 
-               if (direction === 'top') {
-                       pos = pos.add(L.point(-tooltipWidth / 2 + offset.x, -tooltipHeight + offset.y + anchor.y));
-               } else if (direction === 'bottom') {
-                       pos = pos.subtract(L.point(tooltipWidth / 2 - offset.x, -offset.y));
-               } else if (direction === 'center') {
-                       pos = pos.subtract(L.point(tooltipWidth / 2 + offset.x, tooltipHeight / 2 - anchor.y + offset.y));
-               } else if (direction === 'right' || direction === 'auto' && tooltipPoint.x < centerPoint.x) {
-                       direction = 'right';
-                       pos = pos.add([offset.x + anchor.x, anchor.y - tooltipHeight / 2 + offset.y]);
+               style.width = (width + 1) + 'px';
+               style.whiteSpace = '';
+
+               style.height = '';
+
+               var height = container.offsetHeight,
+                   maxHeight = this.options.maxHeight,
+                   scrolledClass = 'leaflet-popup-scrolled';
+
+               if (maxHeight && height > maxHeight) {
+                       style.height = maxHeight + 'px';
+                       L.DomUtil.addClass(container, scrolledClass);
                } else {
-                       direction = 'left';
-                       pos = pos.subtract(L.point(tooltipWidth + anchor.x - offset.x, tooltipHeight / 2 - anchor.y - offset.y));
+                       L.DomUtil.removeClass(container, scrolledClass);
                }
 
-               L.DomUtil.removeClass(container, 'leaflet-tooltip-right');
-               L.DomUtil.removeClass(container, 'leaflet-tooltip-left');
-               L.DomUtil.removeClass(container, 'leaflet-tooltip-top');
-               L.DomUtil.removeClass(container, 'leaflet-tooltip-bottom');
-               L.DomUtil.addClass(container, 'leaflet-tooltip-' + direction);
-               L.DomUtil.setPosition(container, pos);
+               this._containerWidth = this._container.offsetWidth;
        },
 
-       _updatePosition: function () {
-               var pos = this._map.latLngToLayerPoint(this._latlng);
-               this._setPosition(pos);
+       _animateZoom: function (e) {
+               var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
+                   anchor = this._getAnchor();
+               L.DomUtil.setPosition(this._container, pos.add(anchor));
        },
 
-       setOpacity: function (opacity) {
-               this.options.opacity = opacity;
+       _adjustPan: function () {
+               if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; }
 
-               if (this._container) {
-                       L.DomUtil.setOpacity(this._container, opacity);
+               var map = this._map,
+                   marginBottom = parseInt(L.DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0,
+                   containerHeight = this._container.offsetHeight + marginBottom,
+                   containerWidth = this._containerWidth,
+                   layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
+
+               layerPos._add(L.DomUtil.getPosition(this._container));
+
+               var containerPos = map.layerPointToContainerPoint(layerPos),
+                   padding = L.point(this.options.autoPanPadding),
+                   paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding),
+                   paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding),
+                   size = map.getSize(),
+                   dx = 0,
+                   dy = 0;
+
+               if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
+                       dx = containerPos.x + containerWidth - size.x + paddingBR.x;
+               }
+               if (containerPos.x - dx - paddingTL.x < 0) { // left
+                       dx = containerPos.x - paddingTL.x;
+               }
+               if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
+                       dy = containerPos.y + containerHeight - size.y + paddingBR.y;
+               }
+               if (containerPos.y - dy - paddingTL.y < 0) { // top
+                       dy = containerPos.y - paddingTL.y;
+               }
+
+               // @namespace Map
+               // @section Popup events
+               // @event autopanstart: Event
+               // Fired when the map starts autopanning when opening a popup.
+               if (dx || dy) {
+                       map
+                           .fire('autopanstart')
+                           .panBy([dx, dy]);
                }
        },
 
-       _animateZoom: function (e) {
-               var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
-               this._setPosition(pos);
+       _onCloseButtonClick: function (e) {
+               this._close();
+               L.DomEvent.stop(e);
        },
 
        _getAnchor: function () {
-               // Where should we anchor the tooltip on the source layer?
-               return L.point(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
+               // Where should we anchor the popup on the source layer?
+               return L.point(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
        }
 
 });
 
-// @namespace Tooltip
-// @factory L.tooltip(options?: Tooltip options, source?: Layer)
-// Instantiates a Tooltip object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers.
-L.tooltip = function (options, source) {
-       return new L.Tooltip(options, source);
+// @namespace Popup
+// @factory L.popup(options?: Popup options, source?: Layer)
+// Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers.
+L.popup = function (options, source) {
+       return new L.Popup(options, source);
 };
 
+
+/* @namespace Map
+ * @section Interaction Options
+ * @option closePopupOnClick: Boolean = true
+ * Set it to `false` if you don't want popups to close when user clicks the map.
+ */
+L.Map.mergeOptions({
+       closePopupOnClick: true
+});
+
+
 // @namespace Map
 // @section Methods for Layers and Controls
 L.Map.include({
-
-       // @method openTooltip(tooltip: Tooltip): this
-       // Opens the specified tooltip.
+       // @method openPopup(popup: Popup): this
+       // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
        // @alternative
-       // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
-       // Creates a tooltip with the specified content and options and open it.
-       openTooltip: function (tooltip, latlng, options) {
-               if (!(tooltip instanceof L.Tooltip)) {
-                       tooltip = new L.Tooltip(options).setContent(tooltip);
+       // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
+       // Creates a popup with the specified content and options and opens it in the given point on a map.
+       openPopup: function (popup, latlng, options) {
+               if (!(popup instanceof L.Popup)) {
+                       popup = new L.Popup(options).setContent(popup);
                }
 
                if (latlng) {
-                       tooltip.setLatLng(latlng);
+                       popup.setLatLng(latlng);
                }
 
-               if (this.hasLayer(tooltip)) {
+               if (this.hasLayer(popup)) {
                        return this;
                }
 
-               return this.addLayer(tooltip);
+               if (this._popup && this._popup.options.autoClose) {
+                       this.closePopup();
+               }
+
+               this._popup = popup;
+               return this.addLayer(popup);
        },
 
-       // @method closeTooltip(tooltip?: Tooltip): this
-       // Closes the tooltip given as parameter.
-       closeTooltip: function (tooltip) {
-               if (tooltip) {
-                       this.removeLayer(tooltip);
+       // @method closePopup(popup?: Popup): this
+       // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
+       closePopup: function (popup) {
+               if (!popup || popup === this._popup) {
+                       popup = this._popup;
+                       this._popup = null;
+               }
+               if (popup) {
+                       this.removeLayer(popup);
                }
                return this;
        }
-
 });
 
-
-
 /*
  * @namespace Layer
- * @section Tooltip methods example
+ * @section Popup methods example
  *
- * All layers share a set of methods convenient for binding tooltips to it.
+ * All layers share a set of methods convenient for binding popups to it.
  *
  * ```js
- * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
- * layer.openTooltip();
- * layer.closeTooltip();
+ * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
+ * layer.openPopup();
+ * layer.closePopup();
  * ```
+ *
+ * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened.
  */
 
-// @section Tooltip methods
+// @section Popup methods
 L.Layer.include({
 
-       // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
-       // Binds a tooltip to the layer with the passed `content` and sets up the
+       // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
+       // Binds a popup to the layer with the passed `content` and sets up the
        // neccessary event listeners. If a `Function` is passed it will receive
        // the layer as the first argument and should return a `String` or `HTMLElement`.
-       bindTooltip: function (content, options) {
+       bindPopup: function (content, options) {
 
-               if (content instanceof L.Tooltip) {
+               if (content instanceof L.Popup) {
                        L.setOptions(content, options);
-                       this._tooltip = content;
+                       this._popup = content;
                        content._source = this;
                } else {
-                       if (!this._tooltip || options) {
-                               this._tooltip = L.tooltip(options, this);
+                       if (!this._popup || options) {
+                               this._popup = new L.Popup(options, this);
                        }
-                       this._tooltip.setContent(content);
-
+                       this._popup.setContent(content);
                }
 
-               this._initTooltipInteractions();
-
-               if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
-                       this.openTooltip();
+               if (!this._popupHandlersAdded) {
+                       this.on({
+                               click: this._openPopup,
+                               remove: this.closePopup,
+                               move: this._movePopup
+                       });
+                       this._popupHandlersAdded = true;
                }
 
                return this;
        },
 
-       // @method unbindTooltip(): this
-       // Removes the tooltip previously bound with `bindTooltip`.
-       unbindTooltip: function () {
-               if (this._tooltip) {
-                       this._initTooltipInteractions(true);
-                       this.closeTooltip();
-                       this._tooltip = null;
+       // @method unbindPopup(): this
+       // Removes the popup previously bound with `bindPopup`.
+       unbindPopup: function () {
+               if (this._popup) {
+                       this.off({
+                               click: this._openPopup,
+                               remove: this.closePopup,
+                               move: this._movePopup
+                       });
+                       this._popupHandlersAdded = false;
+                       this._popup = null;
                }
                return this;
        },
 
-       _initTooltipInteractions: function (remove) {
-               if (!remove && this._tooltipHandlersAdded) { return; }
-               var onOff = remove ? 'off' : 'on',
-                   events = {
-                       remove: this.closeTooltip,
-                       move: this._moveTooltip
-                   };
-               if (!this._tooltip.options.permanent) {
-                       events.mouseover = this._openTooltip;
-                       events.mouseout = this.closeTooltip;
-                       if (this._tooltip.options.sticky) {
-                               events.mousemove = this._moveTooltip;
-                       }
-                       if (L.Browser.touch) {
-                               events.click = this._openTooltip;
-                       }
-               } else {
-                       events.add = this._openTooltip;
-               }
-               this[onOff](events);
-               this._tooltipHandlersAdded = !remove;
-       },
-
-       // @method openTooltip(latlng?: LatLng): this
-       // Opens the bound tooltip at the specificed `latlng` or at the default tooltip anchor if no `latlng` is passed.
-       openTooltip: function (layer, latlng) {
+       // @method openPopup(latlng?: LatLng): this
+       // Opens the bound popup at the specificed `latlng` or at the default popup anchor if no `latlng` is passed.
+       openPopup: function (layer, latlng) {
                if (!(layer instanceof L.Layer)) {
                        latlng = layer;
                        layer = this;
@@ -6881,6164 +7309,5862 @@ L.Layer.include({
                        latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
                }
 
-               if (this._tooltip && this._map) {
-
-                       // set tooltip source to this layer
-                       this._tooltip._source = layer;
-
-                       // update the tooltip (content, layout, ect...)
-                       this._tooltip.update();
+               if (this._popup && this._map) {
+                       // set popup source to this layer
+                       this._popup._source = layer;
 
-                       // open the tooltip on the map
-                       this._map.openTooltip(this._tooltip, latlng);
+                       // update the popup (content, layout, ect...)
+                       this._popup.update();
 
-                       // Tooltip container may not be defined if not permanent and never
-                       // opened.
-                       if (this._tooltip.options.interactive && this._tooltip._container) {
-                               L.DomUtil.addClass(this._tooltip._container, 'leaflet-clickable');
-                               this.addInteractiveTarget(this._tooltip._container);
-                       }
+                       // open the popup on the map
+                       this._map.openPopup(this._popup, latlng);
                }
 
                return this;
        },
 
-       // @method closeTooltip(): this
-       // Closes the tooltip bound to this layer if it is open.
-       closeTooltip: function () {
-               if (this._tooltip) {
-                       this._tooltip._close();
-                       if (this._tooltip.options.interactive && this._tooltip._container) {
-                               L.DomUtil.removeClass(this._tooltip._container, 'leaflet-clickable');
-                               this.removeInteractiveTarget(this._tooltip._container);
-                       }
+       // @method closePopup(): this
+       // Closes the popup bound to this layer if it is open.
+       closePopup: function () {
+               if (this._popup) {
+                       this._popup._close();
                }
                return this;
        },
 
-       // @method toggleTooltip(): this
-       // Opens or closes the tooltip bound to this layer depending on its current state.
-       toggleTooltip: function (target) {
-               if (this._tooltip) {
-                       if (this._tooltip._map) {
-                               this.closeTooltip();
+       // @method togglePopup(): this
+       // Opens or closes the popup bound to this layer depending on its current state.
+       togglePopup: function (target) {
+               if (this._popup) {
+                       if (this._popup._map) {
+                               this.closePopup();
                        } else {
-                               this.openTooltip(target);
+                               this.openPopup(target);
                        }
                }
                return this;
        },
 
-       // @method isTooltipOpen(): boolean
-       // Returns `true` if the tooltip bound to this layer is currently open.
-       isTooltipOpen: function () {
-               return this._tooltip.isOpen();
+       // @method isPopupOpen(): boolean
+       // Returns `true` if the popup bound to this layer is currently open.
+       isPopupOpen: function () {
+               return this._popup.isOpen();
        },
 
-       // @method setTooltipContent(content: String|HTMLElement|Tooltip): this
-       // Sets the content of the tooltip bound to this layer.
-       setTooltipContent: function (content) {
-               if (this._tooltip) {
-                       this._tooltip.setContent(content);
+       // @method setPopupContent(content: String|HTMLElement|Popup): this
+       // Sets the content of the popup bound to this layer.
+       setPopupContent: function (content) {
+               if (this._popup) {
+                       this._popup.setContent(content);
                }
                return this;
        },
 
-       // @method getTooltip(): Tooltip
-       // Returns the tooltip bound to this layer.
-       getTooltip: function () {
-               return this._tooltip;
+       // @method getPopup(): Popup
+       // Returns the popup bound to this layer.
+       getPopup: function () {
+               return this._popup;
        },
 
-       _openTooltip: function (e) {
+       _openPopup: function (e) {
                var layer = e.layer || e.target;
 
-               if (!this._tooltip || !this._map) {
+               if (!this._popup) {
                        return;
                }
-               this.openTooltip(layer, this._tooltip.options.sticky ? e.latlng : undefined);
-       },
 
-       _moveTooltip: function (e) {
-               var latlng = e.latlng, containerPoint, layerPoint;
-               if (this._tooltip.options.sticky && e.originalEvent) {
-                       containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
-                       layerPoint = this._map.containerPointToLayerPoint(containerPoint);
-                       latlng = this._map.layerPointToLatLng(layerPoint);
+               if (!this._map) {
+                       return;
                }
-               this._tooltip.setLatLng(latlng);
-       }
-});
-
 
+               // prevent map click
+               L.DomEvent.stop(e);
 
-/*
- * Tooltip extension to L.Marker, adding tooltip-related methods.
- */
+               // if this inherits from Path its a vector and we can just
+               // open the popup at the new location
+               if (layer instanceof L.Path) {
+                       this.openPopup(e.layer || e.target, e.latlng);
+                       return;
+               }
 
-L.Marker.include({
-       _getTooltipAnchor: function () {
-               return this.options.icon.options.tooltipAnchor || [0, 0];
+               // otherwise treat it like a marker and figure out
+               // if we should toggle it open/closed
+               if (this._map.hasLayer(this._popup) && this._popup._source === layer) {
+                       this.closePopup();
+               } else {
+                       this.openPopup(layer, e.latlng);
+               }
+       },
+
+       _movePopup: function (e) {
+               this._popup.setLatLng(e.latlng);
        }
 });
 
 
 
 /*
- * @class LayerGroup
- * @aka L.LayerGroup
- * @inherits Layer
- *
- * Used to group several layers and handle them as one. If you add it to the map,
- * any layers added or removed from the group will be added/removed on the map as
- * well. Extends `Layer`.
+ * @class Tooltip
+ * @inherits DivOverlay
+ * @aka L.Tooltip
+ * Used to display small texts on top of map layers.
  *
  * @example
  *
  * ```js
- * L.layerGroup([marker1, marker2])
- *     .addLayer(polyline)
- *     .addTo(map);
+ * marker.bindTooltip("my tooltip text").openTooltip();
  * ```
+ * Note about tooltip offset. Leaflet takes two options in consideration
+ * for computing tooltip offseting:
+ * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
+ *   Add a positive x offset to move the tooltip to the right, and a positive y offset to
+ *   move it to the bottom. Negatives will move to the left and top.
+ * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
+ *   should adapt this value if you use a custom icon.
  */
 
-L.LayerGroup = L.Layer.extend({
 
-       initialize: function (layers) {
-               this._layers = {};
+// @namespace Tooltip
+L.Tooltip = L.DivOverlay.extend({
 
-               var i, len;
+       // @section
+       // @aka Tooltip options
+       options: {
+               // @option pane: String = 'tooltipPane'
+               // `Map pane` where the tooltip will be added.
+               pane: 'tooltipPane',
 
-               if (layers) {
-                       for (i = 0, len = layers.length; i < len; i++) {
-                               this.addLayer(layers[i]);
-                       }
-               }
-       },
+               // @option offset: Point = Point(0, 0)
+               // Optional offset of the tooltip position.
+               offset: [0, 0],
 
-       // @method addLayer(layer: Layer): this
-       // Adds the given layer to the group.
-       addLayer: function (layer) {
-               var id = this.getLayerId(layer);
+               // @option direction: String = 'auto'
+               // Direction where to open the tooltip. Possible values are: `right`, `left`,
+               // `top`, `bottom`, `center`, `auto`.
+               // `auto` will dynamicaly switch between `right` and `left` according to the tooltip
+               // position on the map.
+               direction: 'auto',
 
-               this._layers[id] = layer;
+               // @option permanent: Boolean = false
+               // Whether to open the tooltip permanently or only on mouseover.
+               permanent: false,
 
-               if (this._map) {
-                       this._map.addLayer(layer);
-               }
+               // @option sticky: Boolean = false
+               // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
+               sticky: false,
 
-               return this;
-       },
+               // @option interactive: Boolean = false
+               // If true, the tooltip will listen to the feature events.
+               interactive: false,
 
-       // @method removeLayer(layer: Layer): this
-       // Removes the given layer from the group.
-       // @alternative
-       // @method removeLayer(id: Number): this
-       // Removes the layer with the given internal ID from the group.
-       removeLayer: function (layer) {
-               var id = layer in this._layers ? layer : this.getLayerId(layer);
+               // @option opacity: Number = 0.9
+               // Tooltip container opacity.
+               opacity: 0.9
+       },
 
-               if (this._map && this._layers[id]) {
-                       this._map.removeLayer(this._layers[id]);
-               }
+       onAdd: function (map) {
+               L.DivOverlay.prototype.onAdd.call(this, map);
+               this.setOpacity(this.options.opacity);
 
-               delete this._layers[id];
+               // @namespace Map
+               // @section Tooltip events
+               // @event tooltipopen: TooltipEvent
+               // Fired when a tooltip is opened in the map.
+               map.fire('tooltipopen', {tooltip: this});
 
-               return this;
+               if (this._source) {
+                       // @namespace Layer
+                       // @section Tooltip events
+                       // @event tooltipopen: TooltipEvent
+                       // Fired when a tooltip bound to this layer is opened.
+                       this._source.fire('tooltipopen', {tooltip: this}, true);
+               }
        },
 
-       // @method hasLayer(layer: Layer): Boolean
-       // Returns `true` if the given layer is currently added to the group.
-       hasLayer: function (layer) {
-               return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers);
-       },
+       onRemove: function (map) {
+               L.DivOverlay.prototype.onRemove.call(this, map);
 
-       // @method clearLayers(): this
-       // Removes all the layers from the group.
-       clearLayers: function () {
-               for (var i in this._layers) {
-                       this.removeLayer(this._layers[i]);
+               // @namespace Map
+               // @section Tooltip events
+               // @event tooltipclose: TooltipEvent
+               // Fired when a tooltip in the map is closed.
+               map.fire('tooltipclose', {tooltip: this});
+
+               if (this._source) {
+                       // @namespace Layer
+                       // @section Tooltip events
+                       // @event tooltipclose: TooltipEvent
+                       // Fired when a tooltip bound to this layer is closed.
+                       this._source.fire('tooltipclose', {tooltip: this}, true);
                }
-               return this;
        },
 
-       // @method invoke(methodName: String, …): this
-       // Calls `methodName` on every layer contained in this group, passing any
-       // additional parameters. Has no effect if the layers contained do not
-       // implement `methodName`.
-       invoke: function (methodName) {
-               var args = Array.prototype.slice.call(arguments, 1),
-                   i, layer;
-
-               for (i in this._layers) {
-                       layer = this._layers[i];
+       getEvents: function () {
+               var events = L.DivOverlay.prototype.getEvents.call(this);
 
-                       if (layer[methodName]) {
-                               layer[methodName].apply(layer, args);
-                       }
+               if (L.Browser.touch && !this.options.permanent) {
+                       events.preclick = this._close;
                }
 
-               return this;
+               return events;
        },
 
-       onAdd: function (map) {
-               for (var i in this._layers) {
-                       map.addLayer(this._layers[i]);
+       _close: function () {
+               if (this._map) {
+                       this._map.closeTooltip(this);
                }
        },
 
-       onRemove: function (map) {
-               for (var i in this._layers) {
-                       map.removeLayer(this._layers[i]);
-               }
+       _initLayout: function () {
+               var prefix = 'leaflet-tooltip',
+                   className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
+
+               this._contentNode = this._container = L.DomUtil.create('div', className);
        },
 
-       // @method eachLayer(fn: Function, context?: Object): this
-       // Iterates over the layers of the group, optionally specifying context of the iterator function.
-       // ```js
-       // group.eachLayer(function (layer) {
-       //      layer.bindPopup('Hello');
-       // });
-       // ```
-       eachLayer: function (method, context) {
-               for (var i in this._layers) {
-                       method.call(context, this._layers[i]);
+       _updateLayout: function () {},
+
+       _adjustPan: function () {},
+
+       _setPosition: function (pos) {
+               var map = this._map,
+                   container = this._container,
+                   centerPoint = map.latLngToContainerPoint(map.getCenter()),
+                   tooltipPoint = map.layerPointToContainerPoint(pos),
+                   direction = this.options.direction,
+                   tooltipWidth = container.offsetWidth,
+                   tooltipHeight = container.offsetHeight,
+                   offset = L.point(this.options.offset),
+                   anchor = this._getAnchor();
+
+               if (direction === 'top') {
+                       pos = pos.add(L.point(-tooltipWidth / 2 + offset.x, -tooltipHeight + offset.y + anchor.y, true));
+               } else if (direction === 'bottom') {
+                       pos = pos.subtract(L.point(tooltipWidth / 2 - offset.x, -offset.y, true));
+               } else if (direction === 'center') {
+                       pos = pos.subtract(L.point(tooltipWidth / 2 + offset.x, tooltipHeight / 2 - anchor.y + offset.y, true));
+               } else if (direction === 'right' || direction === 'auto' && tooltipPoint.x < centerPoint.x) {
+                       direction = 'right';
+                       pos = pos.add(L.point(offset.x + anchor.x, anchor.y - tooltipHeight / 2 + offset.y, true));
+               } else {
+                       direction = 'left';
+                       pos = pos.subtract(L.point(tooltipWidth + anchor.x - offset.x, tooltipHeight / 2 - anchor.y - offset.y, true));
                }
-               return this;
+
+               L.DomUtil.removeClass(container, 'leaflet-tooltip-right');
+               L.DomUtil.removeClass(container, 'leaflet-tooltip-left');
+               L.DomUtil.removeClass(container, 'leaflet-tooltip-top');
+               L.DomUtil.removeClass(container, 'leaflet-tooltip-bottom');
+               L.DomUtil.addClass(container, 'leaflet-tooltip-' + direction);
+               L.DomUtil.setPosition(container, pos);
        },
 
-       // @method getLayer(id: Number): Layer
-       // Returns the layer with the given internal ID.
-       getLayer: function (id) {
-               return this._layers[id];
+       _updatePosition: function () {
+               var pos = this._map.latLngToLayerPoint(this._latlng);
+               this._setPosition(pos);
        },
 
-       // @method getLayers(): Layer[]
-       // Returns an array of all the layers added to the group.
-       getLayers: function () {
-               var layers = [];
+       setOpacity: function (opacity) {
+               this.options.opacity = opacity;
 
-               for (var i in this._layers) {
-                       layers.push(this._layers[i]);
+               if (this._container) {
+                       L.DomUtil.setOpacity(this._container, opacity);
                }
-               return layers;
        },
 
-       // @method setZIndex(zIndex: Number): this
-       // Calls `setZIndex` on every layer contained in this group, passing the z-index.
-       setZIndex: function (zIndex) {
-               return this.invoke('setZIndex', zIndex);
+       _animateZoom: function (e) {
+               var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
+               this._setPosition(pos);
        },
 
-       // @method getLayerId(layer: Layer): Number
-       // Returns the internal ID for a layer
-       getLayerId: function (layer) {
-               return L.stamp(layer);
+       _getAnchor: function () {
+               // Where should we anchor the tooltip on the source layer?
+               return L.point(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
        }
-});
 
+});
 
-// @factory L.layerGroup(layers: Layer[])
-// Create a layer group, optionally given an initial set of layers.
-L.layerGroup = function (layers) {
-       return new L.LayerGroup(layers);
+// @namespace Tooltip
+// @factory L.tooltip(options?: Tooltip options, source?: Layer)
+// Instantiates a Tooltip object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers.
+L.tooltip = function (options, source) {
+       return new L.Tooltip(options, source);
 };
 
+// @namespace Map
+// @section Methods for Layers and Controls
+L.Map.include({
 
+       // @method openTooltip(tooltip: Tooltip): this
+       // Opens the specified tooltip.
+       // @alternative
+       // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
+       // Creates a tooltip with the specified content and options and open it.
+       openTooltip: function (tooltip, latlng, options) {
+               if (!(tooltip instanceof L.Tooltip)) {
+                       tooltip = new L.Tooltip(options).setContent(tooltip);
+               }
+
+               if (latlng) {
+                       tooltip.setLatLng(latlng);
+               }
+
+               if (this.hasLayer(tooltip)) {
+                       return this;
+               }
+
+               return this.addLayer(tooltip);
+       },
+
+       // @method closeTooltip(tooltip?: Tooltip): this
+       // Closes the tooltip given as parameter.
+       closeTooltip: function (tooltip) {
+               if (tooltip) {
+                       this.removeLayer(tooltip);
+               }
+               return this;
+       }
+
+});
 
 /*
- * @class FeatureGroup
- * @aka L.FeatureGroup
- * @inherits LayerGroup
- *
- * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers:
- *  * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip))
- *  * Events are propagated to the `FeatureGroup`, so if the group has an event
- * handler, it will handle events from any of the layers. This includes mouse events
- * and custom events.
- *  * Has `layeradd` and `layerremove` events
+ * @namespace Layer
+ * @section Tooltip methods example
  *
- * @example
+ * All layers share a set of methods convenient for binding tooltips to it.
  *
  * ```js
- * L.featureGroup([marker1, marker2, polyline])
- *     .bindPopup('Hello world!')
- *     .on('click', function() { alert('Clicked on a member of the group!'); })
- *     .addTo(map);
+ * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
+ * layer.openTooltip();
+ * layer.closeTooltip();
  * ```
  */
 
-L.FeatureGroup = L.LayerGroup.extend({
-
-       addLayer: function (layer) {
-               if (this.hasLayer(layer)) {
-                       return this;
-               }
-
-               layer.addEventParent(this);
+// @section Tooltip methods
+L.Layer.include({
 
-               L.LayerGroup.prototype.addLayer.call(this, layer);
+       // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
+       // Binds a tooltip to the layer with the passed `content` and sets up the
+       // neccessary event listeners. If a `Function` is passed it will receive
+       // the layer as the first argument and should return a `String` or `HTMLElement`.
+       bindTooltip: function (content, options) {
 
-               // @event layeradd: LayerEvent
-               // Fired when a layer is added to this `FeatureGroup`
-               return this.fire('layeradd', {layer: layer});
-       },
+               if (content instanceof L.Tooltip) {
+                       L.setOptions(content, options);
+                       this._tooltip = content;
+                       content._source = this;
+               } else {
+                       if (!this._tooltip || options) {
+                               this._tooltip = L.tooltip(options, this);
+                       }
+                       this._tooltip.setContent(content);
 
-       removeLayer: function (layer) {
-               if (!this.hasLayer(layer)) {
-                       return this;
                }
-               if (layer in this._layers) {
-                       layer = this._layers[layer];
-               }
-
-               layer.removeEventParent(this);
 
-               L.LayerGroup.prototype.removeLayer.call(this, layer);
+               this._initTooltipInteractions();
 
-               // @event layerremove: LayerEvent
-               // Fired when a layer is removed from this `FeatureGroup`
-               return this.fire('layerremove', {layer: layer});
-       },
+               if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
+                       this.openTooltip();
+               }
 
-       // @method setStyle(style: Path options): this
-       // Sets the given path options to each layer of the group that has a `setStyle` method.
-       setStyle: function (style) {
-               return this.invoke('setStyle', style);
+               return this;
        },
 
-       // @method bringToFront(): this
-       // Brings the layer group to the top of all other layers
-       bringToFront: function () {
-               return this.invoke('bringToFront');
+       // @method unbindTooltip(): this
+       // Removes the tooltip previously bound with `bindTooltip`.
+       unbindTooltip: function () {
+               if (this._tooltip) {
+                       this._initTooltipInteractions(true);
+                       this.closeTooltip();
+                       this._tooltip = null;
+               }
+               return this;
        },
 
-       // @method bringToBack(): this
-       // Brings the layer group to the top of all other layers
-       bringToBack: function () {
-               return this.invoke('bringToBack');
+       _initTooltipInteractions: function (remove) {
+               if (!remove && this._tooltipHandlersAdded) { return; }
+               var onOff = remove ? 'off' : 'on',
+                   events = {
+                       remove: this.closeTooltip,
+                       move: this._moveTooltip
+                   };
+               if (!this._tooltip.options.permanent) {
+                       events.mouseover = this._openTooltip;
+                       events.mouseout = this.closeTooltip;
+                       if (this._tooltip.options.sticky) {
+                               events.mousemove = this._moveTooltip;
+                       }
+                       if (L.Browser.touch) {
+                               events.click = this._openTooltip;
+                       }
+               } else {
+                       events.add = this._openTooltip;
+               }
+               this[onOff](events);
+               this._tooltipHandlersAdded = !remove;
        },
 
-       // @method getBounds(): LatLngBounds
-       // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children).
-       getBounds: function () {
-               var bounds = new L.LatLngBounds();
+       // @method openTooltip(latlng?: LatLng): this
+       // Opens the bound tooltip at the specificed `latlng` or at the default tooltip anchor if no `latlng` is passed.
+       openTooltip: function (layer, latlng) {
+               if (!(layer instanceof L.Layer)) {
+                       latlng = layer;
+                       layer = this;
+               }
 
-               for (var id in this._layers) {
-                       var layer = this._layers[id];
-                       bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
+               if (layer instanceof L.FeatureGroup) {
+                       for (var id in this._layers) {
+                               layer = this._layers[id];
+                               break;
+                       }
                }
-               return bounds;
-       }
-});
 
-// @factory L.featureGroup(layers: Layer[])
-// Create a feature group, optionally given an initial set of layers.
-L.featureGroup = function (layers) {
-       return new L.FeatureGroup(layers);
-};
+               if (!latlng) {
+                       latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
+               }
 
+               if (this._tooltip && this._map) {
 
+                       // set tooltip source to this layer
+                       this._tooltip._source = layer;
 
-/*
- * @class Renderer
- * @inherits Layer
- * @aka L.Renderer
- *
- * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
- * DOM container of the renderer, its bounds, and its zoom animation.
- *
- * A `Renderer` works as an implicit layer group for all `Path`s - the renderer
- * itself can be added or removed to the map. All paths use a renderer, which can
- * be implicit (the map will decide the type of renderer and use it automatically)
- * or explicit (using the [`renderer`](#path-renderer) option of the path).
- *
- * Do not use this class directly, use `SVG` and `Canvas` instead.
- *
- * @event update: Event
- * Fired when the renderer updates its bounds, center and zoom, for example when
- * its map has moved
- */
+                       // update the tooltip (content, layout, ect...)
+                       this._tooltip.update();
 
-L.Renderer = L.Layer.extend({
+                       // open the tooltip on the map
+                       this._map.openTooltip(this._tooltip, latlng);
 
-       // @section
-       // @aka Renderer options
-       options: {
-               // @option padding: Number = 0.1
-               // How much to extend the clip area around the map view (relative to its size)
-               // e.g. 0.1 would be 10% of map view in each direction
-               padding: 0.1
-       },
+                       // Tooltip container may not be defined if not permanent and never
+                       // opened.
+                       if (this._tooltip.options.interactive && this._tooltip._container) {
+                               L.DomUtil.addClass(this._tooltip._container, 'leaflet-clickable');
+                               this.addInteractiveTarget(this._tooltip._container);
+                       }
+               }
 
-       initialize: function (options) {
-               L.setOptions(this, options);
-               L.stamp(this);
+               return this;
        },
 
-       onAdd: function () {
-               if (!this._container) {
-                       this._initContainer(); // defined by renderer implementations
-
-                       if (this._zoomAnimated) {
-                               L.DomUtil.addClass(this._container, 'leaflet-zoom-animated');
+       // @method closeTooltip(): this
+       // Closes the tooltip bound to this layer if it is open.
+       closeTooltip: function () {
+               if (this._tooltip) {
+                       this._tooltip._close();
+                       if (this._tooltip.options.interactive && this._tooltip._container) {
+                               L.DomUtil.removeClass(this._tooltip._container, 'leaflet-clickable');
+                               this.removeInteractiveTarget(this._tooltip._container);
                        }
                }
-
-               this.getPane().appendChild(this._container);
-               this._update();
+               return this;
        },
 
-       onRemove: function () {
-               L.DomUtil.remove(this._container);
+       // @method toggleTooltip(): this
+       // Opens or closes the tooltip bound to this layer depending on its current state.
+       toggleTooltip: function (target) {
+               if (this._tooltip) {
+                       if (this._tooltip._map) {
+                               this.closeTooltip();
+                       } else {
+                               this.openTooltip(target);
+                       }
+               }
+               return this;
        },
 
-       getEvents: function () {
-               var events = {
-                       viewreset: this._reset,
-                       zoom: this._onZoom,
-                       moveend: this._update
-               };
-               if (this._zoomAnimated) {
-                       events.zoomanim = this._onAnimZoom;
-               }
-               return events;
-       },
-
-       _onAnimZoom: function (ev) {
-               this._updateTransform(ev.center, ev.zoom);
-       },
-
-       _onZoom: function () {
-               this._updateTransform(this._map.getCenter(), this._map.getZoom());
-       },
-
-       _updateTransform: function (center, zoom) {
-               var scale = this._map.getZoomScale(zoom, this._zoom),
-                   position = L.DomUtil.getPosition(this._container),
-                   viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
-                   currentCenterPoint = this._map.project(this._center, zoom),
-                   destCenterPoint = this._map.project(center, zoom),
-                   centerOffset = destCenterPoint.subtract(currentCenterPoint),
-
-                   topLeftOffset = viewHalf.multiplyBy(-scale).add(position).add(viewHalf).subtract(centerOffset);
-
-               if (L.Browser.any3d) {
-                       L.DomUtil.setTransform(this._container, topLeftOffset, scale);
-               } else {
-                       L.DomUtil.setPosition(this._container, topLeftOffset);
-               }
-       },
-
-       _reset: function () {
-               this._update();
-               this._updateTransform(this._center, this._zoom);
-       },
-
-       _update: function () {
-               // Update pixel bounds of renderer container (for positioning/sizing/clipping later)
-               // Subclasses are responsible of firing the 'update' event.
-               var p = this.options.padding,
-                   size = this._map.getSize(),
-                   min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
-
-               this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
-
-               this._center = this._map.getCenter();
-               this._zoom = this._map.getZoom();
-       }
-});
-
-
-L.Map.include({
-       // @namespace Map; @method getRenderer(layer: Path): Renderer
-       // Returns the instance of `Renderer` that should be used to render the given
-       // `Path`. It will ensure that the `renderer` options of the map and paths
-       // are respected, and that the renderers do exist on the map.
-       getRenderer: function (layer) {
-               // @namespace Path; @option renderer: Renderer
-               // Use this specific instance of `Renderer` for this path. Takes
-               // precedence over the map's [default renderer](#map-renderer).
-               var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
-
-               if (!renderer) {
-                       // @namespace Map; @option preferCanvas: Boolean = false
-                       // Whether `Path`s should be rendered on a `Canvas` renderer.
-                       // By default, all `Path`s are rendered in a `SVG` renderer.
-                       renderer = this._renderer = (this.options.preferCanvas && L.canvas()) || L.svg();
-               }
-
-               if (!this.hasLayer(renderer)) {
-                       this.addLayer(renderer);
-               }
-               return renderer;
-       },
-
-       _getPaneRenderer: function (name) {
-               if (name === 'overlayPane' || name === undefined) {
-                       return false;
-               }
-
-               var renderer = this._paneRenderers[name];
-               if (renderer === undefined) {
-                       renderer = (L.SVG && L.svg({pane: name})) || (L.Canvas && L.canvas({pane: name}));
-                       this._paneRenderers[name] = renderer;
-               }
-               return renderer;
-       }
-});
-
-
-
-/*
- * @class Path
- * @aka L.Path
- * @inherits Interactive layer
- *
- * An abstract class that contains options and constants shared between vector
- * overlays (Polygon, Polyline, Circle). Do not use it directly