[Pkg-osm-commits] [SCM] leaflet branch, upstream, updated. upstream/0.2.1-1-g3c26a35

Andrew Harvey andrew.harvey4 at gmail.com
Tue Feb 14 10:17:14 UTC 2012


The following commit has been merged in the upstream branch:
commit 3c26a353049ec8c63919d26dda5ee0b481f02de4
Author: Andrew Harvey <andrew.harvey4 at gmail.com>
Date:   Tue Feb 14 08:50:34 2012 +1100

    Imported Upstream version 0.3

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49ebfe2..df4074e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,121 @@
 Leaflet Changelog
 =================
 
+(all changes without author notice are by [@mourner](https://github.com/mourner))
+
 ## 0.3 (master)
 
+### Major features
+
+ * Added **Canvas backend** for vector layers (polylines, polygons, circles). This enables vector support on Android < 3, and it can also be optionally preferred over SVG for a performance gain in some cases. Thanks to [@florianf](https://github.com/florianf) for a big part of this work.
+ * Added **layers control** (`Control.Layers`) for convenient layer switching.
+ * Added ability to set **max bounds** within which users can pan/zoom. [#93](https://github.com/CloudMade/Leaflet/issues/93)
+
+### Improvements
+
+#### Usability improvements
+
+ * Map now preserves its center after resize.
+ * When panning to another copy of the world (that's infinite horizontally), map overlays now jump to corresponding positions. [#273](https://github.com/CloudMade/Leaflet/issues/273)
+ * Limited maximum zoom change on a single mouse wheel movement (so you won't zoom across the whole zoom range in one scroll). [#149](https://github.com/CloudMade/Leaflet/issues/149)
+ * Significantly improved line simplification performance (noticeable when rendering polylines/polygons with tens of thousands of points)
+ * Improved circles performance by not drawing them if they're off the clip region.
+
+#### API improvements
+
+ * Added ability to add a tile layer below all others (`map.addLayer(layer, true)`) (useful for switching base tile layers).
+ * Added `Map` `zoomstart` event (thanks to [@Fabiz](https://github.com/Fabiz)). [#377](https://github.com/CloudMade/Leaflet/pull/377)
+ * Improved `Map` `locate` method, added ability to watch location continuously and more options. [#212](https://github.com/CloudMade/Leaflet/issues/212)
+ * Added second argument `inside` to `Map` `getBoundsZoom` method that allows you to get appropriate zoom for the view to fit *inside* the given bounds.
+ * Added `hasLayer` method to `Map`.
+ * Added `Marker` `zIndexOffset` option to be able to set certain markers below/above others. [#65](https://github.com/CloudMade/Leaflet/issues/65)
+ * Added `urlParams` third optional argument to `TileLayer` constructor for convenience: an object with properties that will be evaluated in the URL template.
+ * Added `TileLayer` `continuousWorld` option to disable tile coordinates checking/wrapping.
+ * Added `TileLayer` `tileunload` event fired when tile gets removed after panning (by [@CodeJosch](https://github.com/CodeJosch)). [#256](https://github.com/CloudMade/Leaflet/pull/256)
+ * Added `TileLayer` `zoomOffset` option useful for non-256px tiles (by [@msaspence](https://github.com/msaspence)).
+ * Added `TileLayer` `zoomReverse` option to reverse zoom numbering (by [@Majiir](https://github.com/Majiir)). [#406](https://github.com/CloudMade/Leaflet/pull/406)
+ * Added `TileLayer.Canvas` `redraw` method (by [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)). [#459](https://github.com/CloudMade/Leaflet/pull/459)
+ * Added `Polyline` `closestLayerPoint` method that's can be useful for interaction features (by [@anru](https://github.com/anru)). [#186](https://github.com/CloudMade/Leaflet/pull/186)
+ * Added `setLatLngs` method to `MultiPolyline` and `MultiPolygon` (by [@anru](https://github.com/anru)). [#194](https://github.com/CloudMade/Leaflet/pull/194)
+ * Added `getBounds` method to `Polyline` and `Polygon` (by [@JasonSanford](https://github.com/JasonSanford)). [#253](https://github.com/CloudMade/Leaflet/pull/253)
+ * Added `FeatureGroup` `setStyle` method (also inherited by `MultiPolyline` and `MultiPolygon`). [#353](https://github.com/CloudMade/Leaflet/issues/353)
+ * Added `FeatureGroup` `invoke` method to call a particular method on all layers of the group with the given arguments.
+ * Added `ImageOverlay` `load` event. [#213](https://github.com/CloudMade/Leaflet/issues/213)
+ * Added `minWidth` option to `Popup` (by [@marphi](https://github.com/marphi)). [#214](https://github.com/CloudMade/Leaflet/pull/214)
+ * Improved `LatLng` constructor to be more tolerant (and throw descriptive error if latitude or longitude can't be interpreted as a number). [#136](https://github.com/CloudMade/Leaflet/issues/136)
+ * Added `LatLng` `distanceTo` method (great circle distance) (by [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)). [#462](https://github.com/CloudMade/Leaflet/pull/462)
+ * Added `LatLngBounds` `toBBoxString` method for convenience (by [@JasonSanford](https://github.com/JasonSanford)). [#263](https://github.com/CloudMade/Leaflet/pull/263)
+ * Added `LatLngBounds` `intersects(otherBounds)` method (thanks to [@pagameba](https://github.com/pagameba)). [#350](https://github.com/CloudMade/Leaflet/pull/350)
+ * Added `Bounds` `intersects(otherBounds)` method. [#461](https://github.com/CloudMade/Leaflet/issues/461)
+ * Added `L.Util.template` method for simple string template evaluation.
+ * Added `DomUtil.removeClass` method (by [@anru](https://github.com/anru)).
+ * Added ability to pass empty imageUrl to icons for creating transparent clickable regions (by [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)). [#460](https://github.com/CloudMade/Leaflet/pull/460)
+ * Improved browser-specific code to rely more on feature detection rather than user agent string.
+ * Improved superclass access mechanism to work with inheritance chains of 3 or more classes; now you should use `Klass.superclass` instead of `this.superclass` (by [@anru](https://github.com/anru)). [#179](https://github.com/CloudMade/Leaflet/pull/179)
+
+#### Development workflow improvements
+
+ * Build system completely overhauled to be based on Node.js, Jake, JSHint and UglifyJS.
+ * All code is now linted for errors and conformity with a strict code style (with JSHint), and wont build unless the check passes.
+
+### Bugfixes
+
+#### General bugfixes
+
+ * Fixed a bug where `Circle` was rendered with incorrect radius (didn't take projection exagerration into account). [#331](https://github.com/CloudMade/Leaflet/issues/331)
+ * Fixed a bug where `Map` `getBounds` would work incorrectly on a date line cross. [#295](https://github.com/CloudMade/Leaflet/issues/295)
+ * Fixed a bug where polygons and polylines sometimes rendered incorrectly on some zoom levels. [#381](https://github.com/CloudMade/Leaflet/issues/381)
+ * Fixed a bug where fast mouse wheel zoom worked incorrectly when approaching min/max zoom values.
+ * Fixed a bug where `GeoJSON` `pointToLayer` option wouldn't work in a `GeometryCollection`. [#391](https://github.com/CloudMade/Leaflet/issues/391)
+ * Fixed a bug with incorrect rendering of GeoJSON on a date line cross. [#354](https://github.com/CloudMade/Leaflet/issues/354)
+ * Fixed a bug where map panning would stuck forever after releasing the mouse over an iframe or a flash object (thanks to [@sten82](https://github.com/sten82)). [#297](https://github.com/CloudMade/Leaflet/pull/297) [#64](https://github.com/CloudMade/Leaflet/issues/64)
+ * Fixed a bug where mouse wheel zoom worked incorrectly if map is inside scrolled container (partially by [@chrillo](https://github.com/chrillo)). [#206](https://github.com/CloudMade/Leaflet/issues/206)
+ * Fixed a bug where it was possible to add the same listener twice. [#281](https://github.com/CloudMade/Leaflet/issues/281)
+ * Fixed a bug where `Circle` was rendered with incorrect radius (didn't take projection exaggeration into account). [#331](https://github.com/CloudMade/Leaflet/issues/331)
+ * Fixed a bug where `Marker` `setIcon` was not working properly (by [@marphi](https://github.com/marphi)). [#218](https://github.com/CloudMade/Leaflet/pull/218) [#311](https://github.com/CloudMade/Leaflet/issues/311)
+ * Fixed a bug where `Marker` `setLatLng` was not working if it's set before adding the marker to a map. [#222](https://github.com/CloudMade/Leaflet/issues/222)
+ * Fixed a bug where marker popup would not move on `Marker` `setLatLng` (by [@tjarratt](https://github.com/tjarratt)). [#272](https://github.com/CloudMade/Leaflet/pull/272)
+ * Fixed a bug where static properties of a child class would not override the parent ones.
+ * Fixed broken popup `closePopup` option (by [@jgerigmeyer](https://github.com/jgerigmeyer)).
+ * Fixed a bug that caused en error when dragging marker with icon without shadow (by [@anru](https://github.com/anru)). [#178](https://github.com/CloudMade/Leaflet/issues/178)
+ * Fixed a typo in `Bounds` `contains` method (by [@anru](https://github.com/anru)). [#180](https://github.com/CloudMade/Leaflet/pull/180)
+ * Fixed a bug where creating an empty `Polygon` with `new L.Polygon()` would raise an error.
+ * Fixed a bug where drag event fired before the actual movement of layer (by [@anru](https://github.com/anru)). [#197](https://github.com/CloudMade/Leaflet/pull/197)
+ * Fixed a bug where map click caused an error if dragging is initially disabled. [#196](https://github.com/CloudMade/Leaflet/issues/196)
+ * Fixed a bug where map `movestart` event would fire after zoom animation.
+ * Fixed a bug where attribution prefix would not update on `setPrefix`. [#195](https://github.com/CloudMade/Leaflet/issues/195)
+ * Fixed a bug where `TileLayer` `load` event wouldn't fire in some edge cases (by [@dravnic](https://github.com/dravnic)).
+ * Fixed a bug related to clearing background tiles after zooming (by [@neno-giscloud](https://github.com/neno-giscloud) & [@dravnic](https://github.com/dravnic)).
+ * Fixed a bug that sometimes caused map flickering after zoom animation.
+ * Fixed a bug related to cleaning up after removing tile layers (by [@dravnic](https://github.com/dravnic)). [#276](https://github.com/CloudMade/Leaflet/pull/276)
+ * Fixed a bug that made selecting text in the attribution control impossible. [#279](https://github.com/CloudMade/Leaflet/issues/279)
+ * Fixed a bug when initializing a map in a non-empty div. [#278](https://github.com/CloudMade/Leaflet/issues/278)
+ * Fixed a bug where `movestart` didn't fire on panning animation.
+ * Fixed a bug in Elliptical Mercator formula that affeted `EPSG:3395` CRS (by [@Savvkin](https://github.com/Savvkin)). [#358](https://github.com/CloudMade/Leaflet/pull/358)
+
+#### Browser bugfixes
+
+ * Fixed occasional crashes on Mac Safari (thanks to [@lapinos03](https://github.com/lapinos03)). [#191](https://github.com/CloudMade/Leaflet/issues/191)
+ * Fixed a bug where resizing the map would sometimes make it blurry on WebKit (by [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)). [#453](https://github.com/CloudMade/Leaflet/pull/453)
+ * Fixed a bug that raised error in IE6-8 when clicking on popup close button. [#235](https://github.com/CloudMade/Leaflet/issues/235)
+ * Fixed a bug with Safari not redrawing UI immediately after closing a popup. [#296](https://github.com/CloudMade/Leaflet/issues/296)
+ * Fixed a bug that caused performance drop and high CPU usage when calling `setView` or `panTo` to the current center. [#231](https://github.com/CloudMade/Leaflet/issues/231)
+ * Fixed a bug that caused map overlays to appear blurry in some cases under WebKit browsers.
+ * Fixed a bug that was causing errors in some Webkit/Linux builds (requestAnimationFrame-related), thanks to Chris Martens.
+
+#### Mobile browser bugfixes
+
+ * Fixed a bug that caused an error when clicking vector layers under iOS. [#204](https://github.com/CloudMade/Leaflet/issues/204)
+ * Fixed crash on Android 3+ when panning or zooming (by [@florian](https://github.com/florianf)). [#137](https://github.com/CloudMade/Leaflet/issues/137)
+ * Fixed a bug on Android 2/3 that sometimes caused the map to disappear after zooming. [#69](https://github.com/CloudMade/Leaflet/issues/69)
+ * Fixed a bug on Android 3 that caused tiles to shift position on a big map.
+ * Fixed a bug that caused the map to pan when touch-panning inside a popup. [#452](https://github.com/CloudMade/Leaflet/issues/452)
+ * Fixed a bug that caused click delays on zoom control.
+
+
 ## 0.2.1 (2011-06-18)
 
- * Fixed regression that caused error in `TileLayer.Canvas`
+ * Fixed regression that caused error in `TileLayer.Canvas`.
 
 ## 0.2 (2011-06-17)
 
@@ -14,26 +124,26 @@ Leaflet Changelog
  * Added **WMS** support (`TileLayer.WMS` layer).
  * Added different **projections** support, having `EPSG:3857`, `EPSG:4326` and `EPSG:3395` out of the box (through `crs` option in `Map`). Thanks to [@Miroff](https://github.com/Miroff) & [@Komzpa](https://github.com/Komzpa) for great advice and explanation regarding this.
  * Added **GeoJSON** layer support.
- 
+
 ### Improvements
- 
+
 #### Usability improvements
- 
+
  * Improved panning performance in Chrome and FF considerably with the help of `requestAnimationFrame`. [#130](https://github.com/CloudMade/Leaflet/issues/130)
  * Improved click responsiveness in mobile WebKit (now it happens without delay). [#26](https://github.com/CloudMade/Leaflet/issues/26)
  * Added tap tolerance (so click happens even if you moved your finger slighly when tapping).
  * Improved geolocation error handling: better error messages, explicit timeout, set world view on locateAndSetView failure. [#61](https://github.com/CloudMade/Leaflet/issues/61)
- 
+
 #### API improvements
 
- * Added **MultiPolyline** and **MultiPolygon** layers. [#77](https://github.com/CloudMade/Leaflet/issues/77) 
+ * Added **MultiPolyline** and **MultiPolygon** layers. [#77](https://github.com/CloudMade/Leaflet/issues/77)
  * Added **LayerGroup** and **FeatureGroup** layers for grouping other layers.
  * Added **TileLayer.Canvas** for easy creation of canvas-based tile layers.
  * Changed `Circle` to be zoom-dependent (with radius in meters); circle of a permanent size is now called `CircleMarker`.
  * Added `mouseover` and `mouseout` events to map, markers and paths; added map `mousemove` event.
  * Added `setLatLngs`, `spliceLatLngs`, `addLatLng`, `getLatLngs` methods to polylines and polygons.
  * Added `setLatLng` and `setRadius` methods to `Circle` and `CircleMarker`.
- * Improved `LatLngBounds contains` method to accept `LatLng` in addition to `LatLngBounds`, the same for `Bounds contains` and `Point` 
+ * Improved `LatLngBounds contains` method to accept `LatLng` in addition to `LatLngBounds`, the same for `Bounds contains` and `Point`
  * Improved `LatLngBounds` & `Bounds` to allow their instantiation without arguments (by [@snc](https://github.com/snc)).
  * Added TMS tile numbering support through `TileLayer` `scheme: 'tms'` option (by [@tmcw](https://github.com/tmcw)).
  * Added `TileLayer` `noWrap` option to disable wrapping `x` tile coordinate (by [@jasondavies](https://github.com/jasondavies)).
@@ -43,15 +153,15 @@ Leaflet Changelog
  * Added `maxZoom` argument to `map.locateAndSetView` method.
  * Added ability to pass Geolocation options to map `locate` and `locateAndSetView` methods (by [@JasonSanford](https://github.com/JasonSanford)).
  * Improved `Popup` to accept HTML elements in addition to strings as its content.
- 
+
 #### Development workflow improvements
- 
+
  * Added `Makefile` for building `leaflet.js` on non-Windows machines (by [@tmcw](https://github.com/tmcw)).
  * Improved `debug/leaflet-include.js` script to allow using it outside of `debug` folder (by [@antonj](https://github.com/antonj)).
  * Improved `L` definition to be compatible with CommonJS. [#122](https://github.com/CloudMade/Leaflet/issues/122)
- 
+
 ### Bug fixes
- 
+
 #### General bugfixes
 
  * Fixed a bug where zooming is broken if the map contains a polygon and you zoom to an area where it's not visible. [#47](https://github.com/CloudMade/Leaflet/issues/47)
@@ -59,16 +169,16 @@ Leaflet Changelog
  * Fixed a bug where marker that was added, removed and then added again would not appear on the map. [#66](https://github.com/CloudMade/Leaflet/issues/66)
  * Fixed a bug where tile layer that was added, removed and then added again would not appear on the map.
  * Fixed a bug where some tiles would not load when panning across the date line. [#97](https://github.com/CloudMade/Leaflet/issues/97)
- * Fixed a bug where map div with `position: absolute` is reset to `relative`. [#100](https://github.com/CloudMade/Leaflet/issues/100) 
+ * Fixed a bug where map div with `position: absolute` is reset to `relative`. [#100](https://github.com/CloudMade/Leaflet/issues/100)
  * Fixed a bug that caused an error when trying to add a marker without shadow in its icon.
  * Fixed a bug where popup content would not update on `setContent` call. [#94](https://github.com/CloudMade/Leaflet/issues/94)
  * Fixed a bug where double click zoom wouldn't work if popup is opened on map click
  * Fixed a bug with click propagation on popup close button. [#99](https://github.com/CloudMade/Leaflet/issues/99)
  * Fixed inability to remove ImageOverlay layer.
- 
+
 #### Browser bugfixes
- 
- * Fixed a bug where paths would not appear in IE8. 
+
+ * Fixed a bug where paths would not appear in IE8.
  * Fixed a bug where there were occasional slowdowns before zoom animation in WebKit. [#123](https://github.com/CloudMade/Leaflet/issues/123)
  * Fixed incorrect zoom animation & popup styling in Opera 11.11.
  * Fixed popup fade animation in Firefox and Opera.
@@ -85,4 +195,4 @@ Leaflet Changelog
 
 ## 0.1 (2011-05-13)
 
- * Initial Leaflet release.
+Initial Leaflet release.
diff --git a/Jakefile.js b/Jakefile.js
new file mode 100644
index 0000000..43eab8b
--- /dev/null
+++ b/Jakefile.js
@@ -0,0 +1,65 @@
+var build = require('./build/build.js'),
+	lint = require('./build/hint.js');
+
+var crlf = '\r\n',
+	COPYRIGHT = '/*' + crlf + ' Copyright (c) 2010-2011, CloudMade, Vladimir Agafonkin' + crlf +
+                ' Leaflet is a modern open-source JavaScript library for interactive maps.' + crlf +
+                ' http://leaflet.cloudmade.com' + crlf + '*/' + crlf;
+
+desc('Check Leaflet source for errors with JSHint');
+task('lint', function () {
+	var files = build.getFiles();
+	
+	console.log('Checking for JS errors...');
+	
+	var errorsFound = lint.jshint(files);
+	
+	if (errorsFound > 0) {
+		console.log(errorsFound + ' error(s) found.\n');
+		fail();
+	} else {
+		console.log('\tCheck passed');
+	}
+});
+
+desc('Combine and compress Leaflet source files');
+task('build', ['lint'], function (compsBase32, buildName) {
+	var pathPart = 'dist/leaflet' + (buildName ? '-' + buildName : ''),
+		srcPath = pathPart + '-src.js',
+		path = pathPart + '.js';
+
+	var files = build.getFiles(compsBase32);
+
+	console.log('Concatenating ' + files.length + ' files...');
+	var content = build.combineFiles(files);
+	
+	var oldSrc = build.load(srcPath),
+		newSrc = COPYRIGHT + content,
+		srcDelta = build.getSizeDelta(newSrc, oldSrc);
+		
+	console.log('\tUncompressed size: ' + newSrc.length + ' bytes (' + srcDelta + ')');
+		
+	if (newSrc === oldSrc) {
+		console.log('\tNo changes');
+	} else {
+		build.save(srcPath, newSrc);
+		console.log('\tSaved to ' + srcPath);
+	}
+	
+	console.log('Compressing...');
+
+	var oldCompressed = build.load(path),
+		newCompressed = COPYRIGHT + build.uglify(content),
+		delta = build.getSizeDelta(newCompressed, oldCompressed);
+		
+	console.log('\tCompressed size: ' + newCompressed.length + ' bytes (' + delta + ')');
+
+	if (newCompressed === oldCompressed) {
+		console.log('\tNo changes');
+	} else {
+		build.save(path, newCompressed);
+		console.log('\tSaved to ' + path);
+	}
+});
+
+task('default', ['build']);
\ No newline at end of file
diff --git a/README.md b/README.md
index b6caae9..db9238a 100644
--- a/README.md
+++ b/README.md
@@ -7,4 +7,26 @@ It is built from the ground up to work efficiently and smoothly on both platform
 Check out the website for more information: [leaflet.cloudmade.com](http://leaflet.cloudmade.com)
 
 ## Contributing to Leaflet
-Let's make the best open-source library for maps that can possibly exist! Please send your pull requests to [Vladimir Agafonkin](http://github.com/mourner) (Leaflet maintainer) - we'll be happy to accept your contributions! [List of Leaflet contributors](http://github.com/CloudMade/Leaflet/contributors)
\ No newline at end of file
+Let's make the best open-source library for maps that can possibly exist! 
+
+Contributing is simple: make the changes in your fork, make sure that Leaflet builds successfully (see below) and then create a pull request to [Vladimir Agafonkin](http://github.com/mourner) (Leaflet maintainer). Updates to Leaflet [documentation](http://leaflet.cloudmade.com/reference.html) and [examples](http://leaflet.cloudmade.com/examples.html) (located in the `gh-pages` branch) are really appreciated too.
+
+Here's [a list of the awesome people](http://github.com/CloudMade/Leaflet/contributors) that joined us already. Looking forward to _your_ contributions!
+
+## Building Leaflet
+Leaflet build system is powered by the Node.js platform and Jake, JSHint and UglifyJS libraries, which install easily and work well across all major platforms. Here are the steps to install it:
+
+ 1. [Download and install Node](http://nodejs.org)
+ 2. Run the following commands in the command line:
+ 
+ ```
+ npm install -g jake
+ npm install -g jshint
+ npm install -g uglify-js
+ ```
+
+Now that you have everything installed, run `jake` inside the Leaflet directory. This will check Leaflet source files for JavaScript errors and inconsistencies, and then combine and compress it to the `dist` folder.
+
+To make a custom build of the library with only the things you need, use the build helper (`build/build.html`) to choose the components (it figures out dependencies for you) and then run the command generated with it.
+
+If you add any new files to the Leaflet source, make sure to also add them to `build/deps.js` so that the build system knows about them. Happy coding!
\ No newline at end of file
diff --git a/build/Makefile b/build/Makefile
deleted file mode 100644
index 22d6548..0000000
--- a/build/Makefile
+++ /dev/null
@@ -1,65 +0,0 @@
-../dist/leaflet.js: Makefile
-	java -jar ../lib/closure-compiler/compiler.jar \
-	--js ../src/Leaflet.js \
-	--js ../src/core/Util.js \
-	--js ../src/core/Class.js \
-	--js ../src/core/Events.js \
-	--js ../src/core/Browser.js \
-	--js ../src/geometry/Point.js \
-	--js ../src/geometry/Bounds.js \
-	--js ../src/geometry/Transformation.js \
-	--js ../src/geometry/LineUtil.js \
-	--js ../src/geometry/PolyUtil.js \
-	--js ../src/dom/DomEvent.js \
-	--js ../src/dom/DomEvent.DoubleTap.js \
-	--js ../src/dom/DomUtil.js \
-	--js ../src/dom/Draggable.js \
-	--js ../src/dom/transition/Transition.js \
-	--js ../src/dom/transition/Transition.Native.js \
-	--js ../src/dom/transition/Transition.Timer.js \
-	--js ../src/geo/LatLng.js \
-	--js ../src/geo/LatLngBounds.js \
-	--js ../src/geo/projection/Projection.js \
-	--js ../src/geo/projection/Projection.SphericalMercator.js \
-	--js ../src/geo/projection/Projection.LonLat.js \
-	--js ../src/geo/projection/Projection.Mercator.js \
-	--js ../src/geo/crs/CRS.js \
-	--js ../src/geo/crs/CRS.EPSG3857.js \
-	--js ../src/geo/crs/CRS.EPSG4326.js \
-	--js ../src/geo/crs/CRS.EPSG3395.js \
-	--js ../src/layer/LayerGroup.js \
-	--js ../src/layer/FeatureGroup.js \
-	--js ../src/layer/tile/TileLayer.js \
-	--js ../src/layer/tile/TileLayer.WMS.js \
-	--js ../src/layer/tile/TileLayer.Canvas.js \
-	--js ../src/layer/ImageOverlay.js \
-	--js ../src/layer/Popup.js \
-	--js ../src/layer/marker/Icon.js \
-	--js ../src/layer/marker/Marker.js \
-	--js ../src/layer/marker/Marker.Popup.js \
-	--js ../src/layer/vector/Path.js \
-	--js ../src/layer/vector/Path.VML.js \
-	--js ../src/layer/vector/Path.Popup.js \
-	--js ../src/layer/vector/Polyline.js \
-	--js ../src/layer/vector/Polygon.js \
-	--js ../src/layer/vector/MultiPoly.js \
-	--js ../src/layer/vector/Circle.js \
-	--js ../src/layer/vector/CircleMarker.js \
-	--js ../src/layer/GeoJSON.js \
-	--js ../src/handler/Handler.js \
-	--js ../src/handler/MapDrag.js \
-	--js ../src/handler/TouchZoom.js \
-	--js ../src/handler/ScrollWheelZoom.js \
-	--js ../src/handler/DoubleClickZoom.js \
-	--js ../src/handler/ShiftDragZoom.js \
-	--js ../src/handler/MarkerDrag.js \
-	--js ../src/control/Control.js \
-	--js ../src/control/Control.Zoom.js \
-	--js ../src/control/Control.Attribution.js \
-	--js ../src/map/Map.js \
-	--js ../src/map/ext/Map.Geolocation.js \
-	--js ../src/map/ext/Map.Popup.js \
-	--js ../src/map/ext/Map.PanAnimation.js \
-	--js ../src/map/ext/Map.ZoomAnimation.js \
-	--js ../src/map/ext/Map.Control.js \
-	--js_output_file ../dist/leaflet.js
diff --git a/build/build.html b/build/build.html
index c60e5f3..b216002 100644
--- a/build/build.html
+++ b/build/build.html
@@ -2,9 +2,9 @@
 <html>
 <head>
 	<title>Leaflet Build Helper</title>
-	
+
 	<script type="text/javascript" src="deps.js"></script>
-	
+
 	<style type="text/css">
 		body {
 			font: 12px/1.4 Verdana, sans-serif;
@@ -14,22 +14,23 @@
 		#container {
 			text-align: left;
 			margin: 0 auto;
-			width: 600px;
+			width: 780px;
 		}
 		#deplist {
 			list-style: none;
 			padding: 0;
 		}
 		#deplist li {
-			padding-top: 10px;
-			padding-bottom: 10px;
-			border-top: 1px solid #eee;
+			padding-top: 7px;
+			padding-bottom: 7px;
+			border-bottom: 1px solid #ddd;
 		}
 		#deplist li.heading {
 			border: none;
-			background: #eee;
+			background: #ddd;
 			padding: 5px 10px;
-			margin-top: 10px;
+			margin-top: 25px;
+			border-radius: 5px;
 		}
 		#deplist input {
 			float: left;
@@ -38,7 +39,7 @@
 		}
 		#deplist label {
 			float: left;
-			width: 190px;
+			width: 160px;
 			font-weight: bold;
 		}
 		#deplist div {
@@ -46,38 +47,61 @@
 			height: 1%;
 		}
 		#deplist .desc {
-		} 
-		
+		}
+
 		#deplist .deps {
 			color: #777;
 		}
-		
+
 		#command {
 			width: 100%;
 		}
+		#command2 {
+			width: 200px;
+		}
+
+		#toolbar {
+			padding-bottom: 10px;
+			border-bottom: 1px solid #ddd;
+		}
+
+		h2 {
+			margin-top: 2em;
+		}
 	</style>
 </head>
 <body>
 	<div id="container">
 		<h1>Leaflet Build Helper</h1>
-		
-		<p>
-			<a id="select-all" href="#all">Select All</a> | 
+
+		<p id="toolbar">
+			<a id="select-all" href="#all">Select All</a> |
 			<a id="deselect-all" href="#none">Deselect All</a>
 		</p>
-		
+
 		<ul id="deplist"></ul>
-		
-		<p>
-			Run this command in the build directory:<br />
-			<input type="text" id="command" />
-		</p>
+
+		<h2>Building using Node and UglifyJS</h2>
+		<ol>
+			<li><a href="http://nodejs.org/#download">Download and install Node</a></li>
+			<li>Run this in the command line:<br />
+			<pre><code>npm install -g jake
+npm install -g jshint
+npm install -g uglify-js</code></pre></li>
+			<li>Run this command inside the Leaflet directory: <br /><input type="text" id="command2" />
+		</ol>
+		<h2>Building using Closure Compiler</h2>
+		<ol>
+			<li><a href="http://closure-compiler.googlecode.com/files/compiler-latest.zip">Download Closure Compiler</a> and extract it into <code>lib/closure-compiler</code> directory</li>
+			<li>Run this command in the root Leaflet directory: <br /><input type="text" id="command" /></li>
+		</ol>
 	</div>
-	
+
 	<script type="text/javascript">
 		var deplist = document.getElementById('deplist'),
-			commandInput = document.getElementById('command');
-		
+			commandInput = document.getElementById('command'),
+			commandInput2 = document.getElementById('command2');
+
 		document.getElementById('select-all').onclick = function() {
 			var checks = deplist.getElementsByTagName('input');
 			for (var i = 0; i < checks.length; i++) {
@@ -86,7 +110,7 @@
 			updateCommand();
 			return false;
 		};
-		
+
 		document.getElementById('deselect-all').onclick = function() {
 			var checks = deplist.getElementsByTagName('input');
 			for (var i = 0; i < checks.length; i++) {
@@ -97,42 +121,53 @@
 			updateCommand();
 			return false;
 		};
-		
+
 		function updateCommand() {
 			var files = {};
 			var checks = deplist.getElementsByTagName('input');
-			for (var i = 0; i < checks.length; i++) {
+			var compsStr = '';
+
+			for (var i = 0, len = checks.length; i < len; i++) {
 				if (checks[i].checked) {
 					var srcs = deps[checks[i].id].src;
-					for (var j = 0; j < srcs.length; j++) {
+					for (var j = 0, len2 = srcs.length; j < len2; j++) {
 						files[srcs[j]] = true;
 					}
+					compsStr = '1' + compsStr;
+				} else {
+					compsStr = '0' + compsStr;
 				}
 			}
-			
-			var command = 'java -jar ../lib/closure-compiler/compiler.jar ';
+
+			var command = 'java -jar lib/closure-compiler/compiler.jar ';
 			for (var src in files) {
-				command += '--js ../src/' + src + ' ';
+				command += '--js src/' + src + ' ';
 			}
-			command += '--js_output_file ../dist/leaflet-custom.js';
-			
+			command += '--js_output_file dist/leaflet-custom.js';
+
 			commandInput.value = command;
+
+			commandInput2.value = 'jake build[' + parseInt(compsStr, 2).toString(32) + ',custom]';
 		}
-		
-		commandInput.onclick = function() {
-			commandInput.focus();
-			commandInput.select();
+
+		function inputSelect() {
+			this.focus();
+			this.select();
 		};
 		
+		commandInput.onclick = inputSelect;
+		commandInput2.onclick = inputSelect;
+
 		function onCheckboxChange() {
 			if (this.checked) {
 				var depDeps = deps[this.id].deps;
-				if (!depDeps) { return; }
-				for (var i = 0; i < depDeps.length; i++) {
-					var check = document.getElementById(depDeps[i]);
-					if (!check.checked) {
-						check.checked = true;
-						check.onchange();
+				if (depDeps) {
+					for (var i = 0; i < depDeps.length; i++) {
+						var check = document.getElementById(depDeps[i]);
+						if (!check.checked) {
+							check.checked = true;
+							check.onchange();
+						}
 					}
 				}
 			} else {
@@ -141,7 +176,7 @@
 					var dep = deps[checks[i].id];
 					if (!dep.deps) { continue; }
 					for (var j = 0; j < dep.deps.length; j++) {
-						if (dep.deps[j] == this.id) {
+						if (dep.deps[j] === this.id) {
 							if (checks[i].checked) {
 								checks[i].checked = false;
 								checks[i].onchange();
@@ -152,17 +187,17 @@
 			}
 			updateCommand();
 		}
-	
+
 		for (var name in deps) {
 			var li = document.createElement('li');
-			
+
 			if (deps[name].heading) {
 				var heading = document.createElement('li');
 				heading.className = 'heading';
 				heading.appendChild(document.createTextNode(deps[name].heading));
 				deplist.appendChild(heading);
 			}
-			
+
 			var div = document.createElement('div');
 
 			var label = document.createElement('label');
@@ -172,17 +207,17 @@
 			check.id = name;
 			label.appendChild(check);
 			check.onchange = onCheckboxChange;
-			
+
 			if (name == 'Core') {
 				check.checked = true;
 				check.disabled = true;
 			}
-			
+
 			label.appendChild(document.createTextNode(name));
 			label.htmlFor = name;
-			
+
 			li.appendChild(label);
-			
+
 			var desc = document.createElement('span');
 			desc.className = 'desc';
 			desc.appendChild(document.createTextNode(deps[name].desc));
@@ -193,16 +228,16 @@
 				depspan.className = 'deps';
 				depspan.appendChild(document.createTextNode('Deps: ' + depText));
 			}
-			
+
 			div.appendChild(desc);
 			div.appendChild(document.createElement('br'));
 			if (depText) { div.appendChild(depspan); }
-			
+
 			li.appendChild(div);
-			
+
 			deplist.appendChild(li);
 		}
 		updateCommand();
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/build/build.js b/build/build.js
new file mode 100644
index 0000000..68cb44b
--- /dev/null
+++ b/build/build.js
@@ -0,0 +1,79 @@
+var fs = require('fs'),
+	uglifyjs = require('uglify-js'),
+	deps = require('./deps.js').deps;
+
+exports.getFiles = function (compsBase32) {
+	var memo = {},
+		comps;
+
+	if (compsBase32) {
+		comps = parseInt(compsBase32, 32).toString(2).split('');
+		console.log('Managing dependencies...')
+	}
+
+	function addFiles(srcs) {
+		for (var j = 0, len = srcs.length; j < len; j++) {
+			memo[srcs[j]] = true;
+		}
+	}
+
+	for (var i in deps) {
+		if (comps) {
+			if (parseInt(comps.pop(), 2) === 1) {
+				console.log('\t* ' + i);
+				addFiles(deps[i].src);
+			} else {
+				console.log('\t  ' + i);
+			}
+		} else {
+			addFiles(deps[i].src);
+		}
+	}
+
+	var files = [];
+
+	for (var src in memo) {
+		files.push('src/' + src);
+	}
+
+	return files;
+};
+
+exports.uglify = function (code) {
+	var pro = uglifyjs.uglify;
+
+	var ast = uglifyjs.parser.parse(code);
+	ast = pro.ast_mangle(ast);
+	ast = pro.ast_squeeze(ast, {keep_comps: false});
+	ast = pro.ast_squeeze_more(ast);
+
+	return pro.gen_code(ast) + ';';
+};
+
+exports.combineFiles = function (files) {
+	var content = '';
+	for (var i = 0, len = files.length; i < len; i++) {
+		content += fs.readFileSync(files[i], 'utf8') + '\r\n\r\n';
+	}
+	return content;
+};
+
+exports.save = function (savePath, compressed) {
+	return fs.writeFileSync(savePath, compressed, 'utf8');
+};
+
+exports.load = function (loadPath) {
+	try {
+		return fs.readFileSync(loadPath, 'utf8');
+	} catch (e) {
+		return null;
+	}
+};
+
+exports.getSizeDelta = function (newContent, oldContent) {
+	if (!oldContent) {
+		return 'new';
+	}
+	var delta = newContent.length - oldContent.length;
+	return (delta >= 0 ? '+' : '') + delta;
+};
\ No newline at end of file
diff --git a/build/deps.js b/build/deps.js
index 4273564..e6c5ff0 100644
--- a/build/deps.js
+++ b/build/deps.js
@@ -1,10 +1,13 @@
 var deps = {
 	Core: {
 		src: ['Leaflet.js',
-		      'core/Browser.js', 
-		      'core/Class.js', 
-		      'core/Events.js', 
 		      'core/Util.js',
+		      'core/Class.js',
+		      'core/Events.js',
+		      'core/Browser.js',
+		      'geometry/Point.js',
+		      'geometry/Bounds.js',
+		      'geometry/Transformation.js',
 		      'dom/DomUtil.js',
 		      'geo/LatLng.js',
 		      'geo/LatLngBounds.js',
@@ -14,84 +17,87 @@ var deps = {
 		      'geo/crs/CRS.js',
 		      'geo/crs/CRS.EPSG3857.js',
 		      'geo/crs/CRS.EPSG4326.js',
-		      'geometry/Bounds.js',
-		      'geometry/Point.js',
-		      'geometry/Transformation.js',
 		      'map/Map.js'],
 		desc: 'The core of the library, including OOP, events, DOM facilities, basic units, projections (EPSG:3857 and EPSG:4326) and the base Map class.'
 	},
-	
-	
+
+
 	EPSG3395: {
 		src: ['geo/projection/Projection.Mercator.js',
 		      'geo/crs/CRS.EPSG3395.js'],
 		desc: 'EPSG:3395 projection (used by some map providers).',
 		heading: 'Additional projections'
 	},
-	
+
 	TileLayer: {
 		src: ['layer/tile/TileLayer.js'],
 		desc: 'The base class for displaying tile layers on the map.',
 		heading: 'Layers'
 	},
-	
+
 	TileLayerWMS: {
 		src: ['layer/tile/TileLayer.WMS.js'],
 		desc: 'WMS tile layer.',
 		deps: ['TileLayer']
 	},
-	
+
 	TileLayerCanvas: {
 		src: ['layer/tile/TileLayer.Canvas.js'],
 		desc: 'Tile layer made from canvases (for custom drawing purposes).',
 		deps: ['TileLayer']
 	},
-	
+
 	ImageOverlay: {
 		src: ['layer/ImageOverlay.js'],
 		desc: 'Used to display an image over a particular rectangular area of the map.'
 	},
-	
+
 	Marker: {
 		src: ['layer/marker/Icon.js', 'layer/marker/Marker.js'],
 		desc: 'Markers to put on the map.'
 	},
-	
+
 	Popup: {
 		src: ['layer/Popup.js', 'layer/marker/Marker.Popup.js', 'map/ext/Map.Popup.js'],
 		deps: ['Marker'],
 		desc: 'Used to display the map popup (used mostly for binding HTML data to markers and paths on click).'
 	},
-	
+
 	LayerGroup: {
 		src: ['layer/LayerGroup.js'],
 		desc: 'Allows grouping several layers to handle them as one.'
 	},
-	
+
 	FeatureGroup: {
 		src: ['layer/FeatureGroup.js'],
 		deps: ['LayerGroup', 'Popup'],
 		desc: 'Extends LayerGroup with mouse events and bindPopup method shared between layers.'
 	},
-	
-	
+
+
 	Path: {
-		src: ['layer/vector/Path.js', 'layer/vector/Path.Popup.js'],
+		src: ['layer/vector/Path.js', 'layer/vector/Path.SVG.js', 'layer/vector/Path.Popup.js'],
 		desc: 'Vector rendering core (SVG-powered), enables overlaying the map with SVG paths.',
 		heading: 'Vector layers'
 	},
-	
+
 	PathVML: {
 		src: ['layer/vector/Path.VML.js'],
 		desc: 'VML fallback for vector rendering core (IE 6-8).'
 	},
-	
+
+	PathCanvas: {
+		src: ['layer/vector/canvas/Path.Canvas.js'],
+		deps: ['Path', 'Polyline', 'Polygon', 'Circle'],
+		desc: 'Canvas fallback for vector rendering core (makes it work on Android 2+).'
+	},
+
 	Polyline: {
 		src: ['geometry/LineUtil.js', 'layer/vector/Polyline.js'],
 		deps: ['Path'],
 		desc: 'Polyline overlays.'
 	},
-	
+
 	Polygon: {
 		src: ['geometry/PolyUtil.js', 'layer/vector/Polygon.js'],
 		deps: ['Polyline'],
@@ -109,103 +115,122 @@ var deps = {
 		deps: ['Path'],
 		desc: 'Circle overlays (with radius in meters).'
 	},
-	
+
 	CircleMarker: {
 		src: ['layer/vector/CircleMarker.js'],
 		deps: ['Circle'],
 		desc: 'Circle overlays with a constant pixel radius.'
 	},
-	
+
+	VectorsCanvas: {
+		src: ['layer/vector/canvas/Polyline.Canvas.js',
+		      'layer/vector/canvas/Polygon.Canvas.js',
+		      'layer/vector/canvas/Circle.Canvas.js'],
+		deps: ['PathCanvas', 'Polyline', 'Polygon', 'Circle'],
+		desc: 'Canvas fallback for vector layers (polygons, polylines, circles)'
+	},
+
 	GeoJSON: {
 		src: ['layer/GeoJSON.js'],
 		deps: ['Marker', 'MultiPoly', 'FeatureGroup'],
 		desc: 'GeoJSON layer, parses the data and adds corresponding layers above.'
 	},
 
-	
+
 	MapDrag: {
 		src: ['dom/DomEvent.js',
 		      'dom/Draggable.js',
-		      'handler/Handler.js',
-		      'handler/MapDrag.js'],
+		      'core/Handler.js',
+		      'map/handler/Map.Drag.js'],
 		desc: 'Makes the map draggable (by mouse or touch).',
 		heading: 'Interaction'
 	},
-	
+
 	MouseZoom: {
 		src: ['dom/DomEvent.js',
-		      'handler/Handler.js',
-		      'handler/DoubleClickZoom.js',
-		      'handler/ScrollWheelZoom.js'],
+		      'core/Handler.js',
+		      'map/handler/Map.DoubleClickZoom.js',
+		      'map/handler/Map.ScrollWheelZoom.js'],
 		desc: 'Scroll wheel zoom and double click zoom on the map.'
 	},
-	
+
 	TouchZoom: {
 		src: ['dom/DomEvent.js',
 		      'dom/DomEvent.DoubleTap.js',
-		      'handler/Handler.js',
-		      'handler/TouchZoom.js'],
+		      'core/Handler.js',
+		      'map/handler/Map.TouchZoom.js'],
 		deps: ['MapAnimationZoom'],
 		desc: 'Enables smooth touch zooming on iOS and double tap on iOS/Android.'
 	},
-	
-	ShiftDragZoom: {
-		src: ['handler/ShiftDragZoom.js'],
+
+	BoxZoom: {
+		src: ['map/handler/Map.BoxZoom.js'],
 		desc: 'Enables zooming to bounding box by shift-dragging the map.'
 	},
-	
+
 	MarkerDrag: {
-		src: ['handler/MarkerDrag.js'],
+		src: ['layer/marker/Marker.Drag.js'],
 		desc: 'Makes markers draggable (by mouse or touch).'
 	},
-		
-	
+
+
 	ControlZoom: {
-		src: ['control/Control.js', 
-		      'map/ext/Map.Control.js', 
+		src: ['control/Control.js',
+		      'map/ext/Map.Control.js',
 		      'control/Control.Zoom.js'],
 		heading: 'Controls',
 		desc: 'Basic zoom control with two buttons (zoom in / zoom out).'
 	},
-	
-	ControlZoom: {
-		src: ['control/Control.js', 
-		      'map/ext/Map.Control.js', 
+
+	ControlAttrib: {
+		src: ['control/Control.js',
+		      'map/ext/Map.Control.js',
 		      'control/Control.Attribution.js'],
 		desc: 'Attribution control.'
 	},
-	
-	
-	MapAnimationNative: {
+
+	ControlLayers: {
+		src: ['control/Control.js',
+		      'map/ext/Map.Control.js',
+		      'control/Control.Layers.js'],
+		desc: 'Layer Switcher control.'
+	},
+
+
+	AnimationNative: {
 		src: ['dom/DomEvent.js',
 		      'dom/transition/Transition.js',
 		      'dom/transition/Transition.Native.js'],
 		desc: 'Animation core that uses CSS3 Transitions (for powering pan & zoom animations). Works on mobile webkit-powered browsers and some modern desktop browsers.',
 		heading: 'Visual effects'
 	},
-	
-	MapAnimationFallback: {
+
+	AnimationTimer: {
 		src: ['dom/transition/Transition.Timer.js'],
-		deps: ['MapAnimationNative'],
+		deps: ['AnimationNative'],
 		desc: 'Timer-based animation fallback for browsers that don\'t support CSS3 transitions.'
 	},
-	
-	MapAnimationPan: {
-		src: ['map/ext/Map.PanAnimation.js'],
-		deps: ['MapAnimationNative'],
+
+	AnimationPan: {
+		src: ['map/anim/Map.PanAnimation.js'],
+		deps: ['AnimationPan'],
 		desc: 'Panning animation. Can use both native and timer-based animation.'
 	},
-	
-	MapAnimationZoom: {
-		src: ['map/ext/Map.ZoomAnimation.js'],
-		deps: ['MapAnimationPan', 'MapAnimationNative'],
+
+	AnimationZoom: {
+		src: ['map/anim/Map.ZoomAnimation.js'],
+		deps: ['AnimationPan', 'AnimationNative'],
 		desc: 'Smooth zooming animation. So far it works only on browsers that support CSS3 Transitions.'
 	},
-	
-	
-	MapGeolocation: {
+
+
+	Geolocation: {
 		src: ['map/ext/Map.Geolocation.js'],
 		desc: 'Adds Map#locate method and related events to make geolocation easier.',
 		heading: 'Misc'
 	}
-};
\ No newline at end of file
+};
+
+if (typeof exports !== 'undefined') {
+	exports.deps = deps;
+}
diff --git a/build/hint.js b/build/hint.js
new file mode 100644
index 0000000..464bbe1
--- /dev/null
+++ b/build/hint.js
@@ -0,0 +1,30 @@
+var jshint = require('jshint').JSHINT,
+	fs = require('fs'),
+	config = require('./hintrc.js').config;
+
+function jshintSrc(path, src) {
+	jshint(src, config);
+	
+	var errors = jshint.errors,
+		i, len, e, line;
+	
+	for (i = 0, len = errors.length; i < len; i++) {
+		e = errors[i];
+		//console.log(e.evidence);
+		console.log(path + '\tline ' + e.line + '\tcol ' + e.character + '\t ' + e.reason);
+	}
+	
+	return len;
+}
+	
+exports.jshint = function (files) {
+	var errorsFound = 0;
+	
+	for (var i = 0, len = files.length; i < len; i++) {
+		var src = fs.readFileSync(files[i], 'utf8');
+		
+		errorsFound += jshintSrc(files[i], src);
+	}
+	
+	return errorsFound;
+};
\ No newline at end of file
diff --git a/build/hintrc.js b/build/hintrc.js
new file mode 100644
index 0000000..b9f37ac
--- /dev/null
+++ b/build/hintrc.js
@@ -0,0 +1,44 @@
+exports.config = {
+	"browser": true,
+	"predef": ["L"],
+
+	"debug": false,
+	"devel": false,
+
+	"es5": false,
+	"strict": false,
+	"globalstrict": false,
+
+	"asi": false,
+	"laxbreak": false,
+	"bitwise": true,
+	"boss": false,
+	"curly": true,
+	"eqnull": false,
+	"evil": false,
+	"expr": false,
+	"forin": false,
+	"immed": true,
+	"latedef": true,
+	"loopfunc": false,
+	"noarg": true,
+	"regexp": true,
+	"regexdash": false,
+	"scripturl": false,
+	"shadow": false,
+	"supernew": false,
+	"undef": true,
+
+	"newcap": true,
+	"noempty": true,
+	"nonew": true,
+	"nomen": false,
+	"onevar": false,
+	"plusplus": false,
+	"sub": false,
+	"indent": 4,
+
+	"eqeqeq": true,
+	"trailing": true,
+	"white": true
+};
\ No newline at end of file
diff --git a/debug/control/control-layers.html b/debug/control/control-layers.html
new file mode 100644
index 0000000..32d89c3
--- /dev/null
+++ b/debug/control/control-layers.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Leaflet debug page</title>
+
+	<link rel="stylesheet" href="../../dist/leaflet.css" />
+	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+	
+	<link rel="stylesheet" href="../css/screen.css" />
+	
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+
+	<div id="map"></div>
+
+	<script type="text/javascript">
+	
+		function getCloudMadeUrl(styleId) {
+			return 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/' + styleId + '/256/{z}/{x}/{y}.png';
+		}
+
+		var cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
+			cloudmade = new L.TileLayer(getCloudMadeUrl(997), {attribution: cloudmadeAttribution}),
+			cloudmade2 = new L.TileLayer(getCloudMadeUrl(998), {attribution: cloudmadeAttribution});
+		
+		var map = new L.Map('map').addLayer(cloudmade).setView(new L.LatLng(50.5, 30.51), 15);
+		
+		var marker = new L.Marker(new L.LatLng(50.5, 30.505));
+		map.addLayer(marker);
+		
+		var marker2 = new L.Marker(new L.LatLng(50.502, 30.515));
+		map.addLayer(marker2);
+		
+		var layersControl = new L.Control.Layers({
+			'CloudMade Fresh': cloudmade,
+			'CloudMade Pale Dawn': cloudmade2
+		}, {
+			'Some marker': marker,
+			'Another marker': marker2
+		});
+		
+		map.addControl(layersControl);
+		
+	</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/debug/control/map-control.html b/debug/control/map-control.html
index 8d52bc9..119d194 100644
--- a/debug/control/map-control.html
+++ b/debug/control/map-control.html
@@ -8,7 +8,7 @@
 	
 	<link rel="stylesheet" href="../css/screen.css" />
 	
-	<script src="../include.js"></script>
+	<script src="../leaflet-include.js"></script>
 </head>
 <body>
 
diff --git a/debug/geojson/geojson-sample.js b/debug/geojson/geojson-sample.js
index 24ecf05..16f6f57 100644
--- a/debug/geojson/geojson-sample.js
+++ b/debug/geojson/geojson-sample.js
@@ -12,7 +12,7 @@ var geojsonSample = {
 				"color": "blue"
 			}
 		},
-		 
+
 		{
 			"type": "Feature",
 			"geometry": {
@@ -24,7 +24,7 @@ var geojsonSample = {
 				"prop1": 0.0
 			}
 		},
-		 
+
 		{
 			"type": "Feature",
 			"geometry": {
@@ -44,7 +44,10 @@ var geojsonSample = {
 			"geometry": {
 				"type": "MultiPolygon",
 				"coordinates": [[[[100.0, 1.5], [100.5, 1.5], [100.5, 2.0], [100.0, 2.0], [100.0, 1.5]]], [[[100.5, 2.0], [100.5, 2.5], [101.0, 2.5], [101.0, 2.0], [100.5, 2.0]]]]
+			},
+			"properties": {
+				"color": "purple"
 			}
 		}
 	]
-};
\ No newline at end of file
+};
diff --git a/debug/geojson/geojson.html b/debug/geojson/geojson.html
index 319e7c1..3bdd7f9 100644
--- a/debug/geojson/geojson.html
+++ b/debug/geojson/geojson.html
@@ -5,9 +5,9 @@
 
 	<link rel="stylesheet" href="../../dist/leaflet.css" />
 	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
-	
+
 	<link rel="stylesheet" href="../css/screen.css" />
-	
+
 	<script src="../leaflet-include.js"></script>
 </head>
 <body>
@@ -21,36 +21,36 @@
 		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
 			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
 			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution});
-	
+
 		var map = new L.Map('map', {
-			center: new L.LatLng(0.78, 102.37), 
-			zoom: 7, 
+			center: new L.LatLng(0.78, 102.37),
+			zoom: 7,
 			layers: [cloudmade]
 		});
-		
+
 		var geojson = new L.GeoJSON();
-		
+
 		/* points are rendered as markers by default, but you can change this:
-			
+
 		var geojson = new L.GeoJSON(null, {
 			pointToLayer: function(latlng) { return new L.CircleMarker(latlng); }
 		});
 		*/
-		
-		
+
 		geojson.on('featureparse', function(e) {
 			// you can style features depending on their properties, etc.
-			var popupText = 'geometry type: ' + e.geometryType + '<br/>';
-			if (e.layer instanceof L.Path) {
+			var popupText = 'geometry type: ' + e.geometryType;
+
+			if (e.layer.setStyle && e.properties && e.properties.color) {
 				e.layer.setStyle({color: e.properties.color});
-				popupText += 'color: ' + e.properties.color;
+				popupText += '<br/>color: ' + e.properties.color;
 			}
 			e.layer.bindPopup(popupText);
 		});
-		
+
 		geojson.addGeoJSON(geojsonSample);
-		
+
 		map.addLayer(geojson);
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/debug/leaflet-include.js b/debug/leaflet-include.js
index 9ae8f45..48eab66 100644
--- a/debug/leaflet-include.js
+++ b/debug/leaflet-include.js
@@ -2,84 +2,92 @@
 	//TODO replace script list with the one from ../buid/deps.js
 	var scripts = [
 		'Leaflet.js',
-		
-		'core/Util.js', 
+
+		'core/Util.js',
 		'core/Class.js',
 		'core/Events.js',
 		'core/Browser.js',
-		
+
 		'geometry/Point.js',
 		'geometry/Bounds.js',
 		'geometry/Transformation.js',
 		'geometry/LineUtil.js',
 		'geometry/PolyUtil.js',
-		
+
 		'dom/DomEvent.js',
 		'dom/DomEvent.DoubleTap.js',
 		'dom/DomUtil.js',
 		'dom/Draggable.js',
-		
+
 		'dom/transition/Transition.js',
 		'dom/transition/Transition.Native.js',
 		'dom/transition/Transition.Timer.js',
-		
+
 		'geo/LatLng.js',
 		'geo/LatLngBounds.js',
-		
+
 		'geo/projection/Projection.js',
 		'geo/projection/Projection.SphericalMercator.js',
 		'geo/projection/Projection.LonLat.js',
 		'geo/projection/Projection.Mercator.js',
-		
+
 		'geo/crs/CRS.js',
 		'geo/crs/CRS.EPSG3857.js',
 		'geo/crs/CRS.EPSG4326.js',
 		'geo/crs/CRS.EPSG3395.js',
-		
+
+		'map/Map.js',
+
+		'map/ext/Map.Geolocation.js',
+		'map/ext/Map.Popup.js',
+		'map/ext/Map.Control.js',
+
+		'map/anim/Map.PanAnimation.js',
+		'map/anim/Map.ZoomAnimation.js',
+
+		'core/Handler.js',
+		'map/handler/Map.Drag.js',
+		'map/handler/Map.TouchZoom.js',
+		'map/handler/Map.DoubleClickZoom.js',
+		'map/handler/Map.ScrollWheelZoom.js',
+		'map/handler/Map.BoxZoom.js',
+
 		'layer/LayerGroup.js',
 		'layer/FeatureGroup.js',
-		
+
 		'layer/tile/TileLayer.js',
 		'layer/tile/TileLayer.WMS.js',
 		'layer/tile/TileLayer.Canvas.js',
 		'layer/ImageOverlay.js',
 		'layer/Popup.js',
-		
+
 		'layer/marker/Icon.js',
 		'layer/marker/Marker.js',
 		'layer/marker/Marker.Popup.js',
-		
+		'layer/marker/Marker.Drag.js',
+
 		'layer/vector/Path.js',
-		'layer/vector/Path.VML.js',
 		'layer/vector/Path.Popup.js',
+		'layer/vector/Path.SVG.js',
+		'layer/vector/Path.VML.js',
+		'layer/vector/canvas/Path.Canvas.js',
 		'layer/vector/Polyline.js',
+		'layer/vector/canvas/Polyline.Canvas.js',
 		'layer/vector/Polygon.js',
+		'layer/vector/canvas/Polygon.Canvas.js',
 		'layer/vector/MultiPoly.js',
 		'layer/vector/Circle.js',
+		'layer/vector/canvas/Circle.Canvas.js',
 		'layer/vector/CircleMarker.js',
-		
+
 		'layer/GeoJSON.js',
-		
-		'handler/Handler.js',
-		'handler/MapDrag.js',
-		'handler/TouchZoom.js',
-		'handler/DoubleClickZoom.js',
-		'handler/ScrollWheelZoom.js',
-		'handler/ShiftDragZoom.js',
-		'handler/MarkerDrag.js',
-		
+
 		'control/Control.js',
 		'control/Control.Zoom.js',
 		'control/Control.Attribution.js',
-		
-		'map/Map.js',
-		'map/ext/Map.Geolocation.js',
-		'map/ext/Map.Popup.js',
-		'map/ext/Map.PanAnimation.js',
-		'map/ext/Map.ZoomAnimation.js',
-		'map/ext/Map.Control.js'
+		'control/Control.Layers.js',
 	];
-	
+
 	function getSrcUrl() {
 		var scripts = document.getElementsByTagName('script');
 		for (var i = 0; i < scripts.length; i++) {
@@ -92,9 +100,21 @@
 			}
 		}
 	}
-	
+
 	var path = getSrcUrl();
 	for (var i = 0; i < scripts.length; i++) {
 		document.writeln("<script type='text/javascript' src='" + path + "../src/" + scripts[i] + "'></script>");
 	}
-})();
\ No newline at end of file
+})();
+
+function getRandomLatLng(map) {
+	var bounds = map.getBounds(),
+		southWest = bounds.getSouthWest(),
+		northEast = bounds.getNorthEast(),
+		lngSpan = northEast.lng - southWest.lng,
+		latSpan = northEast.lat - southWest.lat;
+
+	return new L.LatLng(
+			southWest.lat + latSpan * Math.random(),
+	        southWest.lng + lngSpan * Math.random());
+}
diff --git a/debug/map/map.html b/debug/map/map.html
index 88bdd5b..0acc769 100644
--- a/debug/map/map.html
+++ b/debug/map/map.html
@@ -5,9 +5,9 @@
 
 	<link rel="stylesheet" href="../../dist/leaflet.css" />
 	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
-	
+
 	<link rel="stylesheet" href="../css/screen.css" />
-	
+
 	<script src="../leaflet-include.js"></script>
 </head>
 <body>
@@ -21,36 +21,34 @@
 			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
 			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution}),
 			latlng = new L.LatLng(50.5, 30.51);
-	
+
 		var map = new L.Map('map', {center: latlng, zoom: 15, layers: [cloudmade]});
-		
+
 		var markers = new L.FeatureGroup();
-		
+
 		function populate() {
-			var bounds = map.getBounds(),
-				southWest = bounds.getSouthWest(),
-				northEast = bounds.getNorthEast(),
-				lngSpan = northEast.lng - southWest.lng,
-				latSpan = northEast.lat - southWest.lat;
-			
 			for (var i = 0; i < 10; i++) {
-				var latlng = new L.LatLng(
-						southWest.lat + latSpan * Math.random(),
-			        	southWest.lng + lngSpan * Math.random());
-				
-				markers.addLayer(new L.Marker(latlng));
+				markers.addLayer(new L.Marker(getRandomLatLng(map)));
 			}
-
 			return false;
-		};
+		}
 
 		markers.bindPopup("<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec odio. Quisque volutpat mattis eros. Nullam malesuada erat ut turpis. Suspendisse urna nibh, viverra non, semper suscipit, posuere a, pede.</p><p>Donec nec justo eget felis facilisis fermentum. Aliquam porttitor mauris sit amet orci. Aenean dignissim pellentesque.</p>");
-		
+
 		map.addLayer(markers);
-		
+
 		populate();
 		L.DomUtil.get('populate').onclick = populate;
-		
+
+//		function logEvent(e) { console.log(e.type); }
+//
+//		map.on('movestart', logEvent);
+//		map.on('move', logEvent);
+//		map.on('moveend', logEvent);
+//
+//		map.on('zoomstart', logEvent);
+//		map.on('zoomend', logEvent);
+
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/debug/map/map-mobile.html b/debug/map/max-bounds.html
similarity index 51%
copy from debug/map/map-mobile.html
copy to debug/map/max-bounds.html
index 27d12ed..fe8d64c 100644
--- a/debug/map/map-mobile.html
+++ b/debug/map/max-bounds.html
@@ -1,42 +1,36 @@
-<!DOCTYPE html>
-<html>
-<head>
-	<title>Leaflet debug page</title>
-
-	<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
-	
-	<link rel="stylesheet" href="../../dist/leaflet.css" />
-	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
-	
-	<link rel="stylesheet" href="../css/mobile.css" />
-	
-	<script src="../leaflet-include.js"></script>
-</head>
-<body>
-
-	<div id="map"></div>
-
-	<script type="text/javascript">
-
-		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
-			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
-			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution});
-	
-		var map = new L.Map('map').addLayer(cloudmade);
-		
-		map.on('locationfound', function(e) {
-			var marker = new L.Marker(e.latlng);
-			map.addLayer(marker);
-			
-			marker.bindPopup("<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec odio. Quisque volutpat mattis eros. Nullam malesuada erat ut turpis. Suspendisse urna nibh, viverra non, semper suscipit, posuere a, pede.</p><p>Donec nec justo eget felis facilisis fermentum. Aliquam porttitor mauris sit amet orci. Aenean dignissim pellentesque felis.</p>");
-		});
-		
-		map.on('locationerror', function(e) {
-			alert(e.message);
-			map.fitWorld();
-		});
-		
-		map.locateAndSetView();
-	</script>
-</body>
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Leaflet debug page</title>
+
+	<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
+
+	<link rel="stylesheet" href="../../dist/leaflet.css" />
+	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+	
+	<link rel="stylesheet" href="../css/mobile.css" />
+	
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+
+	<div id="map"></div>
+
+	<script type="text/javascript">
+
+		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
+			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution}),
+			bounds = new L.LatLngBounds(new L.LatLng(49.5, -11.3), new L.LatLng(61.2, 2.5));
+	
+		var map = new L.Map('map', {
+			center: bounds.getCenter(),
+			zoom: 7,
+			layers: [cloudmade],
+			maxBounds: bounds
+		});
+		
+
+	</script>
+</body>
 </html>
\ No newline at end of file
diff --git a/debug/map/wms.html b/debug/map/scroll.html
similarity index 53%
copy from debug/map/wms.html
copy to debug/map/scroll.html
index 0869472..6ce7c81 100644
--- a/debug/map/wms.html
+++ b/debug/map/scroll.html
@@ -12,26 +12,24 @@
 </head>
 <body>
 
-	<div id="map" style="width: 800px; height: 600px; border: 1px solid #ccc"></div>
+	<div style="position: absolute; top: 100px; left: 100px; border: 1px solid green">
+		<div style="position: relative; margin-top: 500px; width: 800px; border: 1px solid red; margin-left: 200px">
+			<div style="height: 600px; overflow: auto">
+				<div id="map" style="width: 600px; height: 1000px; border: 1px solid #ccc"></div>
+			</div>
+		</div>
+	</div>
 
 	<script type="text/javascript">
-		var map = new L.Map('map');
-	
+
 		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
 			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
-			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution});
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution}),
+			latlng = new L.LatLng(50.5, 30.51);
+	
+		var map = new L.Map('map', {center: latlng, zoom: 15, layers: [cloudmade]});
 	
-		var nexrad = new 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 © 2011 IEM Nexrad",
-			opacity: 0.4
-		});
-
-		var bounds = new L.LatLngBounds(new L.LatLng(32, -126), new L.LatLng(50, -64));
 		
-		map.addLayer(cloudmade).addLayer(nexrad).fitBounds(bounds);
 	</script>
 </body>
 </html>
\ No newline at end of file
diff --git a/debug/vector/vector.html b/debug/vector/editable.html
similarity index 51%
copy from debug/vector/vector.html
copy to debug/vector/editable.html
index 4886f3b..dc7cb97 100644
--- a/debug/vector/vector.html
+++ b/debug/vector/editable.html
@@ -16,23 +16,28 @@
 	<script src="route.js"></script>
 	<script>
 		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
-			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18});
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18}),
+			map = new L.Map('map', {layers: [cloudmade], center: new L.LatLng(50.5, 30.5), zoom: 15});
 	
-		for (var i = 0, latlngs = [], len = route.length; i < len; i++) {
-			latlngs.push(new L.LatLng(route[i][0], route[i][1]));
-		}
-		var path = new L.Polyline(latlngs);
-
-		var map = new L.Map('map', {layers: [cloudmade]});
+		
+		var latlngs = [];
+		latlngs.push(getRandomLatLng(map));
+		latlngs.push(getRandomLatLng(map));
+		latlngs.push(getRandomLatLng(map));
+		
+		var path = new L.Polygon(latlngs);
 
-		map.fitBounds(new L.LatLngBounds(latlngs));
+		console.log(latlngs);
 		
-		map.addLayer(new L.Marker(latlngs[0]));
-		map.addLayer(new L.Marker(latlngs[len - 1]));
+		var marker = new L.Marker(latlngs[0], {draggable: true});
+		map.addLayer(marker);
 		
-		map.addLayer(path);
+		marker.on('drag', function() {
+			latlngs[0] = marker.getLatLng();
+			path.setLatLngs(latlngs);
+		});
 		
-		path.bindPopup("Hello world");
+		map.addLayer(path);
 	</script>
 </body>
 </html>
\ No newline at end of file
diff --git a/debug/vector/vector-bounds.html b/debug/vector/vector-bounds.html
new file mode 100644
index 0000000..a9886d7
--- /dev/null
+++ b/debug/vector/vector-bounds.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Leaflet debug page</title>
+
+	<link rel="stylesheet" href="../../dist/leaflet.css" />
+	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+	
+	<link rel="stylesheet" href="../css/screen.css" />
+	
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+	<div id="map" style="width: 800px; height: 600px; border: 1px solid #ccc"></div>
+    <button onclick="map.fitBounds(path.getBounds())">Zoom to path</button>
+    <button onclick="map.fitBounds(poly.getBounds())">Zoom to polygon</button>
+	<script src="route.js"></script>
+	<script>
+		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18});
+			
+		var poly_points = [
+			[39.70348880963439, -104.98603820800781],
+			[39.69926245589766, -104.95582580566406],
+			[39.67918374111695, -104.94483947753906],
+			[39.663856582926165, -104.95307922363281],
+			[39.66279941218785, -104.98672485351562],
+			[39.70348880963439, -104.98603820800781]
+		];
+		
+		var path_points = [
+			[39.72567292003209, -104.98672485351562],
+			[39.717222671644635, -104.96612548828124],
+			[39.71405356154611, -104.95513916015625],
+			[39.70982785491674, -104.94758605957031],
+			[39.70454535762547, -104.93247985839844],
+			[39.696092520737224, -104.91874694824217],
+			[39.687638648548635, -104.90432739257812],
+			[39.67759833072648, -104.89471435546875]
+		];
+	
+		for (var i = 0, latlngs = [], len = path_points.length; i < len; i++) {
+			latlngs.push(new L.LatLng(path_points[i][0], path_points[i][1]));
+		}
+		var path = new L.Polyline(latlngs);
+		
+		for (var i = 0, latlngs2 = [], len = poly_points.length; i < len; i++) {
+		    latlngs2.push(new L.LatLng(poly_points[i][0], poly_points[i][1]));
+		}
+		var poly = new L.Polygon(latlngs2);
+
+		var map = new L.Map('map', {
+		    layers: [cloudmade],
+		    center: new L.LatLng(39.69596043694606, -104.95084762573242),
+		    zoom: 12
+		});
+
+		//map.fitBounds(new L.LatLngBounds(latlngs));
+		
+		//map.addLayer(new L.Marker(latlngs[0]));
+		//map.addLayer(new L.Marker(latlngs[len - 1]));
+		
+		map.addLayer(path);
+		map.addLayer(poly);
+		
+		path.bindPopup("Hello world");
+	</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/debug/vector/vector-canvas.html b/debug/vector/vector-canvas.html
new file mode 100644
index 0000000..eddec93
--- /dev/null
+++ b/debug/vector/vector-canvas.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Leaflet debug page</title>
+
+	<link rel="stylesheet" href="../../dist/leaflet.css" />
+	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+	
+	<link rel="stylesheet" href="../css/screen.css" />
+	
+	<script>
+		L_PREFER_CANVAS = true; // experimental
+	</script>
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+	<div id="map" style="width: 800px; height: 600px; border: 1px solid #ccc"></div>
+    <button onclick="group.removeLayer(path)">Remove path</button>
+    <button onclick="group.removeLayer(circle)">Remove circle</button>
+    <button onclick="group.clearLayers()">Remove all layers</button>
+
+
+	<script src="route.js"></script>
+	<script>
+		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18});
+	
+		for (var i = 0, latlngs = [], len = route.length; i < len; i++) {
+			latlngs.push(new L.LatLng(route[i][0], route[i][1]));
+		}
+		var path = new L.Polyline(latlngs);
+
+		var map = new L.Map('map', {layers: [cloudmade]});
+		
+		var group = new L.LayerGroup();
+
+		map.fitBounds(new L.LatLngBounds(latlngs));
+		
+		var circleLocation = new L.LatLng(51.508, -0.11),
+		circleOptions = {
+		    color: 'red', 
+		    fillColor: 'yellow', 
+		    fillOpacity: 0.7
+		};
+		
+		var circle = new L.Circle(circleLocation, 500000, circleOptions),
+			circleMarker = new L.CircleMarker(circleLocation, {fillColor: 'blue', fillOpacity: 1, stroke: false});
+		
+		group.addLayer(circle).addLayer(circleMarker);
+		
+		circle.bindPopup('I am a circle');
+		circleMarker.bindPopup('I am a circle marker');
+		
+		group.addLayer(path);		
+		path.bindPopup('I am a polyline');
+		
+		var p1 = latlngs[0],
+			p2 = latlngs[parseInt(len/4)],
+			p3 = latlngs[parseInt(len/3)],
+			p4 = latlngs[parseInt(len/2)],
+			p5 = latlngs[len - 1],
+			polygonPoints = [p1, p2, p3, p4, p5];
+
+		var h1 = new L.LatLng(p1.lat, p1.lng),
+			h2 = new L.LatLng(p2.lat, p2.lng),
+			h3 = new L.LatLng(p3.lat, p3.lng),
+			h4 = new L.LatLng(p4.lat, p4.lng),
+			h5 = new L.LatLng(p5.lat, p5.lng);
+		
+		h1.lng += 20;
+		h2.lat -= 5;
+		h3.lat -= 5;
+		h4.lng -= 10;
+		h5.lng -= 8;
+		h5.lat += 10;
+		
+		var holePoints = [h5, h4, h3, h2, h1];
+
+		var polygon = new L.Polygon([polygonPoints, holePoints], {
+			fillColor: "#333",
+			color: 'green'
+		});
+		group.addLayer(polygon);
+		
+		polygon.bindPopup('I am a polygon');
+		
+		map.addLayer(group);
+
+	</script>
+
+</body>
+</html>
diff --git a/debug/vector/vector-simple.html b/debug/vector/vector-simple.html
new file mode 100644
index 0000000..a9d77d5
--- /dev/null
+++ b/debug/vector/vector-simple.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Leaflet debug page</title>
+
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+
+	<link rel="stylesheet" href="../../dist/leaflet.css" />
+	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+
+	<link rel="stylesheet" href="../css/mobile.css" />
+
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+	<div id="map"></div>
+
+	<script>
+
+		var map = new L.Map('map', {fadeAnimation: false});
+
+		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
+			cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
+			cloudmade = new L.TileLayer(cloudmadeUrl, {
+				maxZoom: 18,
+				attribution: cloudmadeAttribution
+			});
+
+		map.setView(new L.LatLng(51.505, -0.09), 13).addLayer(cloudmade);
+
+
+		var markerLocation = new L.LatLng(51.5, -0.09),
+			marker = new L.Marker(markerLocation);
+
+		map.addLayer(marker);
+
+		marker.bindPopup("<b>Hello world!</b><br />I am a popup.").openPopup();
+
+
+		var circleLocation = new L.LatLng(51.508, -0.11),
+			circleOptions = {
+				color: '#f03',
+				opacity: 0.7
+			},
+			circle = new L.Circle(circleLocation, 500, circleOptions);
+
+		circle.bindPopup("I am a circle.");
+
+		map.addLayer(circle);
+
+
+		var p1 = new L.LatLng(51.509, -0.08),
+			p2 = new L.LatLng(51.503, -0.06),
+			p3 = new L.LatLng(51.51, -0.047),
+			polygonPoints = [ p1, p2, p3 ],
+			polygon = new L.Polygon(polygonPoints);
+
+		polygon.bindPopup("I am a polygon.");
+
+		map.addLayer(polygon);
+	</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/dist/images/layers.png b/dist/images/layers.png
new file mode 100644
index 0000000..9be965f
Binary files /dev/null and b/dist/images/layers.png differ
diff --git a/dist/leaflet.css b/dist/leaflet.css
index 4bc0b76..119ddb2 100644
--- a/dist/leaflet.css
+++ b/dist/leaflet.css
@@ -2,32 +2,32 @@
 
 .leaflet-map-pane,
 .leaflet-tile,
-.leaflet-marker-icon, 
+.leaflet-marker-icon,
 .leaflet-marker-shadow,
-.leaflet-tile-pane, 
+.leaflet-tile-pane,
 .leaflet-overlay-pane,
 .leaflet-shadow-pane,
 .leaflet-marker-pane,
 .leaflet-popup-pane,
 .leaflet-overlay-pane svg,
 .leaflet-zoom-box,
-.leaflet-image-layer { /* TODO optimize classes */ 
+.leaflet-image-layer { /* TODO optimize classes */
 	position: absolute;
 	}
 .leaflet-container {
 	overflow: hidden;
 	}
-.leaflet-tile-pane {
+.leaflet-tile-pane, .leaflet-container {
 	-webkit-transform: translate3d(0,0,0);
 	}
-.leaflet-tile, 
-.leaflet-marker-icon, 
+.leaflet-tile,
+.leaflet-marker-icon,
 .leaflet-marker-shadow {
 	-moz-user-select: none;
 	-webkit-user-select: none;
 	user-select: none;
 	}
-.leaflet-marker-icon, 
+.leaflet-marker-icon,
 .leaflet-marker-shadow {
 	display: block;
 	}
@@ -35,14 +35,16 @@
 	cursor: pointer;
 	}
 .leaflet-container img {
-	max-width: auto;
+	max-width: none !important;
 	}
 
 .leaflet-tile-pane { z-index: 2; }
-.leaflet-overlay-pane { z-index: 3; }
-.leaflet-shadow-pane { z-index: 4; }
-.leaflet-marker-pane { z-index: 5; }
-.leaflet-popup-pane { z-index: 6; }
+
+.leaflet-objects-pane { z-index: 3; }
+.leaflet-overlay-pane { z-index: 4; }
+.leaflet-shadow-pane { z-index: 5; }
+.leaflet-marker-pane { z-index: 6; }
+.leaflet-popup-pane { z-index: 7; }
 
 .leaflet-zoom-box {
 	width: 0;
@@ -79,7 +81,7 @@ a.leaflet-active {
 	}
 .leaflet-bottom {
 	bottom: 0;
-	}	
+	}
 .leaflet-left {
 	left: 0;
 	}
@@ -103,25 +105,29 @@ a.leaflet-active {
 	margin-right: 10px;
 	}
 
-.leaflet-control-zoom {
-	padding: 5px;
-	background: rgba(0, 0, 0, 0.25);
-	
+.leaflet-control-zoom, .leaflet-control-layers {
 	-moz-border-radius: 7px;
 	-webkit-border-radius: 7px;
 	border-radius: 7px;
 	}
+.leaflet-control-zoom {
+	padding: 5px;
+	background: rgba(0, 0, 0, 0.25);
+	}
 .leaflet-control-zoom a {
-	display: block;
-	width: 19px;
-	height: 19px;
+	background-color: rgba(255, 255, 255, 0.75);
+	}
+.leaflet-control-zoom a, .leaflet-control-layers a {
 	background-position: 50% 50%;
 	background-repeat: no-repeat;
-	background-color: rgba(255, 255, 255, 0.75);
-	
+	display: block;
+	}
+.leaflet-control-zoom a {
 	-moz-border-radius: 4px;
 	-webkit-border-radius: 4px;
 	border-radius: 4px;
+	width: 19px;
+	height: 19px;
 	}
 .leaflet-control-zoom a:hover {
 	background-color: #fff;
@@ -137,16 +143,60 @@ a.leaflet-active {
 .leaflet-control-zoom-out {
 	background-image: url(images/zoom-out.png);
 	}
-	
+
+.leaflet-control-layers {
+	-moz-box-shadow: 0 0 7px #999;
+	-webkit-box-shadow: 0 0 7px #999;
+	box-shadow: 0 0 7px #999;
+
+	background: #f8f8f9;
+	}
+.leaflet-control-layers a {
+	background-image: url(images/layers.png);
+	width: 36px;
+	height: 36px;
+	}
+.leaflet-big-buttons .leaflet-control-layers a {
+	width: 44px;
+	height: 44px;
+	}
+.leaflet-control-layers .leaflet-control-layers-list,
+.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
+	display: none;
+	}
+.leaflet-control-layers-expanded .leaflet-control-layers-list {
+	display: block;
+	position: relative;
+	}
+.leaflet-control-layers-expanded {
+	padding: 6px 10px 6px 6px;
+	font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
+	color: #333;
+	background: #fff;
+	}
+.leaflet-control-layers input {
+	margin-top: 2px;
+	position: relative;
+	top: 1px;
+	}
+.leaflet-control-layers label {
+	display: block;
+	}
+.leaflet-control-layers-separator {
+	height: 0;
+	border-top: 1px solid #ddd;
+	margin: 5px -10px 5px -6px;
+	}
+
 .leaflet-container .leaflet-control-attribution {
 	margin: 0;
 	padding: 0 5px;
-	
+
 	font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
 	color: #333;
-	
+
 	background-color: rgba(255, 255, 255, 0.7);
-            
+
 	-moz-box-shadow: 0 0 7px #ccc;
 	-webkit-box-shadow: 0 0 7px #ccc;
 	box-shadow: 0 0 7px #ccc;
@@ -157,7 +207,7 @@ a.leaflet-active {
 
 .leaflet-fade-anim .leaflet-tile {
 	opacity: 0;
-	
+
 	-webkit-transition: opacity 0.2s linear;
 	-moz-transition: opacity 0.2s linear;
 	-o-transition: opacity 0.2s linear;
@@ -216,9 +266,9 @@ a.leaflet-active {
 	width: 15px;
 	height: 15px;
 	padding: 1px;
-	
+
 	margin: -8px auto 0;
-	
+
 	-moz-transform: rotate(45deg);
 	-webkit-transform: rotate(45deg);
 	-ms-transform: rotate(45deg);
@@ -229,10 +279,10 @@ a.leaflet-active {
 	position: absolute;
 	top: 9px;
 	right: 9px;
-	
+
 	width: 10px;
 	height: 10px;
-	
+
 	overflow: hidden;
 	}
 .leaflet-popup-content p {
@@ -255,13 +305,13 @@ a.leaflet-active {
 	}
 .leaflet-popup-content-wrapper, .leaflet-popup-tip {
 	background: white;
-	
+
 	box-shadow: 0 1px 10px #888;
 	-moz-box-shadow: 0 1px 10px #888;
 	 -webkit-box-shadow: 0 1px 14px #999;
 	}
 .leaflet-popup-content-wrapper {
-	-moz-border-radius: 20px; 
+	-moz-border-radius: 20px;
 	-webkit-border-radius: 20px;
 	border-radius: 20px;
 	}
@@ -270,4 +320,4 @@ a.leaflet-active {
 	}
 .leaflet-popup-close-button {
 	background: white url(images/popup-close.png);
-	}
\ No newline at end of file
+	}
diff --git a/dist/leaflet.ie.css b/dist/leaflet.ie.css
index 141a16f..a120c0c 100644
--- a/dist/leaflet.ie.css
+++ b/dist/leaflet.ie.css
@@ -41,6 +41,8 @@
 .leaflet-control-zoom a:hover {
 	background-color: #fff;
 	}
-.leaflet-control-attribution {
-	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#B2FFFFFF,endColorstr=#B2FFFFFF);
+.leaflet-control-layers-toggle {
+	}
+.leaflet-control-attribution, .leaflet-control-layers {
+	background: white;
 	}
\ No newline at end of file
diff --git a/spec/runner.html b/spec/runner.html
index 6931d2c..3013985 100644
--- a/spec/runner.html
+++ b/spec/runner.html
@@ -12,33 +12,7 @@
 			L = 'test'; //to test L#noConflict later
 		</script>
 		
-		<script type="text/javascript" src="../src/Leaflet.js"></script>
-	  
-		<!-- /core -->
-		<script type="text/javascript" src="../src/core/Util.js"></script>
-		<script type="text/javascript" src="../src/core/Class.js"></script>
-		<script type="text/javascript" src="../src/core/Events.js"></script>
-		<script type="text/javascript" src="../src/core/Browser.js"></script>
-
-		<!-- /dom -->
-		<script type="text/javascript" src="../src/dom/DomEvent.js"></script>
-		<script type="text/javascript" src="../src/dom/DomUtil.js"></script>
-	
-		<!-- /geo -->
-		<script type="text/javascript" src="../src/geo/LatLng.js"></script>
-		<script type="text/javascript" src="../src/geo/LatLngBounds.js"></script>
-		<script type="text/javascript" src="../src/geo/Projection.js"></script>
-
-		<!-- /geometry -->
-		<script type="text/javascript" src="../src/geometry/Point.js"></script>
-		<script type="text/javascript" src="../src/geometry/Bounds.js"></script>
-		<script type="text/javascript" src="../src/geometry/Transformation.js"></script>
-
-		<!-- /layer -->
-		<script type="text/javascript" src="../src/layer/TileLayer.js"></script>
-		
-		<!-- /map -->
-		<script type="text/javascript" src="../src/map/Map.js"></script>
+		<script type="text/javascript" src="../debug/leaflet-include.js"></script>
 		
 
 	<!-- spec files -->
@@ -67,6 +41,7 @@
 
 		<!-- /layer -->
 		<script type="text/javascript" src="suites/layer/TileLayerSpec.js"></script>
+		<script type="text/javascript" src="suites/layer/vector/PolylineGeometrySpec.js"></script>
 		
 		<!-- /map -->
 		<script type="text/javascript" src="suites/map/MapSpec.js"></script>
diff --git a/spec/suites/SpecHelper.js b/spec/suites/SpecHelper.js
index 8b82704..fb88857 100644
--- a/spec/suites/SpecHelper.js
+++ b/spec/suites/SpecHelper.js
@@ -1,5 +1,28 @@
 function noSpecs() {
-	it('should have specs', function() {
+	xit('should have specs', function() {
 		expect('specs').toBe();
 	});
+}
+
+if (!Array.prototype.map) {
+  Array.prototype.map = function(fun /*, thisp */) {
+    "use strict";
+
+    if (this === void 0 || this === null)
+      throw new TypeError();
+
+    var t = Object(this);
+    var len = t.length >>> 0;
+    if (typeof fun !== "function")
+      throw new TypeError();
+
+    var res = new Array(len);
+    var thisp = arguments[1];
+    for (var i = 0; i < len; i++) {
+      if (i in t)
+        res[i] = fun.call(thisp, t[i], i, t);
+    }
+
+    return res;
+  };
 }
\ No newline at end of file
diff --git a/spec/suites/core/ClassSpec.js b/spec/suites/core/ClassSpec.js
index 7a28915..48e5d5f 100644
--- a/spec/suites/core/ClassSpec.js
+++ b/spec/suites/core/ClassSpec.js
@@ -6,8 +6,8 @@ describe("Class", function() {
 			method;
 		
 		beforeEach(function() {
-			constructor = jasmine.createSpy(),
-			method = jasmine.createSpy();
+			constructor = jasmine.createSpy("Klass constructor");
+			method = jasmine.createSpy("Klass#bar method");
 
 			Klass = L.Class.extend({
 				statics: {bla: 1},
@@ -55,10 +55,10 @@ describe("Class", function() {
 			var b = new Klass2();
 			
 			expect(constructor).not.toHaveBeenCalled();
-			b.superclass.initialize.call(this);
+			b.constructor.superclass.initialize.call(this);
 			expect(constructor).toHaveBeenCalled();
 
-			b.superclass.bar.call(this);
+			b.constructor.superclass.bar.call(this);
 			expect(method).toHaveBeenCalled();
 		});
 		
@@ -72,6 +72,12 @@ describe("Class", function() {
 			expect(Klass2.bla).toEqual(1);
 		});
 		
+		it("should override parent static properties", function() {
+			var Klass2 = Klass.extend({statics: {bla: 2}});
+			
+			expect(Klass2.bla).toEqual(2);
+		});
+		
 		it("should include the given mixin", function() {
 			var a = new Klass();
 			expect(a.mixin).toBeTruthy();
@@ -116,5 +122,32 @@ describe("Class", function() {
 				foo3: 4
 			});
 		});
+		
+		it("should have working superclass access with inheritance level > 2", function() {
+			var constructor2 = jasmine.createSpy("Klass2 constructor"),
+				constructor3 = jasmine.createSpy("Klass3 constructor");
+			
+			var Klass2 = Klass.extend({
+				initialize: function() {
+					constructor2();
+					expect(Klass2.superclass).toBe(Klass.prototype);
+					Klass2.superclass.initialize.apply(this, arguments);
+				}
+			});
+			
+			var Klass3 = Klass2.extend({
+				initialize: function() {
+					constructor3();
+					expect(Klass3.superclass).toBe(Klass2.prototype);
+					Klass3.superclass.initialize.apply(this, arguments);
+				}
+			});
+			
+			var a = new Klass3();
+			
+			expect(constructor3).toHaveBeenCalled();
+			expect(constructor2).toHaveBeenCalled();
+			expect(constructor).toHaveBeenCalled();
+		});
 	});
 });
\ No newline at end of file
diff --git a/spec/suites/dom/DomUtilSpec.js b/spec/suites/dom/DomUtilSpec.js
index 60de22f..7814c99 100644
--- a/spec/suites/dom/DomUtilSpec.js
+++ b/spec/suites/dom/DomUtilSpec.js
@@ -23,6 +23,36 @@ describe('DomUtil', function() {
 		});
 	});
 	
+	describe('#addClass, #removeClass, #hasClass', function() {
+		it('should has defined class for test element', function() {
+			el.className = 'bar foo baz ';
+			expect(L.DomUtil.hasClass(el, 'foo')).toBeTruthy();
+			expect(L.DomUtil.hasClass(el, 'bar')).toBeTruthy();
+			expect(L.DomUtil.hasClass(el, 'baz')).toBeTruthy();
+			expect(L.DomUtil.hasClass(el, 'boo')).toBeFalsy();
+		});
+		
+		it('should properly addClass and removeClass for element', function() {
+			el.className = '';
+			L.DomUtil.addClass(el, 'foo');
+			
+			expect(el.className).toEqual('foo');
+			expect(L.DomUtil.hasClass(el, 'foo')).toBeTruthy();
+			
+			L.DomUtil.addClass(el, 'bar');
+			expect(el.className).toEqual('foo bar');
+			expect(L.DomUtil.hasClass(el, 'foo')).toBeTruthy();
+			
+			L.DomUtil.removeClass(el, 'foo');
+			expect(el.className).toEqual('bar');
+			expect(L.DomUtil.hasClass(el, 'foo')).toBeFalsy();
+			
+			el.className = 'foo bar barz';
+			L.DomUtil.removeClass(el, 'bar');
+			expect(el.className).toEqual('foo barz');
+		})
+	});
+	
 	describe('#setPosition', noSpecs);
 	
 	describe('#getStyle', noSpecs);
diff --git a/spec/suites/geo/LatLngSpec.js b/spec/suites/geo/LatLngSpec.js
index a7498e7..ccf1706 100644
--- a/spec/suites/geo/LatLngSpec.js
+++ b/spec/suites/geo/LatLngSpec.js
@@ -21,7 +21,7 @@ describe('LatLng', function() {
 		it("should clamp longtitude to lie between -180 and 180", function() {
 			var a = new L.LatLng(0, 190).lng;
 			expect(a).toEqual(-170);
-	
+
 			var b = new L.LatLng(0, 360).lng;
 			expect(b).toEqual(0);
 			
@@ -36,7 +36,13 @@ describe('LatLng', function() {
 	
 			var f = new L.LatLng(0, -380).lng;
 			expect(f).toEqual(-20);
-		});
+
+			var g = new L.LatLng(0, 90).lng;
+			expect(g).toEqual(90);
+
+			var h = new L.LatLng(0, 180).lng;
+			expect(h).toEqual(180);
+	 });
 		
 		it("should not clamp latitude and longtitude if unbounded flag set to true", function() {
 			var a = new L.LatLng(150, 0, true).lat;
diff --git a/spec/suites/geo/ProjectionSpec.js b/spec/suites/geo/ProjectionSpec.js
index 6b9c7b6..4aa6fda 100644
--- a/spec/suites/geo/ProjectionSpec.js
+++ b/spec/suites/geo/ProjectionSpec.js
@@ -1,4 +1,4 @@
-describe("Projection.Mercator", function() {
+xdescribe("Projection.Mercator", function() {
 	var p = L.Projection.Mercator;
 	
 	beforeEach(function() {
diff --git a/spec/suites/geometry/BoundsSpec.js b/spec/suites/geometry/BoundsSpec.js
index eee05e4..22aa42a 100644
--- a/spec/suites/geometry/BoundsSpec.js
+++ b/spec/suites/geometry/BoundsSpec.js
@@ -40,4 +40,14 @@ describe('Bounds', function() {
 			expect(a.getCenter()).toEqual(new L.Point(22, 26));
 		});
 	});
+    
+	describe('#contains', function() {
+	    it('should contains other bounds or point', function() {
+	        a.extend(new L.Point(50, 10));
+	        expect(a.contains(b)).toBeTruthy();
+	        expect(b.contains(a)).toBeFalsy();
+	        expect(a.contains(new L.Point(24, 25))).toBeTruthy();
+	        expect(a.contains(new L.Point(54, 65))).toBeFalsy();
+	    });
+	});
 });
\ No newline at end of file
diff --git a/spec/suites/layer/vector/PolylineGeometrySpec.js b/spec/suites/layer/vector/PolylineGeometrySpec.js
new file mode 100644
index 0000000..9aec2cf
--- /dev/null
+++ b/spec/suites/layer/vector/PolylineGeometrySpec.js
@@ -0,0 +1,35 @@
+describe('PolylineGeometry', function() {
+	
+	var c = document.createElement('div');
+	c.style.width = '400px';
+	c.style.height = '400px';
+	var map = new L.Map(c);
+	map.setView(new L.LatLng(55.8, 37.6), 6);
+	
+	describe("#distanceTo", function() {
+		it("should calculate correct distances to points", function() {
+			var p1 = map.latLngToLayerPoint(new L.LatLng(55.8, 37.6));
+			var p2 = map.latLngToLayerPoint(new L.LatLng(57.123076977278, 44.861962891635));
+			var latlngs = [[56.485503424111, 35.545556640339], [55.972522915346, 36.116845702918], [55.502459116923, 34.930322265253], [55.31534617509, 38.973291015816]]
+			.map(function(ll) {
+				return new L.LatLng(ll[0], ll[1]);
+			});
+			var polyline = new L.Polyline([], {
+				'noClip': true
+			});
+			map.addLayer(polyline);
+			
+			expect(polyline.closestLayerPoint(p1)).toEqual(null);
+			
+			polyline.setLatLngs(latlngs);
+			var point = polyline.closestLayerPoint(p1);
+			expect(point).not.toEqual(null);
+			expect(point.distance).not.toEqual(Infinity);
+			expect(point.distance).not.toEqual(NaN);
+			
+			var point2 = polyline.closestLayerPoint(p2);
+			
+			expect(point.distance).toBeLessThan(point2.distance);
+		});
+	});
+});
diff --git a/src/Leaflet.js b/src/Leaflet.js
index 750a4a5..e40a58b 100644
--- a/src/Leaflet.js
+++ b/src/Leaflet.js
@@ -1,35 +1,33 @@
-/**
- * @preserve Copyright (c) 2010-2011, CloudMade, Vladimir Agafonkin
- * Leaflet is a BSD-licensed JavaScript library for map display and interaction.
- * See http://cloudmade.github.com/Leaflet/ for more information.
- */
 
-(function(root) {
-	var L = {
-		VERSION: '0.2',
-		
-		ROOT_URL: (function() {
+(function (root) {
+	root.L = {
+		VERSION: '0.3',
+
+		ROOT_URL: root.L_ROOT_URL || (function () {
 			var scripts = document.getElementsByTagName('script'),
-				leafletRe = /^(.*\/)leaflet-?([\w-]*)\.js.*$/;
-			for (var i = 0, len = scripts.length; i < len; i++) {
-				var src = scripts[i].src,
-					res = src && src.match(leafletRe);
-				
-				if (res) {
-					if (res[2] == 'include') break;
-					return res[1];
+				leafletRe = /\/?leaflet[\-\._]?([\w\-\._]*)\.js\??/;
+
+			var i, len, src, matches;
+
+			for (i = 0, len = scripts.length; i < len; i++) {
+				src = scripts[i].src;
+				matches = src.match(leafletRe);
+
+				if (matches) {
+					if (matches[1] === 'include') {
+						return '../../dist/';
+					}
+					return src.replace(leafletRe, '') + '/';
 				}
 			}
-			return '../../dist/';
-		})(),
-		
-		noConflict: function() {
+			return '';
+		}()),
+
+		noConflict: function () {
 			root.L = this._originalL;
 			return this;
 		},
-		
+
 		_originalL: root.L
 	};
-	
-	window.L = L;
-}(this));
+}(this));
diff --git a/src/control/Control.Attribution.js b/src/control/Control.Attribution.js
index 84b31f5..c56e5ed 100644
--- a/src/control/Control.Attribution.js
+++ b/src/control/Control.Attribution.js
@@ -1,47 +1,61 @@
 L.Control.Attribution = L.Class.extend({
-	onAdd: function(map) {
+	initialize: function (prefix) {
+		this._prefix = prefix || 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>';
+		this._attributions = {};
+	},
+
+	onAdd: function (map) {
 		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
+		L.DomEvent.disableClickPropagation(this._container);
 		this._map = map;
-		this._prefix = 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>';
-		this._attributions = {};
 		this._update();
 	},
-	
-	getPosition: function() {
+
+	getPosition: function () {
 		return L.Control.Position.BOTTOM_RIGHT;
 	},
-	
-	getContainer: function() {
+
+	getContainer: function () {
 		return this._container;
 	},
-	
-	setPrefix: function(prefix) {
+
+	setPrefix: function (prefix) {
 		this._prefix = prefix;
+		this._update();
 	},
-	
-	addAttribution: function(text) {
-		if (!text) return;
-		this._attributions[text] = true;
+
+	addAttribution: function (text) {
+		if (!text) {
+			return;
+		}
+		if (!this._attributions[text]) {
+			this._attributions[text] = 0;
+		}
+		this._attributions[text]++;
 		this._update();
 	},
-	
-	removeAttribution: function(text) {
-		if (!text) return;
-		delete this._attributions[text];
+
+	removeAttribution: function (text) {
+		if (!text) {
+			return;
+		}
+		this._attributions[text]--;
 		this._update();
 	},
-	
-	_update: function() {
-		if (!this._map) return;
-		
+
+	_update: function () {
+		if (!this._map) {
+			return;
+		}
+
 		var attribs = [];
-		
+
 		for (var i in this._attributions) {
 			if (this._attributions.hasOwnProperty(i)) {
 				attribs.push(i);
 			}
 		}
-		
+
 		var prefixAndAttribs = [];
 		if (this._prefix) {
 			prefixAndAttribs.push(this._prefix);
@@ -49,7 +63,7 @@ L.Control.Attribution = L.Class.extend({
 		if (attribs.length) {
 			prefixAndAttribs.push(attribs.join(', '));
 		}
-		
+
 		this._container.innerHTML = prefixAndAttribs.join(' — ');
 	}
-});
\ No newline at end of file
+});
diff --git a/src/control/Control.Layers.js b/src/control/Control.Layers.js
new file mode 100644
index 0000000..a8667c1
--- /dev/null
+++ b/src/control/Control.Layers.js
@@ -0,0 +1,174 @@
+
+L.Control.Layers = L.Class.extend({
+	options: {
+		collapsed: true
+	},
+
+	initialize: function (baseLayers, overlays, options) {
+		L.Util.setOptions(this, options);
+
+		this._layers = {};
+
+		for (var i in baseLayers) {
+			if (baseLayers.hasOwnProperty(i)) {
+				this._addLayer(baseLayers[i], i);
+			}
+		}
+
+		for (i in overlays) {
+			if (overlays.hasOwnProperty(i)) {
+				this._addLayer(overlays[i], i, true);
+			}
+		}
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		this._initLayout();
+		this._update();
+	},
+
+	getContainer: function () {
+		return this._container;
+	},
+
+	getPosition: function () {
+		return L.Control.Position.TOP_RIGHT;
+	},
+
+	addBaseLayer: function (layer, name) {
+		this._addLayer(layer, name);
+		this._update();
+		return this;
+	},
+
+	addOverlay: function (layer, name) {
+		this._addLayer(layer, name, true);
+		this._update();
+		return this;
+	},
+
+	removeLayer: function (layer) {
+		var id = L.Util.stamp(layer);
+		delete this._layers[id];
+		this._update();
+		return this;
+	},
+
+	_initLayout: function () {
+		this._container = L.DomUtil.create('div', 'leaflet-control-layers');
+		if (!L.Browser.touch) {
+			L.DomEvent.disableClickPropagation(this._container);
+		}
+
+		this._form = L.DomUtil.create('form', 'leaflet-control-layers-list');
+
+		if (this.options.collapsed) {
+			L.DomEvent.addListener(this._container, 'mouseover', this._expand, this);
+			L.DomEvent.addListener(this._container, 'mouseout', this._collapse, this);
+
+			var link = this._layersLink = L.DomUtil.create('a', 'leaflet-control-layers-toggle');
+			link.href = '#';
+			link.title = 'Layers';
+
+			if (L.Browser.touch) {
+				L.DomEvent.addListener(link, 'click', this._expand, this);
+				//L.DomEvent.disableClickPropagation(link);
+			} else {
+				L.DomEvent.addListener(link, 'focus', this._expand, this);
+			}
+			this._map.on('movestart', this._collapse, this);
+			// TODO keyboard accessibility
+
+			this._container.appendChild(link);
+		} else {
+			this._expand();
+		}
+
+		this._baseLayersList = L.DomUtil.create('div', 'leaflet-control-layers-base', this._form);
+		this._separator = L.DomUtil.create('div', 'leaflet-control-layers-separator', this._form);
+		this._overlaysList = L.DomUtil.create('div', 'leaflet-control-layers-overlays', this._form);
+
+		this._container.appendChild(this._form);
+	},
+
+	_addLayer: function (layer, name, overlay) {
+		var id = L.Util.stamp(layer);
+		this._layers[id] = {
+			layer: layer,
+			name: name,
+			overlay: overlay
+		};
+	},
+
+	_update: function () {
+		if (!this._container) {
+			return;
+		}
+
+		this._baseLayersList.innerHTML = '';
+		this._overlaysList.innerHTML = '';
+
+		var baseLayersPresent = false,
+			overlaysPresent = false;
+
+		for (var i in this._layers) {
+			if (this._layers.hasOwnProperty(i)) {
+				var obj = this._layers[i];
+				this._addItem(obj);
+				overlaysPresent = overlaysPresent || obj.overlay;
+				baseLayersPresent = baseLayersPresent || !obj.overlay;
+			}
+		}
+
+		this._separator.style.display = (overlaysPresent && baseLayersPresent ? '' : 'none');
+	},
+
+	_addItem: function (obj, onclick) {
+		var label = document.createElement('label');
+
+		var input = document.createElement('input');
+		if (!obj.overlay) {
+			input.name = 'leaflet-base-layers';
+		}
+		input.type = obj.overlay ? 'checkbox' : 'radio';
+		input.checked = this._map.hasLayer(obj.layer);
+		input.layerId = L.Util.stamp(obj.layer);
+
+		L.DomEvent.addListener(input, 'click', this._onInputClick, this);
+
+		var name = document.createTextNode(' ' + obj.name);
+
+		label.appendChild(input);
+		label.appendChild(name);
+
+		var container = obj.overlay ? this._overlaysList : this._baseLayersList;
+		container.appendChild(label);
+	},
+
+	_onInputClick: function () {
+		var i, input, obj,
+			inputs = this._form.getElementsByTagName('input'),
+			inputsLen = inputs.length;
+
+		for (i = 0; i < inputsLen; i++) {
+			input = inputs[i];
+			obj = this._layers[input.layerId];
+
+			if (input.checked) {
+				this._map.addLayer(obj.layer, !obj.overlay);
+			} else {
+				this._map.removeLayer(obj.layer);
+			}
+		}
+	},
+
+	_expand: function () {
+		L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
+	},
+
+	_collapse: function () {
+		this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
+	}
+});
diff --git a/src/control/Control.Zoom.js b/src/control/Control.Zoom.js
index d6964fd..38cfd8e 100644
--- a/src/control/Control.Zoom.js
+++ b/src/control/Control.Zoom.js
@@ -1,9 +1,9 @@
 
 L.Control.Zoom = L.Class.extend({
-	onAdd: function(map) {
+	onAdd: function (map) {
 		this._map = map;
 		this._container = L.DomUtil.create('div', 'leaflet-control-zoom');
-		
+
 		this._zoomInButton = this._createButton(
 				'Zoom in', 'leaflet-control-zoom-in', this._map.zoomIn, this._map);
 		this._zoomOutButton = this._createButton(
@@ -12,25 +12,27 @@ L.Control.Zoom = L.Class.extend({
 		this._container.appendChild(this._zoomInButton);
 		this._container.appendChild(this._zoomOutButton);
 	},
-	
-	getContainer: function() { 
-		return this._container; 
+
+	getContainer: function () {
+		return this._container;
 	},
-	
-	getPosition: function() {
+
+	getPosition: function () {
 		return L.Control.Position.TOP_LEFT;
 	},
-	
-	_createButton: function(title, className, fn, context) {
+
+	_createButton: function (title, className, fn, context) {
 		var link = document.createElement('a');
 		link.href = '#';
 		link.title = title;
 		link.className = className;
 
-		L.DomEvent.disableClickPropagation(link);
+		if (!L.Browser.touch) {
+			L.DomEvent.disableClickPropagation(link);
+		}
 		L.DomEvent.addListener(link, 'click', L.DomEvent.preventDefault);
 		L.DomEvent.addListener(link, 'click', fn, context);
-		
+
 		return link;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/control/Control.js b/src/control/Control.js
index a01c680..2d60e95 100644
--- a/src/control/Control.js
+++ b/src/control/Control.js
@@ -6,4 +6,4 @@ L.Control.Position = {
 	TOP_RIGHT: 'topRight',
 	BOTTOM_LEFT: 'bottomLeft',
 	BOTTOM_RIGHT: 'bottomRight'
-};
\ No newline at end of file
+};
diff --git a/src/core/Browser.js b/src/core/Browser.js
index 0604ed6..ec54817 100644
--- a/src/core/Browser.js
+++ b/src/core/Browser.js
@@ -1,23 +1,53 @@
-(function() {
+(function () {
 	var ua = navigator.userAgent.toLowerCase(),
 		ie = !!window.ActiveXObject,
-		webkit = ua.indexOf("webkit") != -1,
-		mobile = ua.indexOf("mobi") != -1,
-		android = ua.indexOf("android") != -1,
+		webkit = ua.indexOf("webkit") !== -1,
+		mobile = typeof orientation !== 'undefined' ? true : false,
+		android = ua.indexOf("android") !== -1,
 		opera = window.opera;
-	
+
 	L.Browser = {
 		ie: ie,
 		ie6: ie && !window.XMLHttpRequest,
+
 		webkit: webkit,
-		webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()),
-		mobileWebkit: webkit && (mobile || android),
+		webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()),
+
+		gecko: ua.indexOf("gecko") !== -1,
+
+		opera: opera,
+
+		android: android,
+		mobileWebkit: mobile && webkit,
 		mobileOpera: mobile && opera,
-		gecko: ua.indexOf("gecko") != -1,
-		android: android
+
+		mobile: mobile,
+		touch: (function () {
+			var touchSupported = false,
+				startName = 'ontouchstart';
+
+			// WebKit, etc
+			if (startName in document.documentElement) {
+				return true;
+			}
+
+			// Firefox/Gecko
+			var e = document.createElement('div');
+
+			// If no support for basic event stuff, unlikely to have touch support
+			if (!e.setAttribute || !e.removeAttribute) {
+				return false;
+			}
+
+			e.setAttribute(startName, 'return;');
+			if (typeof e[startName] === 'function') {
+				touchSupported = true;
+			}
+
+			e.removeAttribute(startName);
+			e = null;
+
+			return touchSupported;
+		}())
 	};
-	
-	//TODO replace ugly ua sniffing with feature detection
-	
-	L.Browser.touch = L.Browser.mobileWebkit || L.Browser.mobileOpera;
-})();
\ No newline at end of file
+}());
diff --git a/src/core/Class.js b/src/core/Class.js
index 09a9e53..e1bfc0d 100644
--- a/src/core/Class.js
+++ b/src/core/Class.js
@@ -2,43 +2,50 @@
  * Class powers the OOP facilities of the library. Thanks to John Resig and Dean Edwards for inspiration!
  */
 
-L.Class = function() {}; 
+L.Class = function () {};
+
+L.Class.extend = function (/*Object*/ props) /*-> Class*/ {
 
-L.Class.extend = function(/*Object*/ props) /*-> Class*/ {
-	
 	// extended class with the new prototype
-	var NewClass = function() {
-		if (!L.Class._prototyping && this.initialize) {
+	var NewClass = function () {
+		if (this.initialize) {
 			this.initialize.apply(this, arguments);
 		}
 	};
 
 	// instantiate class without calling constructor
-	L.Class._prototyping = true;
-	var proto = new this();
-	L.Class._prototyping = false;
+	var F = function () {};
+	F.prototype = this.prototype;
+	var proto = new F();
 
 	proto.constructor = NewClass;
 	NewClass.prototype = proto;
-	
+
 	// add superclass access
-	proto.superclass = this.prototype;
-	
+	NewClass.superclass = this.prototype;
+
 	// add class name
 	//proto.className = props;
-	
+
+	//inherit parent's statics
+	for (var i in this) {
+		if (this.hasOwnProperty(i) && i !== 'prototype' && i !== 'superclass') {
+			NewClass[i] = this[i];
+		}
+	}
+
 	// mix static properties into the class
 	if (props.statics) {
 		L.Util.extend(NewClass, props.statics);
 		delete props.statics;
 	}
-	
+
 	// mix includes into the prototype
 	if (props.includes) {
 		L.Util.extend.apply(null, [proto].concat(props.includes));
 		delete props.includes;
 	}
-	
+
 	// merge options
 	if (props.options && proto.options) {
 		props.options = L.Util.extend({}, proto.options, props.options);
@@ -46,21 +53,14 @@ L.Class.extend = function(/*Object*/ props) /*-> Class*/ {
 
 	// mix given properties into the prototype
 	L.Util.extend(proto, props);
-	
+
 	// allow inheriting further
-	NewClass.extend = arguments.callee;
-	
+	NewClass.extend = L.Class.extend;
+
 	// method for adding properties to prototype
-	NewClass.include = function(props) {
+	NewClass.include = function (props) {
 		L.Util.extend(this.prototype, props);
 	};
-	
-	//inherit parent's statics
-	for (var i in this) {
-		if (this.hasOwnProperty(i) && i != 'prototype') {
-			NewClass[i] = this[i];
-		}
-	}
-	
+
 	return NewClass;
-};
\ No newline at end of file
+};
diff --git a/src/core/Events.js b/src/core/Events.js
index 53ea20f..f010b39 100644
--- a/src/core/Events.js
+++ b/src/core/Events.js
@@ -1,31 +1,33 @@
 /*
- * L.Mixin.Events adds custom events functionality to Leaflet classes 
+ * L.Mixin.Events adds custom events functionality to Leaflet classes
  */
 
 L.Mixin = {};
 
 L.Mixin.Events = {
-	addEventListener: function(/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
+	addEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
 		var events = this._leaflet_events = this._leaflet_events || {};
 		events[type] = events[type] || [];
 		events[type].push({
 			action: fn,
-			context: context
+			context: context || this
 		});
 		return this;
 	},
-	
-	hasEventListeners: function(/*String*/ type) /*-> Boolean*/ {
+
+	hasEventListeners: function (/*String*/ type) /*-> Boolean*/ {
 		var k = '_leaflet_events';
 		return (k in this) && (type in this[k]) && (this[k][type].length > 0);
 	},
-	
-	removeEventListener: function(/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
-		if (!this.hasEventListeners(type)) { return this; }
-		
+
+	removeEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
+		if (!this.hasEventListeners(type)) {
+			return this;
+		}
+
 		for (var i = 0, events = this._leaflet_events, len = events[type].length; i < len; i++) {
 			if (
-				(events[type][i].action === fn) && 
+				(events[type][i].action === fn) &&
 				(!context || (events[type][i].context === context))
 			) {
 				events[type].splice(i, 1);
@@ -34,25 +36,27 @@ L.Mixin.Events = {
 		}
 		return this;
 	},
-	
-	fireEvent: function(/*String*/ type, /*(optional) Object*/ data) {
-		if (!this.hasEventListeners(type)) { return; }
-		
+
+	fireEvent: function (/*String*/ type, /*(optional) Object*/ data) {
+		if (!this.hasEventListeners(type)) {
+			return this;
+		}
+
 		var event = L.Util.extend({
 			type: type,
 			target: this
 		}, data);
-		
+
 		var listeners = this._leaflet_events[type].slice();
-		
+
 		for (var i = 0, len = listeners.length; i < len; i++) {
 			listeners[i].action.call(listeners[i].context || this, event);
 		}
-		
+
 		return this;
 	}
 };
 
 L.Mixin.Events.on = L.Mixin.Events.addEventListener;
 L.Mixin.Events.off = L.Mixin.Events.removeEventListener;
-L.Mixin.Events.fire = L.Mixin.Events.fireEvent;
\ No newline at end of file
+L.Mixin.Events.fire = L.Mixin.Events.fireEvent;
diff --git a/src/core/Handler.js b/src/core/Handler.js
new file mode 100644
index 0000000..4a7eeec
--- /dev/null
+++ b/src/core/Handler.js
@@ -0,0 +1,29 @@
+/*
+ * L.Handler classes are used internally to inject interaction features to classes like Map and Marker.
+ */
+
+L.Handler = L.Class.extend({
+	initialize: function (map) {
+		this._map = map;
+	},
+
+	enable: function () {
+		if (this._enabled) {
+			return;
+		}
+		this._enabled = true;
+		this.addHooks();
+	},
+
+	disable: function () {
+		if (!this._enabled) {
+			return;
+		}
+		this._enabled = false;
+		this.removeHooks();
+	},
+
+	enabled: function () {
+		return !!this._enabled;
+	}
+});
diff --git a/src/core/Util.js b/src/core/Util.js
index 28daa28..1bd85e2 100644
--- a/src/core/Util.js
+++ b/src/core/Util.js
@@ -3,7 +3,7 @@
  */
 
 L.Util = {
-	extend: function(/*Object*/ dest) /*-> Object*/ {	// merge src properties into dest
+	extend: function (/*Object*/ dest) /*-> Object*/ {	// merge src properties into dest
 		var sources = Array.prototype.slice.call(arguments, 1);
 		for (var j = 0, len = sources.length, src; j < len; j++) {
 			src = sources[j] || {};
@@ -16,54 +16,54 @@ L.Util = {
 		return dest;
 	},
 
-	bind: function(/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
-		return function() {
+	bind: function (/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
+		return function () {
 			return fn.apply(obj, arguments);
 		};
 	},
 
-	stamp: (function() {
+	stamp: (function () {
 		var lastId = 0, key = '_leaflet_id';
-		return function(/*Object*/ obj) {
+		return function (/*Object*/ obj) {
 			obj[key] = obj[key] || ++lastId;
 			return obj[key];
 		};
-	})(),
-	
-	requestAnimFrame: (function() {
+	}()),
+
+	requestAnimFrame: (function () {
 		function timeoutDefer(callback) {
 			window.setTimeout(callback, 1000 / 60);
 		}
-		
-		var requestFn = window.requestAnimationFrame || 
-			window.webkitRequestAnimationFrame || 
-			window.mozRequestAnimationFrame || 
-			window.oRequestAnimationFrame || 
-			window.msRequestAnimationFrame || 
+
+		var requestFn = window.requestAnimationFrame ||
+			window.webkitRequestAnimationFrame ||
+			window.mozRequestAnimationFrame ||
+			window.oRequestAnimationFrame ||
+			window.msRequestAnimationFrame ||
 			timeoutDefer;
-		
-		return function(callback, context, immediate) {
-			callback = context ? L.Util.bind(callback, context) : context;
+
+		return function (callback, context, immediate, contextEl) {
+			callback = context ? L.Util.bind(callback, context) : callback;
 			if (immediate && requestFn === timeoutDefer) {
 				callback();
 			} else {
-				requestFn(callback);
+				requestFn(callback, contextEl);
 			}
 		};
-	})(),
+	}()),
 
-	limitExecByInterval: function(fn, time, context) {	
+	limitExecByInterval: function (fn, time, context) {
 		var lock, execOnUnlock, args;
-		function exec(){
+		function exec() {
 			lock = false;
 			if (execOnUnlock) {
 				args.callee.apply(context, args);
 				execOnUnlock = false;
 			}
 		}
-		return function() {
+		return function () {
 			args = arguments;
-			if (!lock) {				
+			if (!lock) {
 				lock = true;
 				setTimeout(exec, time);
 				fn.apply(context, args);
@@ -72,19 +72,21 @@ L.Util = {
 			}
 		};
 	},
-	
-	falseFn: function() { return false; },
-	
-	formatNum: function(num, digits) {
+
+	falseFn: function () {
+		return false;
+	},
+
+	formatNum: function (num, digits) {
 		var pow = Math.pow(10, digits || 5);
 		return Math.round(num * pow) / pow;
 	},
-	
-	setOptions: function(obj, options) {
+
+	setOptions: function (obj, options) {
 		obj.options = L.Util.extend({}, obj.options, options);
 	},
-	
-	getParamString: function(obj) {
+
+	getParamString: function (obj) {
 		var params = [];
 		for (var i in obj) {
 			if (obj.hasOwnProperty(i)) {
@@ -92,5 +94,15 @@ L.Util = {
 			}
 		}
 		return '?' + params.join('&');
+	},
+
+	template: function (str, data) {
+		return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
+			var value = data[key];
+			if (!data.hasOwnProperty(key)) {
+				throw new Error('No value provided for variable ' + str);
+			}
+			return value;
+		});
 	}
 };
diff --git a/src/dom/DomEvent.DoubleTap.js b/src/dom/DomEvent.DoubleTap.js
index 08bd79b..adacb00 100644
--- a/src/dom/DomEvent.DoubleTap.js
+++ b/src/dom/DomEvent.DoubleTap.js
@@ -1,6 +1,6 @@
 L.Util.extend(L.DomEvent, {
 	// inspired by Zepto touch code by Thomas Fuchs
-	addDoubleTapListener: function(obj, handler, id) {
+	addDoubleTapListener: function (obj, handler, id) {
 		var last,
 			doubleTap = false,
 			delay = 250,
@@ -8,13 +8,15 @@ L.Util.extend(L.DomEvent, {
 			pre = '_leaflet_',
 			touchstart = 'touchstart',
 			touchend = 'touchend';
-		
+
 		function onTouchStart(e) {
-			if (e.touches.length != 1) return;
-			
-			var now = Date.now(), 
+			if (e.touches.length !== 1) {
+				return;
+			}
+
+			var now = Date.now(),
 				delta = now - (last || now);
-			
+
 			touch = e.touches[0];
 			doubleTap = (delta > 0 && delta <= delay);
 			last = now;
@@ -28,14 +30,14 @@ L.Util.extend(L.DomEvent, {
 		}
 		obj[pre + touchstart + id] = onTouchStart;
 		obj[pre + touchend + id] = onTouchEnd;
-		
+
 		obj.addEventListener(touchstart, onTouchStart, false);
 		obj.addEventListener(touchend, onTouchEnd, false);
 	},
-	
-	removeDoubleTapListener: function(obj, id) {
+
+	removeDoubleTapListener: function (obj, id) {
 		var pre = '_leaflet_';
 		obj.removeEventListener(obj, obj[pre + 'touchstart' + id], false);
 		obj.removeEventListener(obj, obj[pre + 'touchend' + id], false);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js
index bcabebc..5ac4f07 100644
--- a/src/dom/DomEvent.js
+++ b/src/dom/DomEvent.js
@@ -4,24 +4,31 @@
 
 L.DomEvent = {
 	/* inpired by John Resig, Dean Edwards and YUI addEvent implementations */
-	addListener: function(/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn, /*Object*/ context) {
-		var id = L.Util.stamp(fn);
-		
-		function handler(e) {
-			return fn.call(context || obj, e || L.DomEvent._getEvent());
+	addListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn, /*Object*/ context) {
+		var id = L.Util.stamp(fn),
+			key = '_leaflet_' + type + id;
+
+		if (obj[key]) {
+			return;
 		}
-		
-		if (L.Browser.touch && (type == 'dblclick') && this.addDoubleTapListener) {
+
+		var handler = function (e) {
+			return fn.call(context || obj, e || L.DomEvent._getEvent());
+		};
+
+		if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
 			this.addDoubleTapListener(obj, handler, id);
 		} else if ('addEventListener' in obj) {
-			if (type == 'mousewheel') {
+			if (type === 'mousewheel') {
 				obj.addEventListener('DOMMouseScroll', handler, false);
 				obj.addEventListener(type, handler, false);
-			} else if ((type == 'mouseenter') || (type == 'mouseleave')) {
+			} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
 				var originalHandler = handler,
-					newType = (type == 'mouseenter' ? 'mouseover' : 'mouseout');
-				handler = function(e) {
-					if (!L.DomEvent._checkMouse(obj, e)) return;
+					newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
+				handler = function (e) {
+					if (!L.DomEvent._checkMouse(obj, e)) {
+						return;
+					}
 					return originalHandler(e);
 				};
 				obj.addEventListener(newType, handler, false);
@@ -31,102 +38,117 @@ L.DomEvent = {
 		} else if ('attachEvent' in obj) {
 			obj.attachEvent("on" + type, handler);
 		}
-		
-		obj['_leaflet_' + type + id] = handler;
+
+		obj[key] = handler;
 	},
-	
-	removeListener: function(/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn) {
+
+	removeListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn) {
 		var id = L.Util.stamp(fn),
-			key = '_leaflet_' + type + id;
+			key = '_leaflet_' + type + id,
 			handler = obj[key];
-			
-		if (L.Browser.mobileWebkit && (type == 'dblclick') && this.removeDoubleTapListener) {
+
+		if (!handler) {
+			return;
+		}
+
+		if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
 			this.removeDoubleTapListener(obj, id);
 		} else if ('removeEventListener' in obj) {
-			if (type == 'mousewheel') {
+			if (type === 'mousewheel') {
 				obj.removeEventListener('DOMMouseScroll', handler, false);
 				obj.removeEventListener(type, handler, false);
-			} else if ((type == 'mouseenter') || (type == 'mouseleave')) {
-				obj.removeEventListener((type == 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false);
+			} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
+				obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false);
 			} else {
 				obj.removeEventListener(type, handler, false);
 			}
 		} else if ('detachEvent' in obj) {
 			obj.detachEvent("on" + type, handler);
 		}
-		obj[key] = null; 
+		obj[key] = null;
 	},
-	
-	_checkMouse: function(el, e) {
+
+	_checkMouse: function (el, e) {
 		var related = e.relatedTarget;
-		
-		if (!related) return true;
-		
+
+		if (!related) {
+			return true;
+		}
+
 		try {
-			while (related && (related != el)) { 
-				related = related.parentNode; 
+			while (related && (related !== el)) {
+				related = related.parentNode;
 			}
-		} catch(err) { return false; }
-		
-		return (related != el);
+		} catch (err) {
+			return false;
+		}
+
+		return (related !== el);
 	},
-	
-	_getEvent: function()/*->Event*/ {
+
+	/*jshint noarg:false */ // evil magic for IE
+	_getEvent: function () {
 		var e = window.event;
 		if (!e) {
 			var caller = arguments.callee.caller;
 			while (caller) {
 				e = caller['arguments'][0];
-				if (e && Event == e.constructor) { break; }
+				if (e && window.Event === e.constructor) {
+					break;
+				}
 				caller = caller.caller;
 			}
 		}
 		return e;
 	},
-	
-	stopPropagation: function(/*Event*/ e) {
+	/*jshint noarg:false */
+
+	stopPropagation: function (/*Event*/ e) {
 		if (e.stopPropagation) {
 			e.stopPropagation();
 		} else {
 			e.cancelBubble = true;
 		}
 	},
-	
-	disableClickPropagation: function(/*HTMLElement*/ el) {
-		L.DomEvent.addListener(el, 'mousedown', L.DomEvent.stopPropagation);
+
+	disableClickPropagation: function (/*HTMLElement*/ el) {
+		L.DomEvent.addListener(el, L.Draggable.START, L.DomEvent.stopPropagation);
 		L.DomEvent.addListener(el, 'click', L.DomEvent.stopPropagation);
 		L.DomEvent.addListener(el, 'dblclick', L.DomEvent.stopPropagation);
 	},
-	
-	preventDefault: function(/*Event*/ e) {
+
+	preventDefault: function (/*Event*/ e) {
 		if (e.preventDefault) {
 			e.preventDefault();
 		} else {
 			e.returnValue = false;
 		}
 	},
-	
-	stop: function(e) {
+
+	stop: function (e) {
 		L.DomEvent.preventDefault(e);
 		L.DomEvent.stopPropagation(e);
 	},
-	
-	getMousePosition: function(e, container) {
-		var x = e.pageX ? e.pageX : e.clientX + 
+
+	getMousePosition: function (e, container) {
+		var x = e.pageX ? e.pageX : e.clientX +
 				document.body.scrollLeft + document.documentElement.scrollLeft,
-			y = e.pageY ? e.pageY : e.clientY + 
+			y = e.pageY ? e.pageY : e.clientY +
 					document.body.scrollTop + document.documentElement.scrollTop,
 			pos = new L.Point(x, y);
-			
-		return (container ? 
-					pos.subtract(L.DomUtil.getCumulativeOffset(container)) : pos);
+		return (container ?
+					pos.subtract(L.DomUtil.getViewportOffset(container)) : pos);
 	},
-	
-	getWheelDelta: function(e) {
+
+	getWheelDelta: function (e) {
 		var delta = 0;
-		if (e.wheelDelta) { delta = e.wheelDelta/120; }
-			if (e.detail) { delta = -e.detail/3; }
-			return delta;
+		if (e.wheelDelta) {
+			delta = e.wheelDelta / 120;
+		}
+		if (e.detail) {
+			delta = -e.detail / 3;
+		}
+		return delta;
 	}
 };
 
diff --git a/src/dom/DomUtil.js b/src/dom/DomUtil.js
index 7672bfb..db10201 100644
--- a/src/dom/DomUtil.js
+++ b/src/dom/DomUtil.js
@@ -3,34 +3,56 @@
  */
 
 L.DomUtil = {
-	get: function(id) {
-		return (typeof id == 'string' ? document.getElementById(id) : id);
+	get: function (id) {
+		return (typeof id === 'string' ? document.getElementById(id) : id);
 	},
-	
-	getStyle: function(el, style) {
+
+	getStyle: function (el, style) {
 		var value = el.style[style];
 		if (!value && el.currentStyle) {
 			value = el.currentStyle[style];
 		}
-		if (!value || value == 'auto') {
+		if (!value || value === 'auto') {
 			var css = document.defaultView.getComputedStyle(el, null);
 			value = css ? css[style] : null;
 		}
-		return (value == 'auto' ? null : value);
+		return (value === 'auto' ? null : value);
 	},
-	
-	getCumulativeOffset: function(el) {
-		var top = 0, 
-			left = 0;
+
+	getViewportOffset: function (element) {
+		var top = 0,
+			left = 0,
+			el = element,
+			docBody = document.body;
+
 		do {
 			top += el.offsetTop || 0;
 			left += el.offsetLeft || 0;
+
+			if (el.offsetParent === docBody &&
+					L.DomUtil.getStyle(el, 'position') === 'absolute') {
+				break;
+			}
 			el = el.offsetParent;
 		} while (el);
+
+		el = element;
+
+		do {
+			if (el === docBody) {
+				break;
+			}
+
+			top -= el.scrollTop || 0;
+			left -= el.scrollLeft || 0;
+
+			el = el.parentNode;
+		} while (el);
+
 		return new L.Point(left, top);
 	},
-	
-	create: function(tagName, className, container) {
+
+	create: function (tagName, className, container) {
 		var el = document.createElement(tagName);
 		el.className = className;
 		if (container) {
@@ -38,9 +60,9 @@ L.DomUtil = {
 		}
 		return el;
 	},
-	
-	disableTextSelection: function() {
-		if (document.selection && document.selection.empty) { 
+
+	disableTextSelection: function () {
+		if (document.selection && document.selection.empty) {
 			document.selection.empty();
 		}
 		if (!this._onselectstart) {
@@ -48,38 +70,45 @@ L.DomUtil = {
 			document.onselectstart = L.Util.falseFn;
 		}
 	},
-	
-	enableTextSelection: function() {
+
+	enableTextSelection: function () {
 		document.onselectstart = this._onselectstart;
 		this._onselectstart = null;
 	},
-	
-	CLASS_RE: /(\\s|^)'+cls+'(\\s|$)/,
-	
-	hasClass: function(el, name) {
-		return (el.className.length > 0) && 
+
+	hasClass: function (el, name) {
+		return (el.className.length > 0) &&
 				new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);
 	},
-	
-	addClass: function(el, name) {
+
+	addClass: function (el, name) {
 		if (!L.DomUtil.hasClass(el, name)) {
-			el.className += (el.className ? ' ' : '') + name; 
+			el.className += (el.className ? ' ' : '') + name;
 		}
 	},
-	
-	setOpacity: function(el, value) {
+
+	removeClass: function (el, name) {
+		el.className = el.className.replace(/(\S+)\s*/g, function (w, match) {
+			if (match === name) {
+				return '';
+			}
+			return w;
+		}).replace(/^\s+/, '');
+	},
+
+	setOpacity: function (el, value) {
 		if (L.Browser.ie) {
 			el.style.filter = 'alpha(opacity=' + Math.round(value * 100) + ')';
 		} else {
 			el.style.opacity = value;
 		}
 	},
-	
+
 	//TODO refactor away this ugly translate/position mess
-	
-	testProp: function(props) {
+
+	testProp: function (props) {
 		var style = document.documentElement.style;
-		
+
 		for (var i = 0; i < props.length; i++) {
 			if (props[i] in style) {
 				return props[i];
@@ -87,30 +116,37 @@ L.DomUtil = {
 		}
 		return false;
 	},
-	
-	getTranslateString: function(point) {
-		return L.DomUtil.TRANSLATE_OPEN + 
-				point.x + 'px,' + point.y + 'px' + 
+
+	getTranslateString: function (point) {
+		return L.DomUtil.TRANSLATE_OPEN +
+				point.x + 'px,' + point.y + 'px' +
 				L.DomUtil.TRANSLATE_CLOSE;
 	},
-	
-	getScaleString: function(scale, origin) {
-		 return L.DomUtil.getTranslateString(origin) + 
-         		' scale(' + scale + ') ' +
-         		L.DomUtil.getTranslateString(origin.multiplyBy(-1));
+
+	getScaleString: function (scale, origin) {
+		var preTranslateStr = L.DomUtil.getTranslateString(origin),
+			scaleStr = ' scale(' + scale + ') ',
+			postTranslateStr = L.DomUtil.getTranslateString(origin.multiplyBy(-1));
+
+		return preTranslateStr + scaleStr + postTranslateStr;
 	},
-	
-	setPosition: function(el, point) {
+
+	setPosition: function (el, point) {
 		el._leaflet_pos = point;
-		if (L.Browser.webkit) {
+		if (L.Browser.webkit3d) {
 			el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
+
+			if (L.Browser.android) {
+				el.style['-webkit-perspective'] = '1000';
+				el.style['-webkit-backface-visibility'] = 'hidden';
+			}
 		} else {
 			el.style.left = point.x + 'px';
 			el.style.top = point.y + 'px';
 		}
 	},
-	
-	getPosition: function(el) {
+
+	getPosition: function (el) {
 		return el._leaflet_pos;
 	}
 };
@@ -118,7 +154,7 @@ L.DomUtil = {
 L.Util.extend(L.DomUtil, {
 	TRANSITION: L.DomUtil.testProp(['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']),
 	TRANSFORM: L.DomUtil.testProp(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']),
-	
+
 	TRANSLATE_OPEN: 'translate' + (L.Browser.webkit3d ? '3d(' : '('),
 	TRANSLATE_CLOSE: L.Browser.webkit3d ? ',0)' : ')'
-});
\ No newline at end of file
+});
diff --git a/src/dom/Draggable.js b/src/dom/Draggable.js
index c0aea23..53a0091 100644
--- a/src/dom/Draggable.js
+++ b/src/dom/Draggable.js
@@ -4,126 +4,144 @@
 
 L.Draggable = L.Class.extend({
 	includes: L.Mixin.Events,
-	
+
 	statics: {
 		START: L.Browser.touch ? 'touchstart' : 'mousedown',
 		END: L.Browser.touch ? 'touchend' : 'mouseup',
 		MOVE: L.Browser.touch ? 'touchmove' : 'mousemove',
 		TAP_TOLERANCE: 15
 	},
-	
-	initialize: function(element, dragStartTarget) {
+
+	initialize: function (element, dragStartTarget) {
 		this._element = element;
 		this._dragStartTarget = dragStartTarget || element;
 	},
-	
-	enable: function() {
-		if (this._enabled) { return; }
+
+	enable: function () {
+		if (this._enabled) {
+			return;
+		}
 		L.DomEvent.addListener(this._dragStartTarget, L.Draggable.START, this._onDown, this);
 		this._enabled = true;
 	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
+
+	disable: function () {
+		if (!this._enabled) {
+			return;
+		}
 		L.DomEvent.removeListener(this._dragStartTarget, L.Draggable.START, this._onDown);
 		this._enabled = false;
 	},
 
-	_onDown: function(e) {
-		if (e.shiftKey || ((e.which != 1) && (e.button != 1) && !e.touches)) { return; }
-		
-		if (e.touches && e.touches.length > 1) { return; }
+	_onDown: function (e) {
+		if ((!L.Browser.touch && e.shiftKey) || ((e.which !== 1) && (e.button !== 1) && !e.touches)) {
+			return;
+		}
+
+		if (e.touches && e.touches.length > 1) {
+			return;
+		}
+
+		var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
+			el = first.target;
 
-		var first = (e.touches && e.touches.length == 1 ? e.touches[0] : e);
-		
 		L.DomEvent.preventDefault(e);
-			
-		if (L.Browser.mobileWebkit) {
-			first.target.className += ' leaflet-active';
+
+		if (L.Browser.touch && el.tagName.toLowerCase() === 'a') {
+			el.className += ' leaflet-active';
 		}
-		
+
 		this._moved = false;
-		
-		L.DomUtil.disableTextSelection();
-		this._setMovingCursor();
-		
+		if (this._moving) {
+			return;
+		}
+
+		if (!L.Browser.touch) {
+			L.DomUtil.disableTextSelection();
+			this._setMovingCursor();
+		}
+
 		this._startPos = this._newPos = L.DomUtil.getPosition(this._element);
 		this._startPoint = new L.Point(first.clientX, first.clientY);
-		
+
 		L.DomEvent.addListener(document, L.Draggable.MOVE, this._onMove, this);
 		L.DomEvent.addListener(document, L.Draggable.END, this._onUp, this);
 	},
-	
-	_onMove: function(e) {
-		if (e.touches && e.touches.length > 1) { return; }
+
+	_onMove: function (e) {
+		if (e.touches && e.touches.length > 1) {
+			return;
+		}
 
 		L.DomEvent.preventDefault(e);
-		
-		var first = (e.touches && e.touches.length == 1 ? e.touches[0] : e);
-		
+
+		var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e);
+
 		if (!this._moved) {
 			this.fire('dragstart');
 			this._moved = true;
 		}
+		this._moving = true;
 
 		var newPoint = new L.Point(first.clientX, first.clientY);
 		this._newPos = this._startPos.add(newPoint).subtract(this._startPoint);
-		
-		L.Util.requestAnimFrame(this._updatePosition, this, true);
-		
-		this.fire('drag');
+
+		L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
 	},
-	
-	_updatePosition: function() {
+
+	_updatePosition: function () {
+		this.fire('predrag');
 		L.DomUtil.setPosition(this._element, this._newPos);
+		this.fire('drag');
 	},
-	
-	_onUp: function(e) {
+
+	_onUp: function (e) {
 		if (e.changedTouches) {
 			var first = e.changedTouches[0],
 				el = first.target,
 				dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0;
-			
-			el.className = el.className.replace(' leaflet-active', '');
-			
+
+			if (el.tagName.toLowerCase() === 'a') {
+				el.className = el.className.replace(' leaflet-active', '');
+			}
+
 			if (dist < L.Draggable.TAP_TOLERANCE) {
 				this._simulateEvent('click', first);
 			}
 		}
-		
-		L.DomUtil.enableTextSelection();
-		
-		this._restoreCursor();
-		
+
+		if (!L.Browser.touch) {
+			L.DomUtil.enableTextSelection();
+			this._restoreCursor();
+		}
+
 		L.DomEvent.removeListener(document, L.Draggable.MOVE, this._onMove);
 		L.DomEvent.removeListener(document, L.Draggable.END, this._onUp);
-		
+
 		if (this._moved) {
 			this.fire('dragend');
 		}
+		this._moving = false;
 	},
-	
-	_removeActiveClass: function(el) {
-	},
-	
-	_setMovingCursor: function() {
+
+	_setMovingCursor: function () {
 		this._bodyCursor = document.body.style.cursor;
 		document.body.style.cursor = 'move';
 	},
-	
-	_restoreCursor: function() {
+
+	_restoreCursor: function () {
 		document.body.style.cursor = this._bodyCursor;
 	},
-	
-	_simulateEvent: function(type, e) {
-		var simulatedEvent = document.createEvent('MouseEvent');
-		
+
+	_simulateEvent: function (type, e) {
+		var simulatedEvent = document.createEvent('MouseEvents');
+
 		simulatedEvent.initMouseEvent(
-				type, true, true, window, 1, 
-				e.screenX, e.screenY, 
-				e.clientX, e.clientY, 
+				type, true, true, window, 1,
+				e.screenX, e.screenY,
+				e.clientX, e.clientY,
 				false, false, false, false, 0, null);
-		
+
 		e.target.dispatchEvent(simulatedEvent);
 	}
 });
diff --git a/src/dom/transition/Transition.Native.js b/src/dom/transition/Transition.Native.js
index 6ce16a6..05cc381 100644
--- a/src/dom/transition/Transition.Native.js
+++ b/src/dom/transition/Transition.Native.js
@@ -1,89 +1,102 @@
 /*
- * L.Transition native implementation that powers Leaflet animation 
+ * L.Transition native implementation that powers Leaflet animation
  * in browsers that support CSS3 Transitions
  */
 
 L.Transition = L.Transition.extend({
-	statics: (function() {
+	statics: (function () {
 		var transition = L.DomUtil.TRANSITION,
-			transitionEnd = (transition == 'webkitTransition' || transition == 'OTransition' ? 
+			transitionEnd = (transition === 'webkitTransition' || transition === 'OTransition' ?
 				transition + 'End' : 'transitionend');
-		
+
 		return {
 			NATIVE: !!transition,
-			
+
 			TRANSITION: transition,
 			PROPERTY: transition + 'Property',
 			DURATION: transition + 'Duration',
 			EASING: transition + 'TimingFunction',
 			END: transitionEnd,
-			
+
 			// transition-property value to use with each particular custom property
 			CUSTOM_PROPS_PROPERTIES: {
 				position: L.Browser.webkit ? L.DomUtil.TRANSFORM : 'top, left'
 			}
 		};
-	})(),
-	
+	}()),
+
 	options: {
 		fakeStepInterval: 100
 	},
-	
-	initialize: function(/*HTMLElement*/ el, /*Object*/ options) {
+
+	initialize: function (/*HTMLElement*/ el, /*Object*/ options) {
 		this._el = el;
 		L.Util.setOptions(this, options);
 
 		L.DomEvent.addListener(el, L.Transition.END, this._onTransitionEnd, this);
 		this._onFakeStep = L.Util.bind(this._onFakeStep, this);
 	},
-	
-	run: function(/*Object*/ props) {
+
+	run: function (/*Object*/ props) {
 		var prop,
 			propsList = [],
 			customProp = L.Transition.CUSTOM_PROPS_PROPERTIES;
-		
+
 		for (prop in props) {
 			if (props.hasOwnProperty(prop)) {
 				prop = customProp[prop] ? customProp[prop] : prop;
-				prop = prop.replace(/([A-Z])/g, function(w) { return '-' + w.toLowerCase(); });
+				prop = this._dasherize(prop);
 				propsList.push(prop);
 			}
 		}
-		
+
 		this._el.style[L.Transition.DURATION] = this.options.duration + 's';
 		this._el.style[L.Transition.EASING] = this.options.easing;
 		this._el.style[L.Transition.PROPERTY] = propsList.join(', ');
-		
+
 		for (prop in props) {
 			if (props.hasOwnProperty(prop)) {
 				this._setProperty(prop, props[prop]);
 			}
 		}
-		
+
 		this._inProgress = true;
-		
+
 		this.fire('start');
-		
+
 		if (L.Transition.NATIVE) {
+			clearInterval(this._timer);
 			this._timer = setInterval(this._onFakeStep, this.options.fakeStepInterval);
 		} else {
 			this._onTransitionEnd();
 		}
 	},
-	
-	_onFakeStep: function() {
+
+	_dasherize: (function () {
+		var re = /([A-Z])/g;
+
+		function replaceFn(w) {
+			return '-' + w.toLowerCase();
+		}
+
+		return function (str) {
+			return str.replace(re, replaceFn);
+		};
+	}()),
+
+	_onFakeStep: function () {
 		this.fire('step');
 	},
-	
-	_onTransitionEnd: function() {
+
+	_onTransitionEnd: function () {
 		if (this._inProgress) {
 			this._inProgress = false;
 			clearInterval(this._timer);
-			
+
 			this._el.style[L.Transition.PROPERTY] = 'none';
-		
+
 			this.fire('step');
 			this.fire('end');
 		}
 	}
-});
\ No newline at end of file
+});
diff --git a/src/dom/transition/Transition.Timer.js b/src/dom/transition/Transition.Timer.js
index af4e4ef..7fda493 100644
--- a/src/dom/transition/Transition.Timer.js
+++ b/src/dom/transition/Transition.Timer.js
@@ -1,14 +1,16 @@
 /*
- * L.Transition fallback implementation that powers Leaflet animation 
+ * L.Transition fallback implementation that powers Leaflet animation
  * in browsers that don't support CSS3 Transitions
  */
 
 L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 	statics: {
-		getTime: Date.now || function() { return +new Date(); },
-		
+		getTime: Date.now || function () {
+			return +new Date();
+		},
+
 		TIMER: true,
-		
+
 		EASINGS: {
 			'ease': [0.25, 0.1, 0.25, 1.0],
 			'linear': [0.0, 0.0, 1.0, 1.0],
@@ -16,42 +18,42 @@ L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 			'ease-out': [0, 0, 0.58, 1.0],
 			'ease-in-out': [0.42, 0, 0.58, 1.0]
 		},
-		
+
 		CUSTOM_PROPS_GETTERS: {
 			position: L.DomUtil.getPosition
 		},
-		
+
 		//used to get units from strings like "10.5px" (->px)
 		UNIT_RE: /^[\d\.]+(\D*)$/
 	},
-	
+
 	options: {
 		fps: 50
 	},
-	
-	initialize: function(el, options) {
+
+	initialize: function (el, options) {
 		this._el = el;
 		L.Util.extend(this.options, options);
 
-		var easings = L.Transition.EASINGS[this.options.easing] || L.Transition.EASINGS['ease'];
-		
+		var easings = L.Transition.EASINGS[this.options.easing] || L.Transition.EASINGS.ease;
+
 		this._p1 = new L.Point(0, 0);
 		this._p2 = new L.Point(easings[0], easings[1]);
 		this._p3 = new L.Point(easings[2], easings[3]);
 		this._p4 = new L.Point(1, 1);
-		
+
 		this._step = L.Util.bind(this._step, this);
 		this._interval = Math.round(1000 / this.options.fps);
 	},
-	
-	run: function(props) {
+
+	run: function (props) {
 		this._props = {};
-		
+
 		var getters = L.Transition.CUSTOM_PROPS_GETTERS,
 			re = L.Transition.UNIT_RE;
-		
+
 		this.fire('start');
-		
+
 		for (var prop in props) {
 			if (props.hasOwnProperty(prop)) {
 				var p = {};
@@ -66,17 +68,17 @@ L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 				this._props[prop] = p;
 			}
 		}
-		
+
 		clearInterval(this._timer);
 		this._timer = setInterval(this._step, this._interval);
 		this._startTime = L.Transition.getTime();
 	},
-	
-	_step: function() {
+
+	_step: function () {
 		var time = L.Transition.getTime(),
 			elapsed = time - this._startTime,
 			duration = this.options.duration * 1000;
-		
+
 		if (elapsed < duration) {
 			this._runFrame(this._cubicBezier(elapsed / duration));
 		} else {
@@ -84,11 +86,11 @@ L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 			this._complete();
 		}
 	},
-	
-	_runFrame: function(percentComplete) {
+
+	_runFrame: function (percentComplete) {
 		var setters = L.Transition.CUSTOM_PROPS_SETTERS,
 			prop, p, value;
-		
+
 		for (prop in this._props) {
 			if (this._props.hasOwnProperty(prop)) {
 				p = this._props[prop];
@@ -96,20 +98,20 @@ L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 					value = p.to.subtract(p.from).multiplyBy(percentComplete).add(p.from);
 					setters[prop](this._el, value);
 				} else {
-					this._el.style[prop] = 
+					this._el.style[prop] =
 							((p.to - p.from) * percentComplete + p.from) + p.unit;
 				}
 			}
 		}
 		this.fire('step');
 	},
-	
-	_complete: function() {
+
+	_complete: function () {
 		clearInterval(this._timer);
 		this.fire('end');
 	},
-	
-	_cubicBezier: function(t) {
+
+	_cubicBezier: function (t) {
 		var a = Math.pow(1 - t, 3),
 			b = 3 * Math.pow(1 - t, 2) * t,
 			c = 3 * (1 - t) * Math.pow(t, 2),
@@ -118,7 +120,7 @@ L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
 			p2 = this._p2.multiplyBy(b),
 			p3 = this._p3.multiplyBy(c),
 			p4 = this._p4.multiplyBy(d);
-		
+
 		return p1.add(p2).add(p3).add(p4).y;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/dom/transition/Transition.js b/src/dom/transition/Transition.js
index ccf4857..4e9273b 100644
--- a/src/dom/transition/Transition.js
+++ b/src/dom/transition/Transition.js
@@ -1,23 +1,23 @@
 L.Transition = L.Class.extend({
 	includes: L.Mixin.Events,
-	
+
 	statics: {
 		CUSTOM_PROPS_SETTERS: {
 			position: L.DomUtil.setPosition
 			//TODO transform custom attr
 		},
-		
-		implemented: function() {
+
+		implemented: function () {
 			return L.Transition.NATIVE || L.Transition.TIMER;
 		}
 	},
-	
+
 	options: {
 		easing: 'ease',
 		duration: 0.5
 	},
-	
-	_setProperty: function(prop, value) {
+
+	_setProperty: function (prop, value) {
 		var setters = L.Transition.CUSTOM_PROPS_SETTERS;
 		if (prop in setters) {
 			setters[prop](this._el, value);
@@ -25,4 +25,4 @@ L.Transition = L.Class.extend({
 			this._el.style[prop] = value;
 		}
 	}
-});
\ No newline at end of file
+});
diff --git a/src/geo/LatLng.js b/src/geo/LatLng.js
index fb91654..03897ac 100644
--- a/src/geo/LatLng.js
+++ b/src/geo/LatLng.js
@@ -2,17 +2,24 @@
 	CM.LatLng represents a geographical point with latitude and longtitude coordinates.
 */
 
-L.LatLng = function(/*Number*/ lat, /*Number*/ lng, /*Boolean*/ noWrap) {
+L.LatLng = function (/*Number*/ rawLat, /*Number*/ rawLng, /*Boolean*/ noWrap) {
+	var lat = parseFloat(rawLat),
+		lng = parseFloat(rawLng);
+
+	if (isNaN(lat) || isNaN(lng)) {
+		throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')');
+	}
+
 	if (noWrap !== true) {
 		lat = Math.max(Math.min(lat, 90), -90);					// clamp latitude into -90..90
-		lng = (lng + 180) % 360 + (lng < -180 ? 180 : -180);	// wrap longtitude into -180..180
+		lng = (lng + 180) % 360 + ((lng < -180 || lng === 180) ? 180 : -180);	// wrap longtitude into -180..180
 	}
-	
+
 	//TODO change to lat() & lng()
 	this.lat = lat;
 	this.lng = lng;
 };
-	
+
 L.Util.extend(L.LatLng, {
 	DEG_TO_RAD: Math.PI / 180,
 	RAD_TO_DEG: 180 / Math.PI,
@@ -20,16 +27,34 @@ L.Util.extend(L.LatLng, {
 });
 
 L.LatLng.prototype = {
-	equals: function(/*LatLng*/ obj) {
-		if (!(obj instanceof L.LatLng)) { return false; }
-		
+	equals: function (/*LatLng*/ obj) {
+		if (!(obj instanceof L.LatLng)) {
+			return false;
+		}
+
 		var margin = Math.max(Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng));
 		return margin <= L.LatLng.MAX_MARGIN;
 	},
-	
-	toString: function() {
-		return 'LatLng(' + 
-				L.Util.formatNum(this.lat) + ', ' + 
+
+	toString: function () {
+		return 'LatLng(' +
+				L.Util.formatNum(this.lat) + ', ' +
 				L.Util.formatNum(this.lng) + ')';
+	},
+
+	// Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula
+	distanceTo: function (/*LatLng*/ other)/*->Double*/ {
+		var R = 6378137, // earth radius in meters
+			d2r = L.LatLng.DEG_TO_RAD,
+			dLat = (other.lat - this.lat) * d2r,
+			dLon = (other.lng - this.lng) * d2r,
+			lat1 = this.lat * d2r,
+			lat2 = other.lat * d2r,
+			sin1 = Math.sin(dLat / 2),
+			sin2 = Math.sin(dLon / 2);
+
+		var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2);
+
+		return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
 	}
-};
\ No newline at end of file
+};
diff --git a/src/geo/LatLngBounds.js b/src/geo/LatLngBounds.js
index c4e70ec..7a87ea4 100644
--- a/src/geo/LatLngBounds.js
+++ b/src/geo/LatLngBounds.js
@@ -3,19 +3,21 @@
  */
 
 L.LatLngBounds = L.Class.extend({
-	initialize: function(southWest, northEast) {	// (LatLng, LatLng) or (LatLng[])
-		if (!southWest) return;
+	initialize: function (southWest, northEast) {	// (LatLng, LatLng) or (LatLng[])
+		if (!southWest) {
+			return;
+		}
 		var latlngs = (southWest instanceof Array ? southWest : [southWest, northEast]);
 		for (var i = 0, len = latlngs.length; i < len; i++) {
 			this.extend(latlngs[i]);
 		}
 	},
-	
+
 	// extend the bounds to contain the given point
-	extend: function(/*LatLng*/ latlng) {
+	extend: function (/*LatLng*/ latlng) {
 		if (!this._southWest && !this._northEast) {
-			this._southWest = new L.LatLng(latlng.lat, latlng.lng);
-			this._northEast = new L.LatLng(latlng.lat, latlng.lng);
+			this._southWest = new L.LatLng(latlng.lat, latlng.lng, true);
+			this._northEast = new L.LatLng(latlng.lat, latlng.lng, true);
 		} else {
 			this._southWest.lat = Math.min(latlng.lat, this._southWest.lat);
 			this._southWest.lng = Math.min(latlng.lng, this._southWest.lng);
@@ -23,40 +25,62 @@ L.LatLngBounds = L.Class.extend({
 			this._northEast.lng = Math.max(latlng.lng, this._northEast.lng);
 		}
 	},
-	
-	getCenter: function() /*-> LatLng*/ {
+
+	getCenter: function () /*-> LatLng*/ {
 		return new L.LatLng(
-				(this._southWest.lat + this._northEast.lat) / 2, 
+				(this._southWest.lat + this._northEast.lat) / 2,
 				(this._southWest.lng + this._northEast.lng) / 2);
 	},
-	
-	getSouthWest: function() { return this._southWest; },
-	
-	getNorthEast: function() { return this._northEast; },
-	
-	getNorthWest: function() {
-		return new L.LatLng(this._northEast.lat, this._southWest.lng);
+
+	getSouthWest: function () {
+		return this._southWest;
 	},
-	
-	getSouthEast: function() {
-		return new L.LatLng(this._southWest.lat, this._northEast.lng);
+
+	getNorthEast: function () {
+		return this._northEast;
 	},
-	
-	contains: function(/*LatLngBounds or LatLng*/ obj) /*-> Boolean*/ {
+
+	getNorthWest: function () {
+		return new L.LatLng(this._northEast.lat, this._southWest.lng, true);
+	},
+
+	getSouthEast: function () {
+		return new L.LatLng(this._southWest.lat, this._northEast.lng, true);
+	},
+
+	contains: function (/*LatLngBounds or LatLng*/ obj) /*-> Boolean*/ {
 		var sw = this._southWest,
 			ne = this._northEast,
 			sw2, ne2;
-		
+
 		if (obj instanceof L.LatLngBounds) {
 			sw2 = obj.getSouthWest();
 			ne2 = obj.getNorthEast();
 		} else {
 			sw2 = ne2 = obj;
 		}
-		
+
 		return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
 				(sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
+	},
+
+	intersects: function (/*LatLngBounds*/ bounds) {
+		var sw = this._southWest,
+			ne = this._northEast,
+			sw2 = bounds.getSouthWest(),
+			ne2 = bounds.getNorthEast();
+
+		var latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
+			lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
+
+		return latIntersects && lngIntersects;
+	},
+
+	toBBoxString: function () {
+		var sw = this._southWest,
+			ne = this._northEast;
+		return [sw.lng, sw.lat, ne.lng, ne.lat].join(',');
 	}
 });
 
-//TODO International date line?
\ No newline at end of file
+//TODO International date line?
diff --git a/src/geo/crs/CRS.EPSG3395.js b/src/geo/crs/CRS.EPSG3395.js
index 426dc73..a0d40a9 100644
--- a/src/geo/crs/CRS.EPSG3395.js
+++ b/src/geo/crs/CRS.EPSG3395.js
@@ -1,13 +1,13 @@
 
 L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, {
 	code: 'EPSG:3395',
-	
+
 	projection: L.Projection.Mercator,
-	transformation: (function() { 
+	transformation: (function () {
 		var m = L.Projection.Mercator,
 			r = m.R_MAJOR,
 			r2 = m.R_MINOR;
-		
-		return new L.Transformation(0.5/(Math.PI * r), 0.5, -0.5/(Math.PI * r2), 0.5);
-	})()
-});
\ No newline at end of file
+
+		return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 0.5);
+	}())
+});
diff --git a/src/geo/crs/CRS.EPSG3857.js b/src/geo/crs/CRS.EPSG3857.js
index cbdbd03..d76722a 100644
--- a/src/geo/crs/CRS.EPSG3857.js
+++ b/src/geo/crs/CRS.EPSG3857.js
@@ -1,11 +1,11 @@
 
 L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, {
 	code: 'EPSG:3857',
-	
+
 	projection: L.Projection.SphericalMercator,
-	transformation: new L.Transformation(0.5/Math.PI, 0.5, -0.5/Math.PI, 0.5),
-	
-	project: function(/*LatLng*/ latlng)/*-> Point*/ {
+	transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5),
+
+	project: function (/*LatLng*/ latlng)/*-> Point*/ {
 		var projectedPoint = this.projection.project(latlng),
 			earthRadius = 6378137;
 		return projectedPoint.multiplyBy(earthRadius);
@@ -14,4 +14,4 @@ L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, {
 
 L.CRS.EPSG900913 = L.Util.extend({}, L.CRS.EPSG3857, {
 	code: 'EPSG:900913'
-});
\ No newline at end of file
+});
diff --git a/src/geo/crs/CRS.EPSG4326.js b/src/geo/crs/CRS.EPSG4326.js
index 1550718..26081f9 100644
--- a/src/geo/crs/CRS.EPSG4326.js
+++ b/src/geo/crs/CRS.EPSG4326.js
@@ -1,7 +1,7 @@
 
 L.CRS.EPSG4326 = L.Util.extend({}, L.CRS, {
 	code: 'EPSG:4326',
-	
+
 	projection: L.Projection.LonLat,
-	transformation: new L.Transformation(1/360, 0.5, -1/360, 0.5)
-});
\ No newline at end of file
+	transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5)
+});
diff --git a/src/geo/crs/CRS.js b/src/geo/crs/CRS.js
index 2dc2aa8..eeb633a 100644
--- a/src/geo/crs/CRS.js
+++ b/src/geo/crs/CRS.js
@@ -1,17 +1,17 @@
 
 L.CRS = {
-	latLngToPoint: function(/*LatLng*/ latlng, /*Number*/ scale)/*-> Point*/ {
+	latLngToPoint: function (/*LatLng*/ latlng, /*Number*/ scale)/*-> Point*/ {
 		var projectedPoint = this.projection.project(latlng);
 		return this.transformation._transform(projectedPoint, scale);
 	},
-	
-	pointToLatLng: function(/*Point*/ point, /*Number*/ scale, /*(optional) Boolean*/ unbounded)/*-> LatLng*/ {
+
+	pointToLatLng: function (/*Point*/ point, /*Number*/ scale, /*(optional) Boolean*/ unbounded)/*-> LatLng*/ {
 		var untransformedPoint = this.transformation.untransform(point, scale);
-		return this.projection.unproject(untransformedPoint, unbounded); 
+		return this.projection.unproject(untransformedPoint, unbounded);
 		//TODO get rid of 'unbounded' everywhere
 	},
-	
-	project: function(latlng) {
+
+	project: function (latlng) {
 		return this.projection.project(latlng);
 	}
-};
\ No newline at end of file
+};
diff --git a/src/geo/projection/Projection.LonLat.js b/src/geo/projection/Projection.LonLat.js
index ece2971..0954b4d 100644
--- a/src/geo/projection/Projection.LonLat.js
+++ b/src/geo/projection/Projection.LonLat.js
@@ -1,10 +1,10 @@
 
 L.Projection.LonLat = {
-	project: function(latlng) {
+	project: function (latlng) {
 		return new L.Point(latlng.lng, latlng.lat);
 	},
-	
-	unproject: function(point, unbounded) {
+
+	unproject: function (point, unbounded) {
 		return new L.LatLng(point.y, point.x, unbounded);
 	}
 };
diff --git a/src/geo/projection/Projection.Mercator.js b/src/geo/projection/Projection.Mercator.js
index 9eafff1..e89776a 100644
--- a/src/geo/projection/Projection.Mercator.js
+++ b/src/geo/projection/Projection.Mercator.js
@@ -1,49 +1,51 @@
 
 L.Projection.Mercator = {
 	MAX_LATITUDE: 85.0840591556,
-	
+
 	R_MINOR: 6356752.3142,
 	R_MAJOR: 6378137,
-		
-	project: function(/*LatLng*/ latlng) /*-> Point*/ {
+
+	project: function (/*LatLng*/ latlng) /*-> Point*/ {
 		var d = L.LatLng.DEG_TO_RAD,
 			max = this.MAX_LATITUDE,
 			lat = Math.max(Math.min(max, latlng.lat), -max),
 			r = this.R_MAJOR,
+			r2 = this.R_MINOR,
 			x = latlng.lng * d * r,
 			y = lat * d,
-			tmp = this.R_MINOR / r,
+			tmp = r2 / r,
 			eccent = Math.sqrt(1.0 - tmp * tmp),
 			con = eccent * Math.sin(y);
-			
-		con = Math.pow((1 - con)/(1 + con), eccent * 0.5);
-		
+
+		con = Math.pow((1 - con) / (1 + con), eccent * 0.5);
+
 		var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con;
-		y = -r * Math.log(ts);
-		
+		y = -r2 * Math.log(ts);
+
 		return new L.Point(x, y);
 	},
-	
-	unproject: function(/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {	
+
+	unproject: function (/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {
 		var d = L.LatLng.RAD_TO_DEG,
 			r = this.R_MAJOR,
+			r2 = this.R_MINOR,
 			lng = point.x * d / r,
-			tmp = this.R_MINOR / r,
+			tmp = r2 / r,
 			eccent = Math.sqrt(1 - (tmp * tmp)),
-			ts = Math.exp(- point.y / r),
-			phi = Math.PI/2 - 2 * Math.atan(ts),
+			ts = Math.exp(- point.y / r2),
+			phi = (Math.PI / 2) - 2 * Math.atan(ts),
 			numIter = 15,
 			tol = 1e-7,
 			i = numIter,
 			dphi = 0.1,
 			con;
-		
+
 		while ((Math.abs(dphi) > tol) && (--i > 0)) {
 			con = eccent * Math.sin(phi);
-			dphi = Math.PI/2 - 2 * Math.atan(ts * Math.pow((1.0 - con)/(1.0 + con), 0.5 * eccent)) - phi;
+			dphi = (Math.PI / 2) - 2 * Math.atan(ts * Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi;
 			phi += dphi;
 		}
-			
+
 		return new L.LatLng(phi * d, lng, unbounded);
 	}
 };
diff --git a/src/geo/projection/Projection.SphericalMercator.js b/src/geo/projection/Projection.SphericalMercator.js
index be0532f..275e713 100644
--- a/src/geo/projection/Projection.SphericalMercator.js
+++ b/src/geo/projection/Projection.SphericalMercator.js
@@ -1,23 +1,23 @@
 
 L.Projection.SphericalMercator = {
 	MAX_LATITUDE: 85.0511287798,
-		
-	project: function(/*LatLng*/ latlng) /*-> Point*/ {
+
+	project: function (/*LatLng*/ latlng) /*-> Point*/ {
 		var d = L.LatLng.DEG_TO_RAD,
 			max = this.MAX_LATITUDE,
 			lat = Math.max(Math.min(max, latlng.lat), -max),
 			x = latlng.lng * d,
 			y = lat * d;
-		y = Math.log(Math.tan(Math.PI/4 + y/2));
-		
+		y = Math.log(Math.tan((Math.PI / 4) + (y / 2)));
+
 		return new L.Point(x, y);
 	},
-	
-	unproject: function(/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {	
+
+	unproject: function (/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {
 		var d = L.LatLng.RAD_TO_DEG,
 			lng = point.x * d,
-			lat = (2 * Math.atan(Math.exp(point.y)) - Math.PI/2) * d;
-			
+			lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;
+
 		return new L.LatLng(lat, lng, unbounded);
 	}
 };
diff --git a/src/geometry/Bounds.js b/src/geometry/Bounds.js
index 73448ce..dd70edc 100644
--- a/src/geometry/Bounds.js
+++ b/src/geometry/Bounds.js
@@ -3,8 +3,10 @@
  */
 
 L.Bounds = L.Class.extend({
-	initialize: function(min, max) {	//(Point, Point) or Point[]
-		if (!min) return;
+	initialize: function (min, max) {	//(Point, Point) or Point[]
+		if (!min) {
+			return;
+		}
 		var points = (min instanceof Array ? min : [min, max]);
 		for (var i = 0, len = points.length; i < len; i++) {
 			this.extend(points[i]);
@@ -12,7 +14,7 @@ L.Bounds = L.Class.extend({
 	},
 
 	// extend the bounds to contain the given point
-	extend: function(/*Point*/ point) {
+	extend: function (/*Point*/ point) {
 		if (!this.min && !this.max) {
 			this.min = new L.Point(point.x, point.y);
 			this.max = new L.Point(point.x, point.y);
@@ -23,26 +25,39 @@ L.Bounds = L.Class.extend({
 			this.max.y = Math.max(point.y, this.max.y);
 		}
 	},
-	
-	getCenter: function(round)/*->Point*/ {
+
+	getCenter: function (round)/*->Point*/ {
 		return new L.Point(
-				(this.min.x + this.max.x) / 2, 
+				(this.min.x + this.max.x) / 2,
 				(this.min.y + this.max.y) / 2, round);
 	},
-	
-	contains: function(/*Bounds or Point*/ obj)/*->Boolean*/ {
+
+	contains: function (/*Bounds or Point*/ obj)/*->Boolean*/ {
 		var min, max;
-		
+
 		if (obj instanceof L.Bounds) {
 			min = obj.min;
 			max = obj.max;
 		} else {
-			max = max = obj;
+			min = max = obj;
 		}
-		
-		return (min.x >= this.min.x) && 
+
+		return (min.x >= this.min.x) &&
 				(max.x <= this.max.x) &&
-				(min.y >= this.min.y) && 
+				(min.y >= this.min.y) &&
 				(max.y <= this.max.y);
+	},
+
+	intersects: function (/*Bounds*/ bounds) {
+		var min = this.min,
+			max = this.max,
+			min2 = bounds.min,
+			max2 = bounds.max;
+
+		var xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
+			yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
+
+		return xIntersects && yIntersects;
 	}
-});
\ No newline at end of file
+
+});
diff --git a/src/geometry/LineUtil.js b/src/geometry/LineUtil.js
index 72a8085..ad0bded 100644
--- a/src/geometry/LineUtil.js
+++ b/src/geometry/LineUtil.js
@@ -1,86 +1,114 @@
 /*
- * L.LineUtil contains different utility functions for line segments 
+ * L.LineUtil contains different utility functions for line segments
  * and polylines (clipping, simplification, distances, etc.)
  */
 
 L.LineUtil = {
-	/*
-	 * Simplify polyline with vertex reduction and Douglas-Peucker simplification.
-	 * Improves rendering performance dramatically by lessening the number of points to draw.
-	 */
-	simplify: function(/*Point[]*/ points, /*Number*/ tolerance) {
-		if (!tolerance) return points.slice();
-		
+
+	// Simplify polyline with vertex reduction and Douglas-Peucker simplification.
+	// Improves rendering performance dramatically by lessening the number of points to draw.
+
+	simplify: function (/*Point[]*/ points, /*Number*/ tolerance) {
+		if (!tolerance || !points.length) {
+			return points.slice();
+		}
+
+		var sqTolerance = tolerance * tolerance;
+
 		// stage 1: vertex reduction
-		points = this.reducePoints(points, tolerance);
-		
+		points = this._reducePoints(points, sqTolerance);
+
 		// stage 2: Douglas-Peucker simplification
-		points = this.simplifyDP(points, tolerance);
-		
-		return points; 
+		points = this._simplifyDP(points, sqTolerance);
+
+		return points;
 	},
-	
+
 	// distance from a point to a segment between two points
-	pointToSegmentDistance:  function(/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
-		return Math.sqrt(this._sqPointToSegmentDist(p, p1, p2));	
+	pointToSegmentDistance:  function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
+		return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true));
+	},
+
+	closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
+		return this._sqClosestPointOnSegment(p, p1, p2);
 	},
-	
+
 	// Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm
-	simplifyDP: function(points, tol) {
-		var maxDist2 = 0,
-			index = 0,
-			t2 = tol * tol;
-
-		for (var i = 1, len = points.length, dist2; i < len - 1; i++) {
-			dist2 = this._sqPointToSegmentDist(points[i], points[0], points[len - 1]);
-			if (dist2 > maxDist2) {
+	_simplifyDP: function (points, sqTolerance) {
+
+		var len = points.length,
+			ArrayConstructor = typeof Uint8Array !== 'undefined' ? Uint8Array : Array,
+			markers = new ArrayConstructor(len);
+
+		markers[0] = markers[len - 1] = 1;
+
+		this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
+
+		var i,
+			newPoints = [];
+
+		for (i = 0; i < len; i++) {
+			if (markers[i]) {
+				newPoints.push(points[i]);
+			}
+		}
+
+		return newPoints;
+	},
+
+	_simplifyDPStep: function (points, markers, sqTolerance, first, last) {
+
+		var maxSqDist = 0,
+			index, i, sqDist;
+
+		for (i = first + 1; i <= last - 1; i++) {
+			sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true);
+
+			if (sqDist > maxSqDist) {
 				index = i;
-				maxDist2 = dist2;
+				maxSqDist = sqDist;
 			}
 		}
-		
-		if (maxDist2 >= t2) {
-			var part1 = points.slice(0, index),
-				part2 = points.slice(index),
-				simplifiedPart1 = this.simplifyDP(part1, tol).slice(0, len - 2),
-				simplifiedPart2 = this.simplifyDP(part2, tol);
-			
-			return simplifiedPart1.concat(simplifiedPart2);
-		} else {
-			return [points[0], points[len - 1]];
+
+		if (maxSqDist > sqTolerance) {
+			markers[index] = 1;
+
+			this._simplifyDPStep(points, markers, sqTolerance, first, index);
+			this._simplifyDPStep(points, markers, sqTolerance, index, last);
 		}
 	},
-	
+
 	// reduce points that are too close to each other to a single point
-	reducePoints: function(points, tol) {
-		var reducedPoints = [points[0]],
-			t2 = tol * tol;
-		
+	_reducePoints: function (points, sqTolerance) {
+		var reducedPoints = [points[0]];
+
 		for (var i = 1, prev = 0, len = points.length; i < len; i++) {
-			if (this._sqDist(points[i], points[prev]) < t2) continue;
-			reducedPoints.push(points[i]);
-			prev = i;
+			if (this._sqDist(points[i], points[prev]) > sqTolerance) {
+				reducedPoints.push(points[i]);
+				prev = i;
+			}
 		}
 		if (prev < len - 1) {
 			reducedPoints.push(points[len - 1]);
 		}
 		return reducedPoints;
 	},
-	
-	/*
-	 * Cohen-Sutherland line clipping algorithm.
-	 * Used to avoid rendering parts of a polyline that are not currently visible.
-	 */
-	clipSegment: function(a, b, bounds, useLastCode) {
+
+	/*jshint bitwise:false */ // temporarily allow bitwise oprations
+
+	// Cohen-Sutherland line clipping algorithm.
+	// Used to avoid rendering parts of a polyline that are not currently visible.
+
+	clipSegment: function (a, b, bounds, useLastCode) {
 		var min = bounds.min,
 			max = bounds.max;
-		
+
 		var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds),
 			codeB = this._getBitCode(b, bounds);
-		
+
 		// save 2nd code to avoid calculating it on the next segment
 		this._lastCode = codeB;
-		
+
 		while (true) {
 			// if a,b is inside the clip window (trivial accept)
 			if (!(codeA | codeB)) {
@@ -93,8 +121,8 @@ L.LineUtil = {
 				var codeOut = codeA || codeB,
 					p = this._getEdgeIntersection(a, b, codeOut, bounds),
 					newCode = this._getBitCode(p, bounds);
-				
-				if (codeOut == codeA) {
+
+				if (codeOut === codeA) {
 					a = p;
 					codeA = newCode;
 				} else {
@@ -104,56 +132,74 @@ L.LineUtil = {
 			}
 		}
 	},
-	
-	_getEdgeIntersection: function(a, b, code, bounds) {
+
+	_getEdgeIntersection: function (a, b, code, bounds) {
 		var dx = b.x - a.x,
 			dy = b.y - a.y,
 			min = bounds.min,
 			max = bounds.max;
-		
+
 		if (code & 8) { // top
 			return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y);
 		} else if (code & 4) { // bottom
 			return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y);
-		} else if (code & 2){ // right
+		} else if (code & 2) { // right
 			return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx);
 		} else if (code & 1) { // left
 			return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx);
 		}
 	},
-	
-	_getBitCode: function(/*Point*/ p, bounds) {
+
+	_getBitCode: function (/*Point*/ p, bounds) {
 		var code = 0;
-		
-		if (p.x < bounds.min.x) code |= 1; // left
-		else if (p.x > bounds.max.x) code |= 2; // right
-		if (p.y < bounds.min.y) code |= 4; // bottom
-		else if (p.y > bounds.max.y) code |= 8; // top
-		
+
+		if (p.x < bounds.min.x) { // left
+			code |= 1;
+		} else if (p.x > bounds.max.x) { // right
+			code |= 2;
+		}
+		if (p.y < bounds.min.y) { // bottom
+			code |= 4;
+		} else if (p.y > bounds.max.y) { // top
+			code |= 8;
+		}
+
 		return code;
 	},
-	
+
+	/*jshint bitwise:true */
+
 	// square distance (to avoid unnecessary Math.sqrt calls)
-	_sqDist: function(p1, p2) {
+	_sqDist: function (p1, p2) {
 		var dx = p2.x - p1.x,
 			dy = p2.y - p1.y;
 		return dx * dx + dy * dy;
 	},
-	
-	// square distance from point to a segment
-	_sqPointToSegmentDist: function(p, p1, p2) {
-		var x2 = p2.x - p1.x,
-			y2 = p2.y - p1.y;
-		
-		if (!x2 && !y2) return this._sqDist(p, p1);
-		
-		var dot = (p.x - p1.x) * x2 + (p.y - p1.y) * y2,
-			t = dot / this._sqDist(p1, p2);
-		
-		if (t < 0) return this._sqDist(p, p1);
-		if (t > 1) return this._sqDist(p, p2);
-		
-		var proj = new L.Point(p1.x + x2 * t, p1.y + y2 * t);
-		return this._sqDist(p, proj);
-	}	
-};
\ No newline at end of file
+
+	// return closest point on segment or distance to that point
+	_sqClosestPointOnSegment: function (p, p1, p2, sqDist) {
+		var x = p1.x,
+			y = p1.y,
+			dx = p2.x - x,
+			dy = p2.y - y,
+			dot = dx * dx + dy * dy,
+			t;
+
+		if (dot > 0) {
+			t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
+
+			if (t > 1) {
+				x = p2.x;
+				y = p2.y;
+			} else if (t > 0) {
+				x += dx * t;
+				y += dy * t;
+			}
+		}
+
+		dx = p.x - x;
+		dy = p.y - y;
+
+		return sqDist ? dx * dx + dy * dy : new L.Point(x, y);
+	}
+};
diff --git a/src/geometry/Point.js b/src/geometry/Point.js
index d031ffe..db41457 100644
--- a/src/geometry/Point.js
+++ b/src/geometry/Point.js
@@ -2,65 +2,65 @@
  * L.Point represents a point with x and y coordinates.
  */
 
-L.Point = function(/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
+L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
 	this.x = (round ? Math.round(x) : x);
 	this.y = (round ? Math.round(y) : y);
 };
 
 L.Point.prototype = {
-	add: function(point) {
+	add: function (point) {
 		return this.clone()._add(point);
 	},
-	
-	_add: function(point) {
+
+	_add: function (point) {
 		this.x += point.x;
 		this.y += point.y;
-		return this;		
+		return this;
 	},
-		
-	subtract: function(point) {
+
+	subtract: function (point) {
 		return this.clone()._subtract(point);
 	},
-	
+
 	// destructive subtract (faster)
-	_subtract: function(point) {
+	_subtract: function (point) {
 		this.x -= point.x;
 		this.y -= point.y;
 		return this;
 	},
-	
-	divideBy: function(num, round) {
-		return new L.Point(this.x/num, this.y/num, round);
+
+	divideBy: function (num, round) {
+		return new L.Point(this.x / num, this.y / num, round);
 	},
-	
-	multiplyBy: function(num) {
+
+	multiplyBy: function (num) {
 		return new L.Point(this.x * num, this.y * num);
 	},
-	
-	distanceTo: function(point) {
+
+	distanceTo: function (point) {
 		var x = point.x - this.x,
 			y = point.y - this.y;
-		return Math.sqrt(x*x + y*y);
+		return Math.sqrt(x * x + y * y);
 	},
-	
-	round: function() {
+
+	round: function () {
 		return this.clone()._round();
 	},
-	
+
 	// destructive round
-	_round: function() {
+	_round: function () {
 		this.x = Math.round(this.x);
 		this.y = Math.round(this.y);
 		return this;
 	},
-	
-	clone: function() {
+
+	clone: function () {
 		return new L.Point(this.x, this.y);
 	},
-	
-	toString: function() {
-		return 'Point(' + 
-				L.Util.formatNum(this.x) + ', ' + 
-				L.Util.formatNum(this.y) + ')'; 
+
+	toString: function () {
+		return 'Point(' +
+				L.Util.formatNum(this.x) + ', ' +
+				L.Util.formatNum(this.y) + ')';
 	}
-};
\ No newline at end of file
+};
diff --git a/src/geometry/PolyUtil.js b/src/geometry/PolyUtil.js
index c546070..643b2f8 100644
--- a/src/geometry/PolyUtil.js
+++ b/src/geometry/PolyUtil.js
@@ -1,14 +1,16 @@
 /*
- * L.PolyUtil contains utilify functions for polygons (clipping, etc.). 
+ * L.PolyUtil contains utilify functions for polygons (clipping, etc.).
  */
 
+/*jshint bitwise:false */ // allow bitwise oprations here
+
 L.PolyUtil = {};
 
 /*
  * Sutherland-Hodgeman polygon clipping algorithm.
  * Used to avoid rendering parts of a polygon that are not currently visible.
  */
-L.PolyUtil.clipPolygon = function(points, bounds) {
+L.PolyUtil.clipPolygon = function (points, bounds) {
 	var min = bounds.min,
 		max = bounds.max,
 		clippedPoints,
@@ -17,20 +19,20 @@ L.PolyUtil.clipPolygon = function(points, bounds) {
 		a, b,
 		len, edge, p,
 		lu = L.LineUtil;
-	
+
 	for (i = 0, len = points.length; i < len; i++) {
 		points[i]._code = lu._getBitCode(points[i], bounds);
 	}
-	
+
 	// for each edge (left, bottom, right, top)
 	for (k = 0; k < 4; k++) {
 		edge = edges[k];
 		clippedPoints = [];
-		
+
 		for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
 			a = points[i];
 			b = points[j];
-			
+
 			// if a is inside the clip window
 			if (!(a._code & edge)) {
 				// if b is outside the clip window (a->b goes out of screen)
@@ -40,7 +42,7 @@ L.PolyUtil.clipPolygon = function(points, bounds) {
 					clippedPoints.push(p);
 				}
 				clippedPoints.push(a);
-				
+
 			// else if b is inside the clip window (a->b enters the screen)
 			} else if (!(b._code & edge)) {
 				p = lu._getEdgeIntersection(b, a, edge, bounds);
@@ -50,6 +52,8 @@ L.PolyUtil.clipPolygon = function(points, bounds) {
 		}
 		points = clippedPoints;
 	}
-	
+
 	return points;
-};
\ No newline at end of file
+};
+
+/*jshint bitwise:true */
diff --git a/src/geometry/Transformation.js b/src/geometry/Transformation.js
index 37f4096..fdb20bd 100644
--- a/src/geometry/Transformation.js
+++ b/src/geometry/Transformation.js
@@ -1,31 +1,31 @@
 /*
- * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix. 
+ * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix.
  */
 
 L.Transformation = L.Class.extend({
-	initialize: function(/*Number*/ a, /*Number*/ b, /*Number*/ c, /*Number*/ d) {
+	initialize: function (/*Number*/ a, /*Number*/ b, /*Number*/ c, /*Number*/ d) {
 		this._a = a;
 		this._b = b;
 		this._c = c;
 		this._d = d;
 	},
 
-	transform: function(point, scale) {
+	transform: function (point, scale) {
 		return this._transform(point.clone(), scale);
 	},
-	
+
 	// destructive transform (faster)
-	_transform: function(/*Point*/ point, /*Number*/ scale) /*-> Point*/ {	
+	_transform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
 		scale = scale || 1;
-		point.x = scale * (this._a * point.x + this._b); 
+		point.x = scale * (this._a * point.x + this._b);
 		point.y = scale * (this._c * point.y + this._d);
 		return point;
 	},
-	
-	untransform: function(/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
+
+	untransform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
 		scale = scale || 1;
 		return new L.Point(
-			(point.x/scale - this._b) / this._a,
-			(point.y/scale - this._d) / this._c);
+			(point.x / scale - this._b) / this._a,
+			(point.y / scale - this._d) / this._c);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/handler/DoubleClickZoom.js b/src/handler/DoubleClickZoom.js
deleted file mode 100644
index 121a5e2..0000000
--- a/src/handler/DoubleClickZoom.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * L.Handler.DoubleClickZoom is used internally by L.Map to add double-click zooming.
- */
-
-L.Handler.DoubleClickZoom = L.Handler.extend({
-	enable: function() {
-		if (this._enabled) { return; }
-		this._map.on('dblclick', this._onDoubleClick, this._map);
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		this._map.off('dblclick', this._onDoubleClick, this._map);
-		this._enabled = false;
-	},
-	
-	_onDoubleClick: function(e) {
-		this.setView(e.latlng, this._zoom + 1);
-	}
-});
\ No newline at end of file
diff --git a/src/handler/Handler.js b/src/handler/Handler.js
deleted file mode 100644
index c38a6b6..0000000
--- a/src/handler/Handler.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * L.Handler classes are used internally to inject interaction features to classes like Map and Marker.
- */
-
-L.Handler = L.Class.extend({
-	initialize: function(map) {
-		this._map = map;
-	},
-	
-	enabled: function() {
-		return !!this._enabled;
-	}	
-});
\ No newline at end of file
diff --git a/src/handler/MapDrag.js b/src/handler/MapDrag.js
deleted file mode 100644
index 1c40726..0000000
--- a/src/handler/MapDrag.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * L.Handler.MapDrag is used internally by L.Map to make the map draggable.
- */
-
-L.Handler.MapDrag = L.Handler.extend({
-
-	enable: function() {
-		if (this._enabled) { return; }
-		if (!this._draggable) {
-			this._draggable = new L.Draggable(this._map._mapPane, this._map._container);
-			
-			this._draggable.on('dragstart', this._onDragStart, this);
-			this._draggable.on('drag', this._onDrag, this);
-			this._draggable.on('dragend', this._onDragEnd, this);
-		}
-		this._draggable.enable();
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		this._draggable.disable();
-		this._enabled = false;
-	},
-	
-	moved: function() {
-		return this._draggable._moved;
-	},
-	
-	_onDragStart: function() {
-		this._map.fire('movestart');
-		this._map.fire('dragstart');
-	},
-	
-	_onDrag: function() {
-		this._map.fire('move');
-		this._map.fire('drag');
-	},
-	
-	_onDragEnd: function() {
-		this._map.fire('moveend');
-		this._map.fire('dragend');
-	}
-});
diff --git a/src/handler/MarkerDrag.js b/src/handler/MarkerDrag.js
deleted file mode 100644
index 8e884d5..0000000
--- a/src/handler/MarkerDrag.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
- */
-
-L.Handler.MarkerDrag = L.Handler.extend({
-	initialize: function(marker) {
-		this._marker = marker;
-	},
-	
-	enable: function() {
-		if (this._enabled) { return; }
-		if (!this._draggable) {
-			this._draggable = new L.Draggable(this._marker._icon, this._marker._icon);
-			this._draggable.on('dragstart', this._onDragStart, this);
-			this._draggable.on('drag', this._onDrag, this);
-			this._draggable.on('dragend', this._onDragEnd, this);
-		}
-		this._draggable.enable();
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		this._draggable.disable();
-		this._enabled = false;
-	},
-	
-	moved: function() {
-		return this._draggable && this._draggable._moved;
-	},
-	
-	_onDragStart: function(e) {
-		this._marker.closePopup();
-		
-		this._marker.fire('movestart');
-		this._marker.fire('dragstart');
-	},
-	
-	_onDrag: function(e) {
-		// update shadow position
-		var iconPos = L.DomUtil.getPosition(this._marker._icon);
-		L.DomUtil.setPosition(this._marker._shadow, iconPos);
-		
-		this._marker._latlng = this._marker._map.layerPointToLatLng(iconPos);
-	
-		this._marker.fire('move');
-		this._marker.fire('drag');
-	},
-	
-	_onDragEnd: function() {
-		this._marker.fire('moveend');
-		this._marker.fire('dragend');
-	}
-});
diff --git a/src/handler/ScrollWheelZoom.js b/src/handler/ScrollWheelZoom.js
deleted file mode 100644
index dc877e1..0000000
--- a/src/handler/ScrollWheelZoom.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * L.Handler.ScrollWheelZoom is used internally by L.Map to enable mouse scroll wheel zooming on the map.
- */
-
-L.Handler.ScrollWheelZoom = L.Handler.extend({
-	enable: function() {
-		if (this._enabled) { return; }
-		L.DomEvent.addListener(this._map._container, 'mousewheel', this._onWheelScroll, this);
-		this._delta = 0;
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		L.DomEvent.removeListener(this._map._container, 'mousewheel', this._onWheelScroll);
-		this._enabled = false;
-	},
-	
-	_onWheelScroll: function(e) {
-		this._delta += L.DomEvent.getWheelDelta(e);
-		this._lastMousePos = this._map.mouseEventToContainerPoint(e);
-		
-		clearTimeout(this._timer);
-		this._timer = setTimeout(L.Util.bind(this._performZoom, this), 50);
-		
-		L.DomEvent.preventDefault(e);
-	},
-	
-	_performZoom: function() {
-		var delta = Math.round(this._delta);
-		this._delta = 0;
-		
-		if (!delta) { return; }
-		
-		var center = this._getCenterForScrollWheelZoom(this._lastMousePos, delta),
-			zoom = this._map.getZoom() + delta;
-		
-		if (this._map._limitZoom(zoom) == this._map._zoom) { return; }
-
-		this._map.setView(center, zoom);
-	},
-	
-	_getCenterForScrollWheelZoom: function(mousePos, delta) {
-		var centerPoint = this._map.getPixelBounds().getCenter(),
-			viewHalf = this._map.getSize().divideBy(2),
-			centerOffset = mousePos.subtract(viewHalf).multiplyBy(1 - Math.pow(2, -delta)),
-			newCenterPoint = centerPoint.add(centerOffset);
-		return this._map.unproject(newCenterPoint, this._map._zoom, true);
-	}
-});
\ No newline at end of file
diff --git a/src/layer/FeatureGroup.js b/src/layer/FeatureGroup.js
index 6e45d84..eee0b11 100644
--- a/src/layer/FeatureGroup.js
+++ b/src/layer/FeatureGroup.js
@@ -1,40 +1,40 @@
 /*
- * L.FeatureGroup extends L.LayerGroup by introducing mouse events and bindPopup method shared between a group of layers. 
+ * L.FeatureGroup extends L.LayerGroup by introducing mouse events and bindPopup method shared between a group of layers.
  */
 
 L.FeatureGroup = L.LayerGroup.extend({
 	includes: L.Mixin.Events,
 
-	addLayer: function(layer) {
+	addLayer: function (layer) {
 		this._initEvents(layer);
 		L.LayerGroup.prototype.addLayer.call(this, layer);
-		
+
 		if (this._popupContent && layer.bindPopup) {
 			layer.bindPopup(this._popupContent);
-		} 
+		}
 	},
-	
-	bindPopup: function(content) {
+
+	bindPopup: function (content) {
 		this._popupContent = content;
-		
-		for (var i in this._layers) {
-			if (this._layers.hasOwnProperty(i) && this._layers[i].bindPopup) {
-				this._layers[i].bindPopup(content);
-			}
-		}
+
+		return this.invoke('bindPopup', content);
 	},
-	
+
+	setStyle: function (style) {
+		return this.invoke('setStyle', style);
+	},
+
 	_events: ['click', 'dblclick', 'mouseover', 'mouseout'],
-	
-	_initEvents: function(layer) {
+
+	_initEvents: function (layer) {
 		for (var i = 0, len = this._events.length; i < len; i++) {
 			layer.on(this._events[i], this._propagateEvent, this);
 		}
 	},
-	
-	_propagateEvent: function(e) {
+
+	_propagateEvent: function (e) {
 		e.layer = e.target;
 		e.target = this;
 		this.fire(e.type, e);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/GeoJSON.js b/src/layer/GeoJSON.js
index 6cbd419..f0c2f41 100644
--- a/src/layer/GeoJSON.js
+++ b/src/layer/GeoJSON.js
@@ -1,106 +1,104 @@
 
-L.GeoJSON = L.LayerGroup.extend({
-	includes: L.Mixin.Events,
-	
-	initialize: function(geojson, options) {
+L.GeoJSON = L.FeatureGroup.extend({
+	initialize: function (geojson, options) {
 		L.Util.setOptions(this, options);
 		this._geojson = geojson;
 		this._layers = {};
-		
+
 		if (geojson) {
 			this.addGeoJSON(geojson);
 		}
 	},
-	
-	addGeoJSON: function(geojson) {
+
+	addGeoJSON: function (geojson) {
 		if (geojson.features) {
 			for (var i = 0, len = geojson.features.length; i < len; i++) {
 				this.addGeoJSON(geojson.features[i]);
 			}
 			return;
 		}
-				
-		var isFeature = (geojson.type == 'Feature'),
+
+		var isFeature = (geojson.type === 'Feature'),
 			geometry = (isFeature ? geojson.geometry : geojson),
 			layer = L.GeoJSON.geometryToLayer(geometry, this.options.pointToLayer);
-		
+
 		this.fire('featureparse', {
-			layer: layer, 
+			layer: layer,
 			properties: geojson.properties,
 			geometryType: geometry.type,
 			bbox: geojson.bbox,
 			id: geojson.id
 		});
-		
+
 		this.addLayer(layer);
 	}
 });
 
 L.Util.extend(L.GeoJSON, {
-	geometryToLayer: function(geometry, pointToLayer) {
-		var coords = geometry.coordinates, 
-			latlng, latlngs, 
-			i, len, 
-			layer, 
+	geometryToLayer: function (geometry, pointToLayer) {
+		var coords = geometry.coordinates,
+			latlng, latlngs,
+			i, len,
+			layer,
 			layers = [];
 
 		switch (geometry.type) {
-			case 'Point':
-				latlng = this.coordsToLatLng(coords);
-				return pointToLayer ? pointToLayer(latlng) : new L.Marker(latlng);
-				
-			case 'MultiPoint':
-				for (i = 0, len = coords.length; i < len; i++) {
-					latlng = this.coordsToLatLng(coords[i]);
-					layer = pointToLayer ? pointToLayer(latlng) : new L.Marker(latlng);
-					layers.push(layer);
-				}
-				return new L.FeatureGroup(layers);
-				
-			case 'LineString':
-				latlngs = this.coordsToLatLngs(coords);
-				return new L.Polyline(latlngs);
-				
-			case 'Polygon':
-				latlngs = this.coordsToLatLngs(coords, 1);
-				return new L.Polygon(latlngs);
-				
-			case 'MultiLineString':
-				latlngs = this.coordsToLatLngs(coords, 1);
-				return new L.MultiPolyline(latlngs);
-				
-			case "MultiPolygon":
-				latlngs = this.coordsToLatLngs(coords, 2);
-				return new L.MultiPolygon(latlngs);
-				
-			case "GeometryCollection":
-				for (i = 0, len = geometry.geometries.length; i < len; i++) {
-					layer = this.geometryToLayer(geometry.geometries[i]);
-					layers.push(layer);
-				}
-				return new L.FeatureGroup(layers);
-				
-			default:
-				throw new Error('Invalid GeoJSON object.');
+		case 'Point':
+			latlng = this.coordsToLatLng(coords);
+			return pointToLayer ? pointToLayer(latlng) : new L.Marker(latlng);
+
+		case 'MultiPoint':
+			for (i = 0, len = coords.length; i < len; i++) {
+				latlng = this.coordsToLatLng(coords[i]);
+				layer = pointToLayer ? pointToLayer(latlng) : new L.Marker(latlng);
+				layers.push(layer);
+			}
+			return new L.FeatureGroup(layers);
+
+		case 'LineString':
+			latlngs = this.coordsToLatLngs(coords);
+			return new L.Polyline(latlngs);
+
+		case 'Polygon':
+			latlngs = this.coordsToLatLngs(coords, 1);
+			return new L.Polygon(latlngs);
+
+		case 'MultiLineString':
+			latlngs = this.coordsToLatLngs(coords, 1);
+			return new L.MultiPolyline(latlngs);
+
+		case "MultiPolygon":
+			latlngs = this.coordsToLatLngs(coords, 2);
+			return new L.MultiPolygon(latlngs);
+
+		case "GeometryCollection":
+			for (i = 0, len = geometry.geometries.length; i < len; i++) {
+				layer = this.geometryToLayer(geometry.geometries[i], pointToLayer);
+				layers.push(layer);
+			}
+			return new L.FeatureGroup(layers);
+
+		default:
+			throw new Error('Invalid GeoJSON object.');
 		}
 	},
 
-	coordsToLatLng: function(/*Array*/ coords, /*Boolean*/ reverse)/*: LatLng*/ {
+	coordsToLatLng: function (/*Array*/ coords, /*Boolean*/ reverse)/*: LatLng*/ {
 		var lat = parseFloat(coords[reverse ? 0 : 1]),
 			lng = parseFloat(coords[reverse ? 1 : 0]);
-		return new L.LatLng(lat, lng);
+		return new L.LatLng(lat, lng, true);
 	},
 
-	coordsToLatLngs: function(/*Array*/ coords, /*Number*/ levelsDeep, /*Boolean*/ reverse)/*: Array*/ {
+	coordsToLatLngs: function (/*Array*/ coords, /*Number*/ levelsDeep, /*Boolean*/ reverse)/*: Array*/ {
 		var latlng, latlngs = [],
 			i, len = coords.length;
-		
+
 		for (i = 0; i < len; i++) {
-			latlng = levelsDeep ? 
-					this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) : 
+			latlng = levelsDeep ?
+					this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) :
 					this.coordsToLatLng(coords[i], reverse);
 			latlngs.push(latlng);
 		}
 		return latlngs;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/ImageOverlay.js b/src/layer/ImageOverlay.js
index 4551b2e..fac5ec8 100644
--- a/src/layer/ImageOverlay.js
+++ b/src/layer/ImageOverlay.js
@@ -1,58 +1,58 @@
 L.ImageOverlay = L.Class.extend({
 	includes: L.Mixin.Events,
-	
-	initialize: function(/*String*/ url, /*LatLngBounds*/ bounds) {
+
+	initialize: function (/*String*/ url, /*LatLngBounds*/ bounds) {
 		this._url = url;
 		this._bounds = bounds;
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map) {
 		this._map = map;
-		
+
 		if (!this._image) {
 			this._initImage();
 		}
-		
+
 		map.getPanes().overlayPane.appendChild(this._image);
-		
+
 		map.on('viewreset', this._reset, this);
 		this._reset();
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
 		map.getPanes().overlayPane.removeChild(this._image);
 		map.off('viewreset', this._reset, this);
 	},
-	
-	_initImage: function() {
+
+	_initImage: function () {
 		this._image = L.DomUtil.create('img', 'leaflet-image-layer');
-		
+
 		this._image.style.visibility = 'hidden';
 		//TODO opacity option
-		
+
 		//TODO createImage util method to remove duplication
 		L.Util.extend(this._image, {
 			galleryimg: 'no',
 			onselectstart: L.Util.falseFn,
 			onmousemove: L.Util.falseFn,
-			onload: this._onImageLoad,
+			onload: L.Util.bind(this._onImageLoad, this),
 			src: this._url
 		});
 	},
-	
-	_reset: function() {
+
+	_reset: function () {
 		var topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
 			bottomRight = this._map.latLngToLayerPoint(this._bounds.getSouthEast()),
 			size = bottomRight.subtract(topLeft);
-		
+
 		L.DomUtil.setPosition(this._image, topLeft);
-		
+
 		this._image.style.width = size.x + 'px';
 		this._image.style.height = size.y + 'px';
 	},
-	
-	_onImageLoad: function() {
-		this.style.visibility = '';
-		//TODO fire layerload
+
+	_onImageLoad: function () {
+		this._image.style.visibility = '';
+		this.fire('load');
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/LayerGroup.js b/src/layer/LayerGroup.js
index 58940d4..864541f 100644
--- a/src/layer/LayerGroup.js
+++ b/src/layer/LayerGroup.js
@@ -3,56 +3,72 @@
  */
 
 L.LayerGroup = L.Class.extend({
-	initialize: function(layers) {
+	initialize: function (layers) {
 		this._layers = {};
-		
+
 		if (layers) {
 			for (var i = 0, len = layers.length; i < len; i++) {
 				this.addLayer(layers[i]);
 			}
 		}
 	},
-	
-	addLayer: function(layer) {
+
+	addLayer: function (layer) {
 		var id = L.Util.stamp(layer);
 		this._layers[id] = layer;
-		
+
 		if (this._map) {
 			this._map.addLayer(layer);
 		}
 		return this;
 	},
-	
-	removeLayer: function(layer) {
+
+	removeLayer: function (layer) {
 		var id = L.Util.stamp(layer);
 		delete this._layers[id];
-		
+
 		if (this._map) {
 			this._map.removeLayer(layer);
 		}
 		return this;
 	},
-	
-	clearLayers: function() {
+
+	clearLayers: function () {
 		this._iterateLayers(this.removeLayer, this);
 		return this;
 	},
 
-	onAdd: function(map) {
+	invoke: function (methodName) {
+		var args = Array.prototype.slice.call(arguments, 1),
+			i, layer;
+
+		for (i in this._layers) {
+			if (this._layers.hasOwnProperty(i)) {
+				layer = this._layers[i];
+
+				if (layer[methodName]) {
+					layer[methodName].apply(layer, args);
+				}
+			}
+		}
+		return this;
+	},
+
+	onAdd: function (map) {
 		this._map = map;
 		this._iterateLayers(map.addLayer, map);
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
 		this._iterateLayers(map.removeLayer, map);
 		delete this._map;
 	},
-	
-	_iterateLayers: function(method, context) {
+
+	_iterateLayers: function (method, context) {
 		for (var i in this._layers) {
 			if (this._layers.hasOwnProperty(i)) {
 				method.call(context, this._layers[i]);
 			}
 		}
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/Popup.js b/src/layer/Popup.js
index 4cb14e3..5b7e9fc 100644
--- a/src/layer/Popup.js
+++ b/src/layer/Popup.js
@@ -1,145 +1,159 @@
 
 L.Popup = L.Class.extend({
 	includes: L.Mixin.Events,
-	
+
 	options: {
+		minWidth: 50,
 		maxWidth: 300,
 		autoPan: true,
 		closeButton: true,
-		
 		offset: new L.Point(0, 2),
-		autoPanPadding: new L.Point(5, 5)
+		autoPanPadding: new L.Point(5, 5),
+		className: ''
 	},
-	
-	initialize: function(options) {
+
+	initialize: function (options, source) {
 		L.Util.setOptions(this, options);
+
+		this._source = source;
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map) {
 		this._map = map;
 		if (!this._container) {
 			this._initLayout();
 		}
 		this._updateContent();
-		
+
 		this._container.style.opacity = '0';
 
 		this._map._panes.popupPane.appendChild(this._container);
 		this._map.on('viewreset', this._updatePosition, this);
+
 		if (this._map.options.closePopupOnClick) {
 			this._map.on('preclick', this._close, this);
 		}
+
 		this._update();
-		
+
 		this._container.style.opacity = '1'; //TODO fix ugly opacity hack
-		
+
 		this._opened = true;
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
 		map._panes.popupPane.removeChild(this._container);
+		L.Util.falseFn(this._container.offsetWidth);
+
 		map.off('viewreset', this._updatePosition, this);
 		map.off('click', this._close, this);
 
 		this._container.style.opacity = '0';
-		
+
 		this._opened = false;
 	},
-	
-	setLatLng: function(latlng) {
+
+	setLatLng: function (latlng) {
 		this._latlng = latlng;
 		if (this._opened) {
 			this._update();
 		}
 		return this;
 	},
-	
-	setContent: function(content) {
+
+	setContent: function (content) {
 		this._content = content;
 		if (this._opened) {
 			this._update();
 		}
 		return this;
 	},
-	
-	_close: function() {
+
+	_close: function () {
 		if (this._opened) {
-			this._map.removeLayer(this);
+			this._map.closePopup();
 		}
 	},
-	
-	_initLayout: function() {
-		this._container = L.DomUtil.create('div', 'leaflet-popup');
-		
-		this._closeButton = L.DomUtil.create('a', 'leaflet-popup-close-button', this._container);
-		this._closeButton.href = '#close';
-		this._closeButton.onclick = L.Util.bind(this._onCloseButtonClick, this);
-		
+
+	_initLayout: function () {
+		this._container = L.DomUtil.create('div', 'leaflet-popup ' + this.options.className);
+
+		if (this.options.closeButton) {
+			this._closeButton = L.DomUtil.create('a', 'leaflet-popup-close-button', this._container);
+			this._closeButton.href = '#close';
+			L.DomEvent.addListener(this._closeButton, 'click', this._onCloseButtonClick, this);
+		}
+
 		this._wrapper = L.DomUtil.create('div', 'leaflet-popup-content-wrapper', this._container);
 		L.DomEvent.disableClickPropagation(this._wrapper);
 		this._contentNode = L.DomUtil.create('div', 'leaflet-popup-content', this._wrapper);
-		
+
 		this._tipContainer = L.DomUtil.create('div', 'leaflet-popup-tip-container', this._container);
 		this._tip = L.DomUtil.create('div', 'leaflet-popup-tip', this._tipContainer);
 	},
-	
-	_update: function() {
+
+	_update: function () {
 		this._container.style.visibility = 'hidden';
-		
+
 		this._updateContent();
 		this._updateLayout();
 		this._updatePosition();
-		
+
 		this._container.style.visibility = '';
 
 		this._adjustPan();
 	},
-	
-	_updateContent: function() {
-		if (!this._content) return;
-		
-		if (typeof this._content == 'string') {
+
+	_updateContent: function () {
+		if (!this._content) {
+			return;
+		}
+
+		if (typeof this._content === 'string') {
 			this._contentNode.innerHTML = this._content;
 		} else {
 			this._contentNode.innerHTML = '';
 			this._contentNode.appendChild(this._content);
 		}
 	},
-	
-	_updateLayout: function() {
+
+	_updateLayout: function () {
 		this._container.style.width = '';
 		this._container.style.whiteSpace = 'nowrap';
 
 		var width = this._container.offsetWidth;
-		
-		this._container.style.width = (width > this.options.maxWidth ? this.options.maxWidth : width) + 'px';
+
+		this._container.style.width = (width > this.options.maxWidth ?
+				this.options.maxWidth : (width < this.options.minWidth ? this.options.minWidth : width)) + 'px';
 		this._container.style.whiteSpace = '';
-		
+
 		this._containerWidth = this._container.offsetWidth;
 	},
-	
-	_updatePosition: function() {
+
+	_updatePosition: function () {
 		var pos = this._map.latLngToLayerPoint(this._latlng);
-		
+
 		this._containerBottom = -pos.y - this.options.offset.y;
-		this._containerLeft = pos.x - Math.round(this._containerWidth/2) + this.options.offset.x;
-		
+		this._containerLeft = pos.x - Math.round(this._containerWidth / 2) + this.options.offset.x;
+
 		this._container.style.bottom = this._containerBottom + 'px';
 		this._container.style.left = this._containerLeft + 'px';
 	},
-	
-	_adjustPan: function() {
-		if (!this.options.autoPan) { return; }
-		
+
+	_adjustPan: function () {
+		if (!this.options.autoPan) {
+			return;
+		}
+
 		var containerHeight = this._container.offsetHeight,
 			layerPos = new L.Point(
-				this._containerLeft, 
+				this._containerLeft,
 				-containerHeight - this._containerBottom),
 			containerPos = this._map.layerPointToContainerPoint(layerPos),
 			adjustOffset = new L.Point(0, 0),
 			padding = this.options.autoPanPadding,
 			size = this._map.getSize();
-		
+
 		if (containerPos.x < 0) {
 			adjustOffset.x = containerPos.x - padding.x;
 		}
@@ -152,14 +166,14 @@ L.Popup = L.Class.extend({
 		if (containerPos.y + containerHeight > size.y) {
 			adjustOffset.y = containerPos.y + containerHeight - size.y + padding.y;
 		}
-		
+
 		if (adjustOffset.x || adjustOffset.y) {
 			this._map.panBy(adjustOffset);
 		}
 	},
-	
-	_onCloseButtonClick: function(e) {
+
+	_onCloseButtonClick: function (e) {
 		this._close();
 		L.DomEvent.stop(e);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/marker/Icon.js b/src/layer/marker/Icon.js
index 6df036e..dc7502d 100644
--- a/src/layer/marker/Icon.js
+++ b/src/layer/marker/Icon.js
@@ -1,48 +1,56 @@
 L.Icon = L.Class.extend({
 	iconUrl: L.ROOT_URL + 'images/marker.png',
 	shadowUrl: L.ROOT_URL + 'images/marker-shadow.png',
-	
+
 	iconSize: new L.Point(25, 41),
 	shadowSize: new L.Point(41, 41),
-	
+
 	iconAnchor: new L.Point(13, 41),
 	popupAnchor: new L.Point(0, -33),
-	
-	initialize: function(iconUrl) {
+
+	initialize: function (iconUrl) {
 		if (iconUrl) {
 			this.iconUrl = iconUrl;
 		}
 	},
-	
-	createIcon: function() {
+
+	createIcon: function () {
 		return this._createIcon('icon');
 	},
-	
-	createShadow: function() {
+
+	createShadow: function () {
 		return this._createIcon('shadow');
 	},
-	
-	_createIcon: function(name) {
+
+	_createIcon: function (name) {
 		var size = this[name + 'Size'],
-			src = this[name + 'Url'],
+			src = this[name + 'Url'];
+		if (!src && name === 'shadow') {
+			return null;
+		}
+
+		var img;
+		if (!src) {
+			img = this._createDiv();
+		}
+		else {
 			img = this._createImg(src);
-		
-		if (!src) { return null; }
-		
+		}
+
 		img.className = 'leaflet-marker-' + name;
-		
+
 		img.style.marginLeft = (-this.iconAnchor.x) + 'px';
 		img.style.marginTop = (-this.iconAnchor.y) + 'px';
-		
+
 		if (size) {
 			img.style.width = size.x + 'px';
 			img.style.height = size.y + 'px';
 		}
-		
+
 		return img;
 	},
-	
-	_createImg: function(src) {
+
+	_createImg: function (src) {
 		var el;
 		if (!L.Browser.ie6) {
 			el = document.createElement('img');
@@ -52,5 +60,9 @@ L.Icon = L.Class.extend({
 			el.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + src + '")';
 		}
 		return el;
+	},
+
+	_createDiv: function () {
+		return document.createElement('div');
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/marker/Marker.Drag.js b/src/layer/marker/Marker.Drag.js
new file mode 100644
index 0000000..17df68c
--- /dev/null
+++ b/src/layer/marker/Marker.Drag.js
@@ -0,0 +1,57 @@
+/*
+ * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
+ */
+
+L.Handler.MarkerDrag = L.Handler.extend({
+	initialize: function (marker) {
+		this._marker = marker;
+	},
+
+	addHooks: function () {
+		var icon = this._marker._icon;
+		if (!this._draggable) {
+			this._draggable = new L.Draggable(icon, icon);
+
+			this._draggable
+				.on('dragstart', this._onDragStart, this)
+				.on('drag', this._onDrag, this)
+				.on('dragend', this._onDragEnd, this);
+		}
+		this._draggable.enable();
+	},
+
+	removeHooks: function () {
+		this._draggable.disable();
+	},
+
+	moved: function () {
+		return this._draggable && this._draggable._moved;
+	},
+
+	_onDragStart: function (e) {
+		this._marker
+			.closePopup()
+			.fire('movestart')
+			.fire('dragstart');
+	},
+
+	_onDrag: function (e) {
+		// update shadow position
+		var iconPos = L.DomUtil.getPosition(this._marker._icon);
+		if (this._marker._shadow) {
+			L.DomUtil.setPosition(this._marker._shadow, iconPos);
+		}
+
+		this._marker._latlng = this._marker._map.layerPointToLatLng(iconPos);
+
+		this._marker
+			.fire('move')
+			.fire('drag');
+	},
+
+	_onDragEnd: function () {
+		this._marker
+			.fire('moveend')
+			.fire('dragend');
+	}
+});
diff --git a/src/layer/marker/Marker.Popup.js b/src/layer/marker/Marker.Popup.js
index 4c5cad0..3d4768d 100644
--- a/src/layer/marker/Marker.Popup.js
+++ b/src/layer/marker/Marker.Popup.js
@@ -1,28 +1,42 @@
 /*
- * Popup extension to L.Marker, adding openPopup & bindPopup methods. 
+ * Popup extension to L.Marker, adding openPopup & bindPopup methods.
  */
 
 L.Marker.include({
-	openPopup: function() {
+	openPopup: function () {
 		this._popup.setLatLng(this._latlng);
-		this._map.openPopup(this._popup);
-		
+		if (this._map) {
+			this._map.openPopup(this._popup);
+		}
+
 		return this;
 	},
-	
-	closePopup: function() {
+
+	closePopup: function () {
 		if (this._popup) {
 			this._popup._close();
 		}
+		return this;
 	},
-	
-	bindPopup: function(content, options) {
+
+	bindPopup: function (content, options) {
 		options = L.Util.extend({offset: this.options.icon.popupAnchor}, options);
-		
-		this._popup = new L.Popup(options);
+
+		if (!this._popup) {
+			this.on('click', this.openPopup, this);
+		}
+
+		this._popup = new L.Popup(options, this);
 		this._popup.setContent(content);
-		this.on('click', this.openPopup, this);
-		
+
+		return this;
+	},
+
+	unbindPopup: function () {
+		if (this._popup) {
+			this._popup = null;
+			this.off('click', this.openPopup);
+		}
 		return this;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/marker/Marker.js b/src/layer/marker/Marker.js
index b98bec4..795cb81 100644
--- a/src/layer/marker/Marker.js
+++ b/src/layer/marker/Marker.js
@@ -5,60 +5,85 @@
 L.Marker = L.Class.extend({
 
 	includes: L.Mixin.Events,
-	
+
 	options: {
 		icon: new L.Icon(),
 		title: '',
 		clickable: true,
-		draggable: false
+		draggable: false,
+		zIndexOffset: 0
 	},
-	
-	initialize: function(latlng, options) {
+
+	initialize: function (latlng, options) {
 		L.Util.setOptions(this, options);
 		this._latlng = latlng;
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map) {
 		this._map = map;
-		
+
 		this._initIcon();
-		
+
 		map.on('viewreset', this._reset, this);
 		this._reset();
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
 		this._removeIcon();
-		
+
+		// TODO move to Marker.Popup.js
+		if (this.closePopup) {
+			this.closePopup();
+		}
+
+		this._map = null;
+
 		map.off('viewreset', this._reset, this);
 	},
-	
-	getLatLng: function() {
+
+	getLatLng: function () {
 		return this._latlng;
 	},
-	
-	setLatLng: function(latlng) {
+
+	setLatLng: function (latlng) {
 		this._latlng = latlng;
-		this._reset();
+		if (this._icon) {
+			this._reset();
+
+			if (this._popup) {
+				this._popup.setLatLng(this._latlng);
+			}
+		}
 	},
-	
-	setIcon: function(icon) {
-		this._removeIcon();
-		
-		this._icon = this._shadow = null;
+
+	setZIndexOffset: function (offset) {
+		this.options.zIndexOffset = offset;
+		if (this._icon) {
+			this._reset();
+		}
+	},
+
+	setIcon: function (icon) {
+		if (this._map) {
+			this._removeIcon();
+		}
+
 		this.options.icon = icon;
-		
-		this._initIcon();
+
+		if (this._map) {
+			this._initIcon();
+			this._reset();
+		}
 	},
-	
-	_initIcon: function() {
+
+	_initIcon: function () {
 		if (!this._icon) {
 			this._icon = this.options.icon.createIcon();
-			
+
 			if (this.options.title) {
 				this._icon.title = this.options.title;
 			}
-			
+
 			this._initInteraction();
 		}
 		if (!this._shadow) {
@@ -68,31 +93,32 @@ L.Marker = L.Class.extend({
 		this._map._panes.markerPane.appendChild(this._icon);
 		if (this._shadow) {
 			this._map._panes.shadowPane.appendChild(this._shadow);
-		}		
+		}
 	},
-	
-	_removeIcon: function() {
+
+	_removeIcon: function () {
 		this._map._panes.markerPane.removeChild(this._icon);
 		if (this._shadow) {
 			this._map._panes.shadowPane.removeChild(this._shadow);
 		}
+		this._icon = this._shadow = null;
 	},
-	
-	_reset: function() {
+
+	_reset: function () {
 		var pos = this._map.latLngToLayerPoint(this._latlng).round();
-		
+
 		L.DomUtil.setPosition(this._icon, pos);
 		if (this._shadow) {
 			L.DomUtil.setPosition(this._shadow, pos);
 		}
-		
-		this._icon.style.zIndex = pos.y;
+
+		this._icon.style.zIndex = pos.y + this.options.zIndexOffset;
 	},
-	
-	_initInteraction: function() {
+
+	_initInteraction: function () {
 		if (this.options.clickable) {
 			this._icon.className += ' leaflet-clickable';
-			
+
 			L.DomEvent.addListener(this._icon, 'click', this._onMouseClick, this);
 
 			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
@@ -100,24 +126,24 @@ L.Marker = L.Class.extend({
 				L.DomEvent.addListener(this._icon, events[i], this._fireMouseEvent, this);
 			}
 		}
-		
+
 		if (L.Handler.MarkerDrag) {
 			this.dragging = new L.Handler.MarkerDrag(this);
-			
+
 			if (this.options.draggable) {
 				this.dragging.enable();
 			}
 		}
 	},
-	
-	_onMouseClick: function(e) {
+
+	_onMouseClick: function (e) {
 		L.DomEvent.stopPropagation(e);
 		if (this.dragging && this.dragging.moved()) { return; }
 		this.fire(e.type);
 	},
-	
-	_fireMouseEvent: function(e) {
+
+	_fireMouseEvent: function (e) {
 		this.fire(e.type);
 		L.DomEvent.stopPropagation(e);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/tile/TileLayer.Canvas.js b/src/layer/tile/TileLayer.Canvas.js
index 08bbaae..d1cc4c3 100644
--- a/src/layer/tile/TileLayer.Canvas.js
+++ b/src/layer/tile/TileLayer.Canvas.js
@@ -2,40 +2,53 @@ L.TileLayer.Canvas = L.TileLayer.extend({
 	options: {
 		async: false
 	},
-	
-	initialize: function(options) {
+
+	initialize: function (options) {
 		L.Util.setOptions(this, options);
 	},
-	
-	_createTileProto: function() {
+
+	redraw: function () {
+		for (var i in this._tiles) {
+			var tile = this._tiles[i];
+			this._redrawTile(tile);
+		}
+	},
+
+	_redrawTile: function (tile) {
+		this.drawTile(tile, tile._tilePoint, tile._zoom);
+	},
+
+	_createTileProto: function () {
 		this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');
-		
+
 		var tileSize = this.options.tileSize;
 		this._canvasProto.width = tileSize;
 		this._canvasProto.height = tileSize;
 	},
-	
-	_createTile: function() {
+
+	_createTile: function () {
 		var tile = this._canvasProto.cloneNode(false);
 		tile.onselectstart = tile.onmousemove = L.Util.falseFn;
 		return tile;
 	},
-	
-	_loadTile: function(tile, tilePoint, zoom) {
+
+	_loadTile: function (tile, tilePoint, zoom) {
 		tile._layer = this;
-		
+		tile._tilePoint = tilePoint;
+		tile._zoom = zoom;
+
 		this.drawTile(tile, tilePoint, zoom);
-		
+
 		if (!this.options.async) {
 			this.tileDrawn(tile);
 		}
 	},
-	
-	drawTile: function(tile, tilePoint, zoom) {
+
+	drawTile: function (tile, tilePoint, zoom) {
 		// override with rendering code
 	},
-	
-	tileDrawn: function(tile) {
+
+	tileDrawn: function (tile) {
 		this._tileOnLoad.call(tile);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/tile/TileLayer.WMS.js b/src/layer/tile/TileLayer.WMS.js
index 2f4ad05..17d0103 100644
--- a/src/layer/tile/TileLayer.WMS.js
+++ b/src/layer/tile/TileLayer.WMS.js
@@ -9,12 +9,12 @@ L.TileLayer.WMS = L.TileLayer.extend({
 		transparent: false
 	},
 
-	initialize: function(/*String*/ url, /*Object*/ options) {
+	initialize: function (/*String*/ url, /*Object*/ options) {
 		this._url = url;
-		
+
 		this.wmsParams = L.Util.extend({}, this.defaultWmsParams);
 		this.wmsParams.width = this.wmsParams.height = this.options.tileSize;
-		
+
 		for (var i in options) {
 			// all keys that are not TileLayer options go to WMS params
 			if (!this.options.hasOwnProperty(i)) {
@@ -24,15 +24,15 @@ L.TileLayer.WMS = L.TileLayer.extend({
 
 		L.Util.setOptions(this, options);
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map) {
 		var projectionKey = (parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs');
-		this.wmsParams[projectionKey] = map.options.crs.code;		
+		this.wmsParams[projectionKey] = map.options.crs.code;
 
 		L.TileLayer.prototype.onAdd.call(this, map);
 	},
-	
-	getTileUrl: function(/*Point*/ tilePoint, /*Number*/ zoom)/*-> String*/ {
+
+	getTileUrl: function (/*Point*/ tilePoint, /*Number*/ zoom)/*-> String*/ {
 		var tileSize = this.options.tileSize,
 			nwPoint = tilePoint.multiplyBy(tileSize),
 			sePoint = nwPoint.add(new L.Point(tileSize, tileSize)),
@@ -41,7 +41,7 @@ L.TileLayer.WMS = L.TileLayer.extend({
 			nw = this._map.options.crs.project(nwMap),
 			se = this._map.options.crs.project(seMap),
 			bbox = [nw.x, se.y, se.x, nw.y].join(',');
-		
+
 		return this._url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/tile/TileLayer.js b/src/layer/tile/TileLayer.js
index 68072ee..d3f1748 100644
--- a/src/layer/tile/TileLayer.js
+++ b/src/layer/tile/TileLayer.js
@@ -4,7 +4,7 @@
 
 L.TileLayer = L.Class.extend({
 	includes: L.Mixin.Events,
-	
+
 	options: {
 		minZoom: 0,
 		maxZoom: 18,
@@ -14,101 +14,138 @@ L.TileLayer = L.Class.extend({
 		attribution: '',
 		opacity: 1,
 		scheme: 'xyz',
-    noWrap: false,
-		
-		unloadInvisibleTiles: L.Browser.mobileWebkit,
-		updateWhenIdle: L.Browser.mobileWebkit
+		continuousWorld: false,
+		noWrap: false,
+		zoomOffset: 0,
+		zoomReverse: false,
+
+		unloadInvisibleTiles: L.Browser.mobile,
+		updateWhenIdle: L.Browser.mobile,
+		reuseTiles: L.Browser.mobile
 	},
-	
-	initialize: function(url, options) {
+
+	initialize: function (url, options, urlParams) {
 		L.Util.setOptions(this, options);
-		
+
 		this._url = url;
-		
-		if (typeof this.options.subdomains == 'string') {
+		this._urlParams = urlParams;
+
+		if (typeof this.options.subdomains === 'string') {
 			this.options.subdomains = this.options.subdomains.split('');
 		}
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map, insertAtTheBottom) {
 		this._map = map;
-		
+		this._insertAtTheBottom = insertAtTheBottom;
+
 		// create a container div for tiles
 		this._initContainer();
-		
+
 		// create an image to clone for tiles
 		this._createTileProto();
-		
+
 		// set up events
-		map.on('viewreset', this._reset, this);
-		
+		map.on('viewreset', this._resetCallback, this);
+
 		if (this.options.updateWhenIdle) {
 			map.on('moveend', this._update, this);
 		} else {
-			this._limitedUpdate = L.Util.limitExecByInterval(this._update, 100, this);
+			this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
 			map.on('move', this._limitedUpdate, this);
 		}
-		
+
 		this._reset();
 		this._update();
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
 		this._map.getPanes().tilePane.removeChild(this._container);
 		this._container = null;
-		
-		this._map.off('viewreset', this._reset, this);
-		
+
+		this._map.off('viewreset', this._resetCallback, this);
+
 		if (this.options.updateWhenIdle) {
 			this._map.off('moveend', this._update, this);
 		} else {
 			this._map.off('move', this._limitedUpdate, this);
 		}
 	},
-	
-	getAttribution: function() {
+
+	getAttribution: function () {
 		return this.options.attribution;
 	},
-	
-	setOpacity: function(opacity) {
+
+	setOpacity: function (opacity) {
 		this.options.opacity = opacity;
-		
+
 		this._setOpacity(opacity);
-		
+
 		// stupid webkit hack to force redrawing of tiles
 		if (L.Browser.webkit) {
-			for (i in this._tiles) {
-				this._tiles[i].style.webkitTransform += ' translate(0,0)';
+			for (var i in this._tiles) {
+				if (this._tiles.hasOwnProperty(i)) {
+					this._tiles[i].style.webkitTransform += ' translate(0,0)';
+				}
 			}
 		}
 	},
-	
-	_setOpacity: function(opacity) {
+
+	_setOpacity: function (opacity) {
 		if (opacity < 1) {
 			L.DomUtil.setOpacity(this._container, opacity);
 		}
 	},
-	
-	_initContainer: function() {
-		var tilePane = this._map.getPanes().tilePane;
-		
+
+	_initContainer: function () {
+		var tilePane = this._map.getPanes().tilePane,
+			first = tilePane.firstChild;
+
 		if (!this._container || tilePane.empty) {
-			this._container = L.DomUtil.create('div', 'leaflet-layer', tilePane);
-			
+			this._container = L.DomUtil.create('div', 'leaflet-layer');
+
+			if (this._insertAtTheBottom && first) {
+				tilePane.insertBefore(this._container, first);
+			} else {
+				tilePane.appendChild(this._container);
+			}
+
 			this._setOpacity(this.options.opacity);
 		}
 	},
-	
-	_reset: function() {
+
+	_resetCallback: function (e) {
+		this._reset(e.hard);
+	},
+
+	_reset: function (clearOldContainer) {
+		var key;
+		for (key in this._tiles) {
+			if (this._tiles.hasOwnProperty(key)) {
+				this.fire("tileunload", {tile: this._tiles[key]});
+			}
+		}
 		this._tiles = {};
+
+		if (this.options.reuseTiles) {
+			this._unusedTiles = [];
+		}
+
+		if (clearOldContainer && this._container) {
+			this._container.innerHTML = "";
+		}
 		this._initContainer();
-		this._container.innerHTML = '';
 	},
-	
-	_update: function() {
+
+	_update: function () {
 		var bounds = this._map.getPixelBounds(),
+			zoom = this._map.getZoom(),
 			tileSize = this.options.tileSize;
-		
+
+		if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
+			return;
+		}
+
 		var nwTilePoint = new L.Point(
 				Math.floor(bounds.min.x / tileSize),
 				Math.floor(bounds.min.y / tileSize)),
@@ -116,144 +153,186 @@ L.TileLayer = L.Class.extend({
 				Math.floor(bounds.max.x / tileSize),
 				Math.floor(bounds.max.y / tileSize)),
 			tileBounds = new L.Bounds(nwTilePoint, seTilePoint);
-		
+
 		this._addTilesFromCenterOut(tileBounds);
-		
-		if (this.options.unloadInvisibleTiles) {
+
+		if (this.options.unloadInvisibleTiles || this.options.reuseTiles) {
 			this._removeOtherTiles(tileBounds);
 		}
 	},
-	
-	_addTilesFromCenterOut: function(bounds) {
+
+	_addTilesFromCenterOut: function (bounds) {
 		var queue = [],
 			center = bounds.getCenter();
-		
+
 		for (var j = bounds.min.y; j <= bounds.max.y; j++) {
-			for (var i = bounds.min.x; i <= bounds.max.x; i++) {				
-				if ((i + ':' + j) in this._tiles) { continue; }
+			for (var i = bounds.min.x; i <= bounds.max.x; i++) {
+				if ((i + ':' + j) in this._tiles) {
+					continue;
+				}
 				queue.push(new L.Point(i, j));
 			}
 		}
-		
+
 		// load tiles in order of their distance to center
-		queue.sort(function(a, b) {
+		queue.sort(function (a, b) {
 			return a.distanceTo(center) - b.distanceTo(center);
 		});
-		
+
+		var fragment = document.createDocumentFragment();
+
 		this._tilesToLoad = queue.length;
 		for (var k = 0, len = this._tilesToLoad; k < len; k++) {
-			this._addTile(queue[k]);
+			this._addTile(queue[k], fragment);
 		}
+
+		this._container.appendChild(fragment);
 	},
-	
-	_removeOtherTiles: function(bounds) {
-		var kArr, x, y, key;
-		
+
+	_removeOtherTiles: function (bounds) {
+		var kArr, x, y, key, tile;
+
 		for (key in this._tiles) {
 			if (this._tiles.hasOwnProperty(key)) {
 				kArr = key.split(':');
 				x = parseInt(kArr[0], 10);
 				y = parseInt(kArr[1], 10);
-				
+
 				// remove tile if it's out of bounds
 				if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) {
-					this._tiles[key].src = '';
-					if (this._tiles[key].parentNode == this._container) {
-						this._container.removeChild(this._tiles[key]);
+
+					tile = this._tiles[key];
+					this.fire("tileunload", {tile: tile, url: tile.src});
+
+					if (tile.parentNode === this._container) {
+						this._container.removeChild(tile);
+					}
+					if (this.options.reuseTiles) {
+						this._unusedTiles.push(this._tiles[key]);
 					}
+					//tile.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
+
 					delete this._tiles[key];
 				}
 			}
-		}		
+		}
 	},
-	
-	_addTile: function(tilePoint) {
+
+	_addTile: function (tilePoint, container) {
 		var tilePos = this._getTilePos(tilePoint),
 			zoom = this._map.getZoom(),
-			key = tilePoint.x + ':' + tilePoint.y;
-			
+			key = tilePoint.x + ':' + tilePoint.y,
+			tileLimit = Math.pow(2, this._getOffsetZoom(zoom));
+
 		// wrap tile coordinates
-		var tileLimit = (1 << zoom);
-		if (!this.options.noWrap) {
-      tilePoint.x = ((tilePoint.x % tileLimit) + tileLimit) % tileLimit;
-    }
-		if (tilePoint.y < 0 || tilePoint.y >= tileLimit) { return; }
-		
-		// create tile
-		var tile = this._createTile();
+		if (!this.options.continuousWorld) {
+			if (!this.options.noWrap) {
+				tilePoint.x = ((tilePoint.x % tileLimit) + tileLimit) % tileLimit;
+			} else if (tilePoint.x < 0 || tilePoint.x >= tileLimit) {
+				this._tilesToLoad--;
+				return;
+			}
+
+			if (tilePoint.y < 0 || tilePoint.y >= tileLimit) {
+				this._tilesToLoad--;
+				return;
+			}
+		}
+
+		// get unused tile - or create a new tile
+		var tile = this._getTile();
 		L.DomUtil.setPosition(tile, tilePos);
-		
+
 		this._tiles[key] = tile;
-        
-		if (this.options.scheme == 'tms') {
+
+		if (this.options.scheme === 'tms') {
 			tilePoint.y = tileLimit - tilePoint.y - 1;
 		}
 
 		this._loadTile(tile, tilePoint, zoom);
-		
-		this._container.appendChild(tile);
+
+		container.appendChild(tile);
+	},
+
+	_getOffsetZoom: function (zoom) {
+		zoom = this.options.zoomReverse ? this.options.maxZoom - zoom : zoom;
+		return zoom + this.options.zoomOffset;
 	},
-	
-	_getTilePos: function(tilePoint) {
+
+	_getTilePos: function (tilePoint) {
 		var origin = this._map.getPixelOrigin(),
 			tileSize = this.options.tileSize;
-		
+
 		return tilePoint.multiplyBy(tileSize).subtract(origin);
 	},
-	
+
 	// image-specific code (override to implement e.g. Canvas or SVG tile layer)
-	
-	getTileUrl: function(tilePoint, zoom) {
+
+	getTileUrl: function (tilePoint, zoom) {
 		var subdomains = this.options.subdomains,
 			s = this.options.subdomains[(tilePoint.x + tilePoint.y) % subdomains.length];
 
-		return this._url
-				.replace('{s}', s)
-				.replace('{z}', zoom)
-				.replace('{x}', tilePoint.x)
-				.replace('{y}', tilePoint.y);
+		return L.Util.template(this._url, L.Util.extend({
+			s: s,
+			z: this._getOffsetZoom(zoom),
+			x: tilePoint.x,
+			y: tilePoint.y
+		}, this._urlParams));
 	},
-	
-	_createTileProto: function() {
+
+	_createTileProto: function () {
 		this._tileImg = L.DomUtil.create('img', 'leaflet-tile');
 		this._tileImg.galleryimg = 'no';
-		
+
 		var tileSize = this.options.tileSize;
 		this._tileImg.style.width = tileSize + 'px';
 		this._tileImg.style.height = tileSize + 'px';
 	},
-	
-	_createTile: function() {
+
+	_getTile: function () {
+		if (this.options.reuseTiles && this._unusedTiles.length > 0) {
+			var tile = this._unusedTiles.pop();
+			this._resetTile(tile);
+			return tile;
+		}
+		return this._createTile();
+	},
+
+	_resetTile: function (tile) {
+		// Override if data stored on a tile needs to be cleaned up before reuse
+	},
+
+	_createTile: function () {
 		var tile = this._tileImg.cloneNode(false);
 		tile.onselectstart = tile.onmousemove = L.Util.falseFn;
 		return tile;
 	},
-	
-	_loadTile: function(tile, tilePoint, zoom) {
+
+	_loadTile: function (tile, tilePoint, zoom) {
 		tile._layer = this;
 		tile.onload = this._tileOnLoad;
 		tile.onerror = this._tileOnError;
 		tile.src = this.getTileUrl(tilePoint, zoom);
 	},
-	
-	_tileOnLoad: function(e) {
+
+	_tileOnLoad: function (e) {
 		var layer = this._layer;
-		
-		this.className += ' leaflet-tile-loaded'; 
+
+		this.className += ' leaflet-tile-loaded';
 
 		layer.fire('tileload', {tile: this, url: this.src});
-		
+
 		layer._tilesToLoad--;
 		if (!layer._tilesToLoad) {
 			layer.fire('load');
 		}
 	},
-	
-	_tileOnError: function(e) {
+
+	_tileOnError: function (e) {
 		var layer = this._layer;
-		
+
 		layer.fire('tileerror', {tile: this, url: this.src});
-		
+
 		var newUrl = layer.options.errorTileUrl;
 		if (newUrl) {
 			this.src = newUrl;
diff --git a/src/layer/vector/Circle.js b/src/layer/vector/Circle.js
index c737c19..b48e1f1 100644
--- a/src/layer/vector/Circle.js
+++ b/src/layer/vector/Circle.js
@@ -1,51 +1,68 @@
 /*
- * L.Circle is a circle overlay (with a certain radius in meters). 
+ * L.Circle is a circle overlay (with a certain radius in meters).
  */
 
 L.Circle = L.Path.extend({
-	initialize: function(latlng, radius, options) {
+	initialize: function (latlng, radius, options) {
 		L.Path.prototype.initialize.call(this, options);
-		
+
 		this._latlng = latlng;
 		this._mRadius = radius;
 	},
-	
+
 	options: {
 		fill: true
 	},
-	
-	setLatLng: function(latlng) {
+
+	setLatLng: function (latlng) {
 		this._latlng = latlng;
 		this._redraw();
 		return this;
 	},
-	
-	setRadius: function(radius) {
+
+	setRadius: function (radius) {
 		this._mRadius = radius;
 		this._redraw();
 		return this;
 	},
-	
-	projectLatlngs: function() {
+
+	projectLatlngs: function () {
 		var equatorLength = 40075017,
-			scale = this._map.options.scale(this._map._zoom);
-		
+			hLength = equatorLength * Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
+
+		var lngSpan = (this._mRadius / hLength) * 360,
+			latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngSpan, true),
+			point2 = this._map.latLngToLayerPoint(latlng2);
+
 		this._point = this._map.latLngToLayerPoint(this._latlng);
-		this._radius = (this._mRadius / equatorLength) * scale; 
+		this._radius = Math.round(this._point.x - point2.x);
 	},
-	
-	getPathString: function() {
+
+	getPathString: function () {
 		var p = this._point,
 			r = this._radius;
-		
-		if (L.Path.SVG) {
-			return "M" + p.x + "," + (p.y - r) + 
-					"A" + r + "," + r + ",0,1,1," + 
+
+		if (this._checkIfEmpty()) {
+			return '';
+		}
+
+		if (L.Browser.svg) {
+			return "M" + p.x + "," + (p.y - r) +
+					"A" + r + "," + r + ",0,1,1," +
 					(p.x - 0.1) + "," + (p.y - r) + " z";
 		} else {
 			p._round();
 			r = Math.round(r);
 			return "AL " + p.x + "," + p.y + " " + r + "," + r + " 0," + (65535 * 360);
 		}
+	},
+
+	_checkIfEmpty: function () {
+		var vp = this._map._pathViewport,
+			r = this._radius,
+			p = this._point;
+
+		return p.x - r > vp.max.x || p.y - r > vp.max.y ||
+			p.x + r < vp.min.x || p.y + r < vp.min.y;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/vector/CircleMarker.js b/src/layer/vector/CircleMarker.js
index fa4bacf..0b60d3d 100644
--- a/src/layer/vector/CircleMarker.js
+++ b/src/layer/vector/CircleMarker.js
@@ -1,5 +1,5 @@
 /*
- * L.CircleMarker is a circle overlay with a permanent pixel radius. 
+ * L.CircleMarker is a circle overlay with a permanent pixel radius.
  */
 
 L.CircleMarker = L.Circle.extend({
@@ -7,19 +7,19 @@ L.CircleMarker = L.Circle.extend({
 		radius: 10,
 		weight: 2
 	},
-	
-	initialize: function(latlng, options) {
+
+	initialize: function (latlng, options) {
 		L.Circle.prototype.initialize.call(this, latlng, null, options);
 		this._radius = this.options.radius;
 	},
-	
-	projectLatlngs: function() {
+
+	projectLatlngs: function () {
 		this._point = this._map.latLngToLayerPoint(this._latlng);
 	},
-	
-	setRadius: function(radius) {
+
+	setRadius: function (radius) {
 		this._radius = radius;
 		this._redraw();
 		return this;
-	}	
-});
\ No newline at end of file
+	}
+});
diff --git a/src/layer/vector/MultiPoly.js b/src/layer/vector/MultiPoly.js
index 60d6de6..0109c30 100644
--- a/src/layer/vector/MultiPoly.js
+++ b/src/layer/vector/MultiPoly.js
@@ -1,22 +1,29 @@
 /*
- * Contains L.MultiPolyline and L.MultiPolygon layers. 
+ * Contains L.MultiPolyline and L.MultiPolygon layers.
  */
 
-(function() {
-	function createMulti(klass) {
+(function () {
+	function createMulti(Klass) {
 		return L.FeatureGroup.extend({
-			initialize: function(latlngs, options) {
+			initialize: function (latlngs, options) {
 				this._layers = {};
-				for (var i = 0, len = latlngs.length; i < len; i++) {
-					this.addLayer(new klass(latlngs[i], options));
-				}
+				this._options = options;
+				this.setLatLngs(latlngs);
 			},
 
-			setStyle: function(style) {
-				for (var i in this._layers) {
-					if (this._layers.hasOwnProperty(i) && this._layers[i].setStyle) {
-						this._layers[i].setStyle(style);
+			setLatLngs: function (latlngs) {
+				var i = 0, len = latlngs.length;
+
+				this._iterateLayers(function (layer) {
+					if (i < len) {
+						layer.setLatLngs(latlngs[i++]);
+					} else {
+						this.removeLayer(layer);
 					}
+				}, this);
+
+				while (i < len) {
+					this.addLayer(new Klass(latlngs[i++], this._options));
 				}
 			}
 		});
diff --git a/src/layer/vector/Path.Popup.js b/src/layer/vector/Path.Popup.js
index b82a492..337747b 100644
--- a/src/layer/vector/Path.Popup.js
+++ b/src/layer/vector/Path.Popup.js
@@ -1,24 +1,24 @@
 /*
- * Popup extension to L.Path (polylines, polygons, circles), adding bindPopup method. 
+ * Popup extension to L.Path (polylines, polygons, circles), adding bindPopup method.
  */
 
 L.Path.include({
-	bindPopup: function(content, options) {
+	bindPopup: function (content, options) {
 		if (!this._popup || this._popup.options !== options) {
-			this._popup = new L.Popup(options);
+			this._popup = new L.Popup(options, this);
 		}
 		this._popup.setContent(content);
-		
+
 		if (!this._openPopupAdded) {
 			this.on('click', this._openPopup, this);
 			this._openPopupAdded = true;
 		}
-		
+
 		return this;
 	},
-	
-	_openPopup: function(e) {
+
+	_openPopup: function (e) {
 		this._popup.setLatLng(e.latlng);
 		this._map.openPopup(this._popup);
-	}	
-});
\ No newline at end of file
+	}
+});
diff --git a/src/layer/vector/Path.SVG.js b/src/layer/vector/Path.SVG.js
new file mode 100644
index 0000000..329e2e9
--- /dev/null
+++ b/src/layer/vector/Path.SVG.js
@@ -0,0 +1,138 @@
+L.Path.SVG_NS = 'http://www.w3.org/2000/svg';
+
+L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect);
+
+L.Path = L.Path.extend({
+	statics: {
+		SVG: L.Browser.svg,
+		_createElement: function (name) {
+			return document.createElementNS(L.Path.SVG_NS, name);
+		}
+	},
+
+	getPathString: function () {
+		// form path string here
+	},
+
+	_initElements: function () {
+		this._map._initPathRoot();
+		this._initPath();
+		this._initStyle();
+	},
+
+	_initPath: function () {
+		this._container = L.Path._createElement('g');
+
+		this._path = L.Path._createElement('path');
+		this._container.appendChild(this._path);
+
+		this._map._pathRoot.appendChild(this._container);
+	},
+
+	_initStyle: function () {
+		if (this.options.stroke) {
+			this._path.setAttribute('stroke-linejoin', 'round');
+			this._path.setAttribute('stroke-linecap', 'round');
+		}
+		if (this.options.fill) {
+			this._path.setAttribute('fill-rule', 'evenodd');
+		} else {
+			this._path.setAttribute('fill', 'none');
+		}
+		this._updateStyle();
+	},
+
+	_updateStyle: function () {
+		if (this.options.stroke) {
+			this._path.setAttribute('stroke', this.options.color);
+			this._path.setAttribute('stroke-opacity', this.options.opacity);
+			this._path.setAttribute('stroke-width', this.options.weight);
+		}
+		if (this.options.fill) {
+			this._path.setAttribute('fill', this.options.fillColor || this.options.color);
+			this._path.setAttribute('fill-opacity', this.options.fillOpacity);
+		}
+	},
+
+	_updatePath: function () {
+		var str = this.getPathString();
+		if (!str) {
+			// fix webkit empty string parsing bug
+			str = 'M0 0';
+		}
+		this._path.setAttribute('d', str);
+	},
+
+	// TODO remove duplication with L.Map
+	_initEvents: function () {
+		if (this.options.clickable) {
+			if (!L.Browser.vml) {
+				this._path.setAttribute('class', 'leaflet-clickable');
+			}
+
+			L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
+
+			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'mousemove'];
+			for (var i = 0; i < events.length; i++) {
+				L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
+			}
+		}
+	},
+
+	_onMouseClick: function (e) {
+		if (this._map.dragging && this._map.dragging.moved()) {
+			return;
+		}
+		this._fireMouseEvent(e);
+	},
+
+	_fireMouseEvent: function (e) {
+		if (!this.hasEventListeners(e.type)) {
+			return;
+		}
+		this.fire(e.type, {
+			latlng: this._map.mouseEventToLatLng(e),
+			layerPoint: this._map.mouseEventToLayerPoint(e)
+		});
+		L.DomEvent.stopPropagation(e);
+	}
+});
+
+L.Map.include({
+	_initPathRoot: function () {
+		if (!this._pathRoot) {
+			this._pathRoot = L.Path._createElement('svg');
+			this._panes.overlayPane.appendChild(this._pathRoot);
+
+			this.on('moveend', this._updateSvgViewport);
+			this._updateSvgViewport();
+		}
+	},
+
+	_updateSvgViewport: function () {
+		this._updatePathViewport();
+
+		var vp = this._pathViewport,
+			min = vp.min,
+			max = vp.max,
+			width = max.x - min.x,
+			height = max.y - min.y,
+			root = this._pathRoot,
+			pane = this._panes.overlayPane;
+
+		// Hack to make flicker on drag end on mobile webkit less irritating
+		// Unfortunately I haven't found a good workaround for this yet
+		if (L.Browser.webkit) {
+			pane.removeChild(root);
+		}
+
+		L.DomUtil.setPosition(root, min);
+		root.setAttribute('width', width);
+		root.setAttribute('height', height);
+		root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
+
+		if (L.Browser.webkit) {
+			pane.appendChild(root);
+		}
+	}
+});
diff --git a/src/layer/vector/Path.VML.js b/src/layer/vector/Path.VML.js
index 8481d99..631a3dd 100644
--- a/src/layer/vector/Path.VML.js
+++ b/src/layer/vector/Path.VML.js
@@ -1,61 +1,50 @@
 /*
- * Vector rendering for IE6-8 through VML. 
+ * Vector rendering for IE6-8 through VML.
  * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
  */
 
-L.Path.VML = (function() {
+L.Browser.vml = (function () {
 	var d = document.createElement('div'), s;
 	d.innerHTML = '<v:shape adj="1"/>';
 	s = d.firstChild;
 	s.style.behavior = 'url(#default#VML)';
-	
-	return (s && (typeof s.adj == 'object'));
-})();
 
-L.Path = L.Path.SVG || !L.Path.VML ? L.Path : L.Path.extend({
+	return (s && (typeof s.adj === 'object'));
+}());
+
+L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({
 	statics: {
-		CLIP_PADDING: 0.02
+		VML: true,
+		CLIP_PADDING: 0.02,
+		_createElement: (function () {
+			try {
+				document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
+				return function (name) {
+					return document.createElement('<lvml:' + name + ' class="lvml">');
+				};
+			} catch (e) {
+				return function (name) {
+					return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
+				};
+			}
+		}())
 	},
-	
-	_createElement: (function() { 
-		try {
-			document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
-			return function(name) {
-				return document.createElement('<lvml:' + name + ' class="lvml">');
-			};
-		} catch (e) {
-			return function(name) {
-				return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
-			};
-		}
-	})(),
-	
-	_initRoot: function() {
-		if (!this._map._pathRoot) {
-			this._map._pathRoot = document.createElement('div');
-			this._map._pathRoot.className = 'leaflet-vml-container';
-			this._map._panes.overlayPane.appendChild(this._map._pathRoot);
 
-			this._map.on('moveend', this._updateViewport, this);
-			this._updateViewport();
-		}
-	},
-	
-	_initPath: function() {
-		this._container = this._createElement('shape');
-		this._container.className += ' leaflet-vml-shape' + 
+	_initPath: function () {
+		this._container = L.Path._createElement('shape');
+		this._container.className += ' leaflet-vml-shape' +
 				(this.options.clickable ? ' leaflet-clickable' : '');
 		this._container.coordsize = '1 1';
-		
-		this._path = this._createElement('path');
+
+		this._path = L.Path._createElement('path');
 		this._container.appendChild(this._path);
-		
+
 		this._map._pathRoot.appendChild(this._container);
 	},
-	
-	_initStyle: function() {
+
+	_initStyle: function () {
 		if (this.options.stroke) {
-			this._stroke = this._createElement('stroke');
+			this._stroke = L.Path._createElement('stroke');
 			this._stroke.endcap = 'round';
 			this._container.appendChild(this._stroke);
 		} else {
@@ -63,15 +52,15 @@ L.Path = L.Path.SVG || !L.Path.VML ? L.Path : L.Path.extend({
 		}
 		if (this.options.fill) {
 			this._container.filled = true;
-			this._fill = this._createElement('fill');
+			this._fill = L.Path._createElement('fill');
 			this._container.appendChild(this._fill);
 		} else {
 			this._container.filled = false;
 		}
 		this._updateStyle();
 	},
-	
-	_updateStyle: function() {
+
+	_updateStyle: function () {
 		if (this.options.stroke) {
 			this._stroke.weight = this.options.weight + 'px';
 			this._stroke.color = this.options.color;
@@ -82,10 +71,23 @@ L.Path = L.Path.SVG || !L.Path.VML ? L.Path : L.Path.extend({
 			this._fill.opacity = this.options.fillOpacity;
 		}
 	},
-	
-	_updatePath: function() {
+
+	_updatePath: function () {
 		this._container.style.display = 'none';
 		this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug
 		this._container.style.display = '';
 	}
-});
\ No newline at end of file
+});
+
+L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : {
+	_initPathRoot: function () {
+		if (!this._pathRoot) {
+			this._pathRoot = document.createElement('div');
+			this._pathRoot.className = 'leaflet-vml-container';
+			this._panes.overlayPane.appendChild(this._pathRoot);
+
+			this.on('moveend', this._updatePathViewport);
+			this._updatePathViewport();
+		}
+	}
+});
diff --git a/src/layer/vector/Path.js b/src/layer/vector/Path.js
index 3d4837c..bcaf4ff 100644
--- a/src/layer/vector/Path.js
+++ b/src/layer/vector/Path.js
@@ -4,204 +4,85 @@
 
 L.Path = L.Class.extend({
 	includes: [L.Mixin.Events],
-	
-	statics: (function() {
-		var svgns = 'http://www.w3.org/2000/svg',
-			ce = 'createElementNS';
-		
-		return {
-			SVG_NS: svgns,
-			SVG: !!(document[ce] && document[ce](svgns, 'svg').createSVGRect),
-			
-			// how much to extend the clip area around the map view 
-			// (relative to its size, e.g. 0.5 is half the screen in each direction)
-			CLIP_PADDING: 0.5
-		};
-	})(),
-	
+
+	statics: {
+		// how much to extend the clip area around the map view
+		// (relative to its size, e.g. 0.5 is half the screen in each direction)
+		CLIP_PADDING: 0.5
+	},
+
 	options: {
 		stroke: true,
 		color: '#0033ff',
 		weight: 5,
 		opacity: 0.5,
-		
+
 		fill: false,
 		fillColor: null, //same as color by default
 		fillOpacity: 0.2,
-		
+
 		clickable: true,
-		
-		updateOnMoveEnd: false
+
+		// TODO remove this, as all paths now update on moveend
+		updateOnMoveEnd: true
 	},
-	
-	initialize: function(options) {
+
+	initialize: function (options) {
 		L.Util.setOptions(this, options);
 	},
-	
-	onAdd: function(map) {
+
+	onAdd: function (map) {
 		this._map = map;
-		
+
 		this._initElements();
 		this._initEvents();
 		this.projectLatlngs();
 		this._updatePath();
 
 		map.on('viewreset', this.projectLatlngs, this);
-		
+
 		this._updateTrigger = this.options.updateOnMoveEnd ? 'moveend' : 'viewreset';
 		map.on(this._updateTrigger, this._updatePath, this);
 	},
-	
-	onRemove: function(map) {
+
+	onRemove: function (map) {
+		this._map = null;
+
 		map._pathRoot.removeChild(this._container);
-		map.off('viewreset', this._projectLatlngs, this);
+
+		map.off('viewreset', this.projectLatlngs, this);
 		map.off(this._updateTrigger, this._updatePath, this);
 	},
-	
-	projectLatlngs: function() {
+
+	projectLatlngs: function () {
 		// do all projection stuff here
 	},
-	
-	getPathString: function() {
-		// form path string here
-	},
-	
-	setStyle: function(style) {
+
+	setStyle: function (style) {
 		L.Util.setOptions(this, style);
-		if (this._path) {
+		if (this._container) {
 			this._updateStyle();
 		}
+		return this;
 	},
-	
-	_initElements: function() {
-		this._initRoot();
-		this._initPath();
-		this._initStyle();
-	},
-	
-	_initRoot: function() {
-		if (!this._map._pathRoot) {
-			this._map._pathRoot = this._createElement('svg');
-			this._map._panes.overlayPane.appendChild(this._map._pathRoot);
-
-			this._map.on('moveend', this._updateSvgViewport, this);
-			this._updateSvgViewport();
+
+	_redraw: function () {
+		if (this._map) {
+			this.projectLatlngs();
+			this._updatePath();
 		}
-	},
-	
-	_updateSvgViewport: function() {
-		this._updateViewport();
-		
-		var vp = this._map._pathViewport,
-			min = vp.min,
-			max = vp.max,
-			width = max.x - min.x,
-			height = max.y - min.y,
-			root = this._map._pathRoot,
-			pane = this._map._panes.overlayPane;
-	
-		// Hack to make flicker on drag end on mobile webkit less irritating
-		// Unfortunately I haven't found a good workaround for this yet
-		if (L.Browser.mobileWebkit) { pane.removeChild(root); }
-		
-		L.DomUtil.setPosition(root, min);
-		root.setAttribute('width', width);
-		root.setAttribute('height', height);
-		root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
-		
-		if (L.Browser.mobileWebkit) { pane.appendChild(root); }
-	},
-	
-	_updateViewport: function() {
+	}
+});
+
+L.Map.include({
+	_updatePathViewport: function () {
 		var p = L.Path.CLIP_PADDING,
-			size = this._map.getSize(),
+			size = this.getSize(),
 			//TODO this._map._getMapPanePos()
-			panePos = L.DomUtil.getPosition(this._map._mapPane), 
+			panePos = L.DomUtil.getPosition(this._mapPane),
 			min = panePos.multiplyBy(-1).subtract(size.multiplyBy(p)),
 			max = min.add(size.multiplyBy(1 + p * 2));
-		
-		this._map._pathViewport = new L.Bounds(min, max);
-	},
-	
-	_initPath: function() {
-		this._container = this._createElement('g');
-		
-		this._path = this._createElement('path');
-		this._container.appendChild(this._path);
-		
-		this._map._pathRoot.appendChild(this._container);
-	},
-	
-	_initStyle: function() {
-		if (this.options.stroke) {
-			this._path.setAttribute('stroke-linejoin', 'round');
-			this._path.setAttribute('stroke-linecap', 'round');
-		}
-		if (this.options.fill) {
-			this._path.setAttribute('fill-rule', 'evenodd');
-		} else {
-			this._path.setAttribute('fill', 'none');
-		}
-		this._updateStyle();
-	},
-	
-	_updateStyle: function() {
-		if (this.options.stroke) {
-			this._path.setAttribute('stroke', this.options.color);
-			this._path.setAttribute('stroke-opacity', this.options.opacity);
-			this._path.setAttribute('stroke-width', this.options.weight);
-		}
-		if (this.options.fill) {
-			this._path.setAttribute('fill', this.options.fillColor || this.options.color);
-			this._path.setAttribute('fill-opacity', this.options.fillOpacity);
-		}
-	},
-	
-	_updatePath: function() {
-		var str = this.getPathString();
-		if (!str) {
-			// fix webkit empty string parsing bug
-			str = 'M0 0';
-		}
-		this._path.setAttribute('d', str);
-	},
-	
-	_createElement: function(name) {
-		return document.createElementNS(L.Path.SVG_NS, name);
-	},
-	
-	// TODO remove duplication with L.Map
-	_initEvents: function() {
-		if (this.options.clickable) {
-			if (!L.Path.VML) {
-				this._path.setAttribute('class', 'leaflet-clickable');
-			}
-			
-			L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
-
-			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
-			for (var i = 0; i < events.length; i++) {
-				L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
-			}
-		}
-	},
-	
-	_onMouseClick: function(e) {
-		if (this._map.dragging && this._map.dragging.moved()) { return; }
-		this._fireMouseEvent(e);
-	},
-	
-	_fireMouseEvent: function(e) {
-		if (!this.hasEventListeners(e.type)) { return; }
-		this.fire(e.type, {
-			latlng: this._map.mouseEventToLatLng(e),
-			layerPoint: this._map.mouseEventToLayerPoint(e)
-		});
-		L.DomEvent.stopPropagation(e);
-	},
-	
-	_redraw: function() {
-		this.projectLatlngs();
-		this._updatePath();
+
+		this._pathViewport = new L.Bounds(min, max);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/vector/Polygon.js b/src/layer/vector/Polygon.js
index 52bf2d6..c71fc36 100644
--- a/src/layer/vector/Polygon.js
+++ b/src/layer/vector/Polygon.js
@@ -6,53 +6,59 @@ L.Polygon = L.Polyline.extend({
 	options: {
 		fill: true
 	},
-	
-	initialize: function(latlngs, options) {
+
+	initialize: function (latlngs, options) {
 		L.Polyline.prototype.initialize.call(this, latlngs, options);
-		
-		if (latlngs[0] instanceof Array) {
+
+		if (latlngs && (latlngs[0] instanceof Array)) {
 			this._latlngs = latlngs[0];
 			this._holes = latlngs.slice(1);
 		}
 	},
-	
-	projectLatlngs: function() {
+
+	projectLatlngs: function () {
 		L.Polyline.prototype.projectLatlngs.call(this);
-		
+
 		// project polygon holes points
 		// TODO move this logic to Polyline to get rid of duplication
 		this._holePoints = [];
-		
-		if (!this._holes) return;
-		
+
+		if (!this._holes) {
+			return;
+		}
+
 		for (var i = 0, len = this._holes.length, hole; i < len; i++) {
 			this._holePoints[i] = [];
-			
-			for(var j = 0, len2 = this._holes[i].length; j < len2; j++) {
+
+			for (var j = 0, len2 = this._holes[i].length; j < len2; j++) {
 				this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]);
 			}
 		}
 	},
-	
-	_clipPoints: function() {
+
+	_clipPoints: function () {
 		var points = this._originalPoints,
 			newParts = [];
-		
+
 		this._parts = [points].concat(this._holePoints);
-		
-		if (this.options.noClip) return;
-		
+
+		if (this.options.noClip) {
+			return;
+		}
+
 		for (var i = 0, len = this._parts.length; i < len; i++) {
 			var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport);
-			if (!clipped.length) continue;
+			if (!clipped.length) {
+				continue;
+			}
 			newParts.push(clipped);
 		}
-		
+
 		this._parts = newParts;
 	},
-	
-	_getPathPartStr: function(points) {
+
+	_getPathPartStr: function (points) {
 		var str = L.Polyline.prototype._getPathPartStr.call(this, points);
-		return str + (L.Path.SVG ? 'z' : 'x');
+		return str + (L.Browser.svg ? 'z' : 'x');
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/vector/Polyline.js b/src/layer/vector/Polyline.js
index 606d7d7..4113658 100644
--- a/src/layer/vector/Polyline.js
+++ b/src/layer/vector/Polyline.js
@@ -1,68 +1,100 @@
 
 L.Polyline = L.Path.extend({
-	initialize: function(latlngs, options) {
+	initialize: function (latlngs, options) {
 		L.Path.prototype.initialize.call(this, options);
 		this._latlngs = latlngs;
 	},
-	
+
 	options: {
 		// how much to simplify the polyline on each zoom level
 		// more = better performance and smoother look, less = more accurate
 		smoothFactor: 1.0,
 		noClip: false,
-		
+
 		updateOnMoveEnd: true
 	},
-	
-	projectLatlngs: function() {
+
+	projectLatlngs: function () {
 		this._originalPoints = [];
-		
+
 		for (var i = 0, len = this._latlngs.length; i < len; i++) {
 			this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]);
 		}
 	},
-	
-	getPathString: function() {
+
+	getPathString: function () {
 		for (var i = 0, len = this._parts.length, str = ''; i < len; i++) {
 			str += this._getPathPartStr(this._parts[i]);
 		}
 		return str;
 	},
-	
-	getLatLngs: function() {
+
+	getLatLngs: function () {
 		return this._latlngs;
 	},
-	
-	setLatLngs: function(latlngs) {
+
+	setLatLngs: function (latlngs) {
 		this._latlngs = latlngs;
 		this._redraw();
 		return this;
 	},
-	
-	addLatLng: function(latlng) {
+
+	addLatLng: function (latlng) {
 		this._latlngs.push(latlng);
 		this._redraw();
 		return this;
 	},
-	
-	spliceLatLngs: function(index, howMany) {
+
+	spliceLatLngs: function (index, howMany) {
 		var removed = [].splice.apply(this._latlngs, arguments);
 		this._redraw();
 		return removed;
 	},
-	
-	_getPathPartStr: function(points) {
+
+	closestLayerPoint: function (p) {
+		var minDistance = Infinity, parts = this._parts, p1, p2, minPoint = null;
+
+		for (var j = 0, jLen = parts.length; j < jLen; j++) {
+			var points = parts[j];
+			for (var i = 1, len = points.length; i < len; i++) {
+				p1 = points[i - 1];
+				p2 = points[i];
+				var point = L.LineUtil._sqClosestPointOnSegment(p, p1, p2);
+				if (point._sqDist < minDistance) {
+					minDistance = point._sqDist;
+					minPoint = point;
+				}
+			}
+		}
+		if (minPoint) {
+			minPoint.distance = Math.sqrt(minDistance);
+		}
+		return minPoint;
+	},
+
+	getBounds: function () {
+		var b = new L.LatLngBounds();
+		var latLngs = this.getLatLngs();
+		for (var i = 0, len = latLngs.length; i < len; i++) {
+			b.extend(latLngs[i]);
+		}
+		return b;
+	},
+
+	_getPathPartStr: function (points) {
 		var round = L.Path.VML;
-		
+
 		for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) {
 			p = points[j];
-			if (round) p._round();
+			if (round) {
+				p._round();
+			}
 			str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
 		}
 		return str;
 	},
-	
-	_clipPoints: function() {
+
+	_clipPoints: function () {
 		var points = this._originalPoints,
 			len = points.length,
 			i, k, segment;
@@ -71,42 +103,44 @@ L.Polyline = L.Path.extend({
 			this._parts = [points];
 			return;
 		}
-		
+
 		this._parts = [];
-		
+
 		var parts = this._parts,
 			vp = this._map._pathViewport,
 			lu = L.LineUtil;
-		
+
 		for (i = 0, k = 0; i < len - 1; i++) {
-			segment = lu.clipSegment(points[i], points[i+1], vp, i);
-			if (!segment) continue;
-			
+			segment = lu.clipSegment(points[i], points[i + 1], vp, i);
+			if (!segment) {
+				continue;
+			}
+
 			parts[k] = parts[k] || [];
 			parts[k].push(segment[0]);
-			
+
 			// if segment goes out of screen, or it's the last one, it's the end of the line part
-			if ((segment[1] != points[i+1]) || (i == len - 2)) {
+			if ((segment[1] !== points[i + 1]) || (i === len - 2)) {
 				parts[k].push(segment[1]);
-				k++;  
+				k++;
 			}
 		}
 	},
-	
+
 	// simplify each clipped part of the polyline
-	_simplifyPoints: function() {
+	_simplifyPoints: function () {
 		var parts = this._parts,
 			lu = L.LineUtil;
-		
+
 		for (var i = 0, len = parts.length; i < len; i++) {
 			parts[i] = lu.simplify(parts[i], this.options.smoothFactor);
 		}
 	},
-	
-	_updatePath: function() {
+
+	_updatePath: function () {
 		this._clipPoints();
 		this._simplifyPoints();
-		
+
 		L.Path.prototype._updatePath.call(this);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/layer/vector/canvas/Circle.Canvas.js b/src/layer/vector/canvas/Circle.Canvas.js
new file mode 100644
index 0000000..8a79f84
--- /dev/null
+++ b/src/layer/vector/canvas/Circle.Canvas.js
@@ -0,0 +1,18 @@
+/*
+ * Circle canvas specific drawing parts.
+ */
+
+L.Circle.include(!L.Path.CANVAS ? {} : {
+	_drawPath: function () {
+		var p = this._point;
+		this._ctx.beginPath();
+		this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2);
+	},
+
+	_containsPoint: function (p) {
+		var center = this._point,
+			w2 = this.options.stroke ? this.options.weight / 2 : 0;
+
+		return (p.distanceTo(center) <= this._radius + w2);
+	}
+});
diff --git a/src/layer/vector/canvas/Path.Canvas.js b/src/layer/vector/canvas/Path.Canvas.js
new file mode 100644
index 0000000..019d46c
--- /dev/null
+++ b/src/layer/vector/canvas/Path.Canvas.js
@@ -0,0 +1,146 @@
+/*
+ * Vector rendering for all browsers that support canvas.
+ */
+
+L.Browser.canvas = (function () {
+	return !!document.createElement('canvas').getContext;
+}());
+
+L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path : L.Path.extend({
+	statics: {
+		//CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value
+		CANVAS: true,
+		SVG: false
+	},
+
+	options: {
+		updateOnMoveEnd: true
+	},
+
+	_initElements: function () {
+		this._map._initPathRoot();
+		this._ctx = this._map._canvasCtx;
+	},
+
+	_updateStyle: function () {
+		if (this.options.stroke) {
+			this._ctx.lineWidth = this.options.weight;
+			this._ctx.strokeStyle = this.options.color;
+		}
+		if (this.options.fill) {
+			this._ctx.fillStyle = this.options.fillColor || this.options.color;
+		}
+	},
+
+	_drawPath: function () {
+		var i, j, len, len2, point, drawMethod;
+
+		this._ctx.beginPath();
+
+		for (i = 0, len = this._parts.length; i < len; i++) {
+			for (j = 0, len2 = this._parts[i].length; j < len2; j++) {
+				point = this._parts[i][j];
+				drawMethod = (j === 0 ? 'move' : 'line') + 'To';
+
+				this._ctx[drawMethod](point.x, point.y);
+			}
+			// TODO refactor ugly hack
+			if (this instanceof L.Polygon) {
+				this._ctx.closePath();
+			}
+		}
+	},
+
+	_checkIfEmpty: function () {
+		return !this._parts.length;
+	},
+
+	_updatePath: function () {
+		if (this._checkIfEmpty()) {
+			return;
+		}
+
+		this._drawPath();
+
+		this._ctx.save();
+
+		this._updateStyle();
+
+		var opacity = this.options.opacity,
+			fillOpacity = this.options.fillOpacity;
+
+		if (this.options.fill) {
+			if (fillOpacity < 1) {
+				this._ctx.globalAlpha = fillOpacity;
+			}
+			this._ctx.fill();
+		}
+
+		if (this.options.stroke) {
+			if (opacity < 1) {
+				this._ctx.globalAlpha = opacity;
+			}
+			this._ctx.stroke();
+		}
+
+		this._ctx.restore();
+
+		// TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
+	},
+
+	_initEvents: function () {
+		if (this.options.clickable) {
+			// TODO hand cursor
+			// TODO mouseover, mouseout, dblclick
+			this._map.on('click', this._onClick, this);
+		}
+	},
+
+	_onClick: function (e) {
+		if (this._containsPoint(e.layerPoint)) {
+			this.fire('click', e);
+		}
+	},
+
+    onRemove: function (map) {
+        map.off('viewreset', this._projectLatlngs, this);
+        map.off(this._updateTrigger, this._updatePath, this);
+        map.fire(this._updateTrigger);
+    }
+});
+
+L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : {
+	_initPathRoot: function () {
+		var root = this._pathRoot,
+			ctx;
+
+		if (!root) {
+			root = this._pathRoot = document.createElement("canvas");
+			root.style.position = 'absolute';
+			ctx = this._canvasCtx = root.getContext('2d');
+
+			ctx.lineCap = "round";
+			ctx.lineJoin = "round";
+
+			this._panes.overlayPane.appendChild(root);
+
+			this.on('moveend', this._updateCanvasViewport);
+			this._updateCanvasViewport();
+		}
+	},
+
+	_updateCanvasViewport: function () {
+		this._updatePathViewport();
+
+		var vp = this._pathViewport,
+			min = vp.min,
+			size = vp.max.subtract(min),
+			root = this._pathRoot;
+
+		//TODO check if it's works properly on mobile webkit
+		L.DomUtil.setPosition(root, min);
+		root.width = size.x;
+		root.height = size.y;
+		root.getContext('2d').translate(-min.x, -min.y);
+	}
+});
diff --git a/src/layer/vector/canvas/Polygon.Canvas.js b/src/layer/vector/canvas/Polygon.Canvas.js
new file mode 100644
index 0000000..de9b8ff
--- /dev/null
+++ b/src/layer/vector/canvas/Polygon.Canvas.js
@@ -0,0 +1,34 @@
+
+L.Polygon.include(!L.Path.CANVAS ? {} : {
+	_containsPoint: function (p) {
+		var inside = false,
+			part, p1, p2,
+			i, j, k,
+			len, len2;
+
+		// TODO optimization: check if within bounds first
+
+		if (L.Polyline.prototype._containsPoint.call(this, p, true)) {
+			// click on polygon border
+			return true;
+		}
+
+		// ray casting algorithm for detecting if point is in polygon
+
+		for (i = 0, len = this._parts.length; i < len; i++) {
+			part = this._parts[i];
+
+			for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
+				p1 = part[j];
+				p2 = part[k];
+
+				if (((p1.y > p.y) !== (p2.y > p.y)) &&
+						(p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
+					inside = !inside;
+				}
+			}
+		}
+
+		return inside;
+	}
+});
diff --git a/src/layer/vector/canvas/Polyline.Canvas.js b/src/layer/vector/canvas/Polyline.Canvas.js
new file mode 100644
index 0000000..e0a44ca
--- /dev/null
+++ b/src/layer/vector/canvas/Polyline.Canvas.js
@@ -0,0 +1,27 @@
+
+L.Polyline.include(!L.Path.CANVAS ? {} : {
+	_containsPoint: function (p, closed) {
+		var i, j, k, len, len2, dist, part,
+			w = this.options.weight / 2;
+
+		if (L.Browser.touch) {
+			w += 10; // polyline click tolerance on touch devices
+		}
+
+		for (i = 0, len = this._parts.length; i < len; i++) {
+			part = this._parts[i];
+			for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
+				if (!closed && (j === 0)) {
+					continue;
+				}
+
+				dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]);
+
+				if (dist <= w) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+});
diff --git a/src/map/Map.js b/src/map/Map.js
index d460048..3b0f25e 100644
--- a/src/map/Map.js
+++ b/src/map/Map.js
@@ -4,56 +4,72 @@
 
 L.Map = L.Class.extend({
 	includes: L.Mixin.Events,
-	
+
 	options: {
 		// projection
 		crs: L.CRS.EPSG3857 || L.CRS.EPSG4326,
-		scale: function(zoom) { return 256 * (1 << zoom); },
-		
+		scale: function (zoom) {
+			return 256 * Math.pow(2, zoom);
+		},
+
 		// state
 		center: null,
 		zoom: null,
 		layers: [],
-		
+
 		// interaction
 		dragging: true,
-		touchZoom: L.Browser.mobileWebkit && !L.Browser.android,
-		scrollWheelZoom: !L.Browser.mobileWebkit,
+		touchZoom: L.Browser.touch && !L.Browser.android,
+		scrollWheelZoom: !L.Browser.touch,
 		doubleClickZoom: true,
-		shiftDragZoom: true,
-		
+		boxZoom: true,
+
 		// controls
 		zoomControl: true,
 		attributionControl: true,
-		
+
 		// animation
 		fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android,
 		zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android && !L.Browser.mobileOpera,
-		
+
 		// misc
 		trackResize: true,
-		closePopupOnClick: true
+		closePopupOnClick: true,
+		worldCopyJump: true
 	},
-	
-	
+
+
 	// constructor
-	
-	initialize: function(/*HTMLElement or String*/ id, /*Object*/ options) {
+
+	initialize: function (id, options) { // (HTMLElement or String, Object)
 		L.Util.setOptions(this, options);
-		
+
 		this._container = L.DomUtil.get(id);
-		
+
+		if (this._container._leaflet) {
+			throw new Error("Map container is already initialized.");
+		}
+		this._container._leaflet = true;
+
 		this._initLayout();
-		
-		if (L.DomEvent) { 
-			this._initEvents(); 
-			if (L.Handler) { this._initInteraction(); }
-			if (L.Control) { this._initControls(); }
+
+		if (L.DomEvent) {
+			this._initEvents();
+			if (L.Handler) {
+				this._initInteraction();
+			}
+			if (L.Control) {
+				this._initControls();
+			}
 		}
-		
+
+		if (this.options.maxBounds) {
+			this.setMaxBounds(this.options.maxBounds);
+		}
+
 		var center = this.options.center,
 			zoom = this.options.zoom;
-		
+
 		if (center !== null && zoom !== null) {
 			this.setView(center, zoom, true);
 		}
@@ -63,63 +79,112 @@ L.Map = L.Class.extend({
 		this._tileLayersNum = 0;
 		this._initLayers(layers);
 	},
-	
-	
+
+
 	// public methods that modify map state
-	
+
 	// replaced by animation-powered implementation in Map.PanAnimation.js
-	setView: function(center, zoom, forceReset) {
-		// reset the map view 
+	setView: function (center, zoom) {
+		// reset the map view
 		this._resetView(center, this._limitZoom(zoom));
 		return this;
 	},
-	
-	setZoom: function(/*Number*/ zoom) {
+
+	setZoom: function (zoom) { // (Number)
 		return this.setView(this.getCenter(), zoom);
 	},
-	
-	zoomIn: function() {
+
+	zoomIn: function () {
 		return this.setZoom(this._zoom + 1);
 	},
-	
-	zoomOut: function() {
+
+	zoomOut: function () {
 		return this.setZoom(this._zoom - 1);
 	},
-	
-	fitBounds: function(/*LatLngBounds*/ bounds) {
+
+	fitBounds: function (bounds) { // (LatLngBounds)
 		var zoom = this.getBoundsZoom(bounds);
 		return this.setView(bounds.getCenter(), zoom);
 	},
-	
-	fitWorld: function() {
+
+	fitWorld: function () {
 		var sw = new L.LatLng(-60, -170),
 			ne = new L.LatLng(85, 179);
 		return this.fitBounds(new L.LatLngBounds(sw, ne));
 	},
-	
-	panTo: function(/*LatLng*/ center) {
+
+	panTo: function (center) { // (LatLng)
 		return this.setView(center, this._zoom);
 	},
-	
-	panBy: function(/*Point*/ offset) {
+
+	panBy: function (offset) { // (Point)
 		// replaced with animated panBy in Map.Animation.js
 		this.fire('movestart');
-		
+
 		this._rawPanBy(offset);
-		
+
 		this.fire('move');
 		this.fire('moveend');
-		
+
 		return this;
 	},
-	
-	addLayer: function(layer) {
+
+	setMaxBounds: function (bounds) {
+		this.options.maxBounds = bounds;
+
+		if (!bounds) {
+			this._boundsMinZoom = null;
+			return this;
+		}
+
+		var minZoom = this.getBoundsZoom(bounds, true);
+
+		this._boundsMinZoom = minZoom;
+
+		if (this._loaded) {
+			if (this._zoom < minZoom) {
+				this.setView(bounds.getCenter(), minZoom);
+			} else {
+				this.panInsideBounds(bounds);
+			}
+		}
+		return this;
+	},
+
+	panInsideBounds: function (bounds) {
+		var viewBounds = this.getBounds(),
+			viewSw = this.project(viewBounds.getSouthWest()),
+			viewNe = this.project(viewBounds.getNorthEast()),
+			sw = this.project(bounds.getSouthWest()),
+			ne = this.project(bounds.getNorthEast()),
+			dx = 0,
+			dy = 0;
+
+		if (viewNe.y < ne.y) { // north
+			dy = ne.y - viewNe.y;
+		}
+		if (viewNe.x > ne.x) { // east
+			dx = ne.x - viewNe.x;
+		}
+		if (viewSw.y > sw.y) { // south
+			dy = sw.y - viewSw.y;
+		}
+		if (viewSw.x < sw.x) { // west
+			dx = sw.x - viewSw.x;
+		}
+
+		return this.panBy(new L.Point(dx, dy, true));
+	},
+
+	addLayer: function (layer, insertAtTheTop) {
 		var id = L.Util.stamp(layer);
-		
-		if (this._layers[id]) return this;
-		
+
+		if (this._layers[id]) {
+			return this;
+		}
+
 		this._layers[id] = layer;
-		
+
 		if (layer.options && !isNaN(layer.options.maxZoom)) {
 			this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);
 		}
@@ -127,7 +192,7 @@ L.Map = L.Class.extend({
 			this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);
 		}
 		//TODO getMaxZoom, getMinZoom in ILayer (instead of options)
-		
+
 		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
 			this._tileLayersNum++;
 			layer.on('load', this._onTileLayerLoad, this);
@@ -135,28 +200,28 @@ L.Map = L.Class.extend({
 		if (this.attributionControl && layer.getAttribution) {
 			this.attributionControl.addAttribution(layer.getAttribution());
 		}
-		
-		var onMapLoad = function() {
-			layer.onAdd(this);
+
+		var onMapLoad = function () {
+			layer.onAdd(this, insertAtTheTop);
 			this.fire('layeradd', {layer: layer});
 		};
-		
+
 		if (this._loaded) {
 			onMapLoad.call(this);
 		} else {
 			this.on('load', onMapLoad, this);
 		}
-		
+
 		return this;
 	},
-	
-	removeLayer: function(layer) {
+
+	removeLayer: function (layer) {
 		var id = L.Util.stamp(layer);
-		
+
 		if (this._layers[id]) {
 			layer.onRemove(this);
 			delete this._layers[id];
-			
+
 			if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
 				this._tileLayersNum--;
 				layer.off('load', this._onTileLayerLoad, this);
@@ -164,73 +229,113 @@ L.Map = L.Class.extend({
 			if (this.attributionControl && layer.getAttribution) {
 				this.attributionControl.removeAttribution(layer.getAttribution());
 			}
-			
+
 			this.fire('layerremove', {layer: layer});
 		}
 		return this;
 	},
-	
-	invalidateSize: function() {
+
+	hasLayer: function (layer) {
+		var id = L.Util.stamp(layer);
+		return this._layers.hasOwnProperty(id);
+	},
+
+	invalidateSize: function () {
+		var oldSize = this.getSize();
+
 		this._sizeChanged = true;
-		
+
+		if (this.options.maxBounds) {
+			this.setMaxBounds(this.options.maxBounds);
+		}
+
+		if (!this._loaded) {
+			return this;
+		}
+
+		this._rawPanBy(oldSize.subtract(this.getSize()).divideBy(2, true));
+
 		this.fire('move');
-		
+
 		clearTimeout(this._sizeTimer);
-		this._sizeTimer = setTimeout(L.Util.bind(function() {
+		this._sizeTimer = setTimeout(L.Util.bind(function () {
 			this.fire('moveend');
-		}, this), 200); 
+		}, this), 200);
 
 		return this;
 	},
-	
-	
-	// public methods for getting map state	
-	
-	getCenter: function(/*Boolean*/ unbounded) {
+
+
+	// public methods for getting map state
+
+	getCenter: function (unbounded) { // (Boolean)
 		var viewHalf = this.getSize().divideBy(2),
 			centerPoint = this._getTopLeftPoint().add(viewHalf);
 		return this.unproject(centerPoint, this._zoom, unbounded);
 	},
-	
-	getZoom: function() {
+
+	getZoom: function () {
 		return this._zoom;
 	},
-	
-	getBounds: function() {
+
+	getBounds: function () {
 		var bounds = this.getPixelBounds(),
-			sw = this.unproject(new L.Point(bounds.min.x, bounds.max.y)),
-			ne = this.unproject(new L.Point(bounds.max.x, bounds.min.y));
+			sw = this.unproject(new L.Point(bounds.min.x, bounds.max.y), this._zoom, true),
+			ne = this.unproject(new L.Point(bounds.max.x, bounds.min.y), this._zoom, true);
 		return new L.LatLngBounds(sw, ne);
 	},
-	
-	getMinZoom: function() {
-		return isNaN(this.options.minZoom) ?  this._layersMinZoom || 0 : this.options.minZoom;
+
+	getMinZoom: function () {
+		var z1 = this.options.minZoom || 0,
+			z2 = this._layersMinZoom || 0,
+			z3 = this._boundsMinZoom || 0;
+
+		return Math.max(z1, z2, z3);
 	},
-	
-	getMaxZoom: function() {
-		return isNaN(this.options.maxZoom) ?  this._layersMaxZoom || Infinity : this.options.maxZoom;
+
+	getMaxZoom: function () {
+		var z1 = isNaN(this.options.maxZoom) ? Infinity : this.options.maxZoom,
+			z2 = this._layersMaxZoom || Infinity;
+
+		return Math.min(z1, z2);
 	},
-	
-	getBoundsZoom: function(/*LatLngBounds*/ bounds) {
+
+	getBoundsZoom: function (bounds, inside) { // (LatLngBounds)
 		var size = this.getSize(),
-			zoom = this.getMinZoom(),
+			zoom = this.options.minZoom || 0,
 			maxZoom = this.getMaxZoom(),
 			ne = bounds.getNorthEast(),
 			sw = bounds.getSouthWest(),
-			boundsSize, 
-			nePoint, swPoint;
+			boundsSize,
+			nePoint,
+			swPoint,
+			zoomNotFound = true;
+
+		if (inside) {
+			zoom--;
+		}
+
 		do {
 			zoom++;
 			nePoint = this.project(ne, zoom);
 			swPoint = this.project(sw, zoom);
 			boundsSize = new L.Point(nePoint.x - swPoint.x, swPoint.y - nePoint.y);
-		} while ((boundsSize.x <= size.x) && 
-				 (boundsSize.y <= size.y) && (zoom <= maxZoom));
-		
-		return zoom - 1;
+
+			if (!inside) {
+				zoomNotFound = (boundsSize.x <= size.x) && (boundsSize.y <= size.y);
+			} else {
+				zoomNotFound = (boundsSize.x < size.x) || (boundsSize.y < size.y);
+			}
+		} while (zoomNotFound && (zoom <= maxZoom));
+
+		if (zoomNotFound && inside) {
+			return null;
+		}
+
+		return inside ? zoom : zoom - 1;
 	},
-	
-	getSize: function() {
+
+	getSize: function () {
 		if (!this._size || this._sizeChanged) {
 			this._size = new L.Point(this._container.clientWidth, this._container.clientHeight);
 			this._sizeChanged = false;
@@ -238,138 +343,153 @@ L.Map = L.Class.extend({
 		return this._size;
 	},
 
-	getPixelBounds: function() {
+	getPixelBounds: function () {
 		var topLeftPoint = this._getTopLeftPoint(),
 			size = this.getSize();
 		return new L.Bounds(topLeftPoint, topLeftPoint.add(size));
 	},
-	
-	getPixelOrigin: function() {
+
+	getPixelOrigin: function () {
 		return this._initialTopLeftPoint;
 	},
-	
-	getPanes: function() {
+
+	getPanes: function () {
 		return this._panes;
 	},
-	
-	
+
+
 	// conversion methods
-	
-	mouseEventToContainerPoint: function(/*MouseEvent*/ e) {
+
+	mouseEventToContainerPoint: function (e) { // (MouseEvent)
 		return L.DomEvent.getMousePosition(e, this._container);
 	},
-	
-	mouseEventToLayerPoint: function(/*MouseEvent*/ e) {
+
+	mouseEventToLayerPoint: function (e) { // (MouseEvent)
 		return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
 	},
-	
-	mouseEventToLatLng: function(/*MouseEvent*/ e) {
+
+	mouseEventToLatLng: function (e) { // (MouseEvent)
 		return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
 	},
-	
-	containerPointToLayerPoint: function(/*Point*/ point) {
+
+	containerPointToLayerPoint: function (point) { // (Point)
 		return point.subtract(L.DomUtil.getPosition(this._mapPane));
 	},
-	
-	layerPointToContainerPoint: function(/*Point*/ point) {
+
+	layerPointToContainerPoint: function (point) { // (Point)
 		return point.add(L.DomUtil.getPosition(this._mapPane));
 	},
-	
-	layerPointToLatLng: function(/*Point*/ point) {
+
+	layerPointToLatLng: function (point) { // (Point)
 		return this.unproject(point.add(this._initialTopLeftPoint));
 	},
-	
-	latLngToLayerPoint: function(/*LatLng*/ latlng) {
-		return this.project(latlng)._subtract(this._initialTopLeftPoint);
+
+	latLngToLayerPoint: function (latlng) { // (LatLng)
+		return this.project(latlng)._round()._subtract(this._initialTopLeftPoint);
 	},
 
-	project: function(/*LatLng*/ latlng, /*(optional) Number*/ zoom)/*-> Point*/ {
-		zoom = (typeof zoom == 'undefined' ? this._zoom : zoom);
+	project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
+		zoom = (typeof zoom === 'undefined' ? this._zoom : zoom);
 		return this.options.crs.latLngToPoint(latlng, this.options.scale(zoom));
 	},
-	
-	unproject: function(/*Point*/ point, /*(optional) Number*/ zoom, /*(optional) Boolean*/ unbounded)/*-> Object*/ {
-		zoom = (typeof zoom == 'undefined' ? this._zoom : zoom);
+
+	unproject: function (point, zoom, unbounded) { // (Point[, Number, Boolean]) -> LatLng
+		zoom = (typeof zoom === 'undefined' ? this._zoom : zoom);
 		return this.options.crs.pointToLatLng(point, this.options.scale(zoom), unbounded);
 	},
-	
-	
+
+
 	// private methods that modify map state
-	
-	_initLayout: function() {
+
+	_initLayout: function () {
 		var container = this._container;
-		
+
+		container.innerHTML = '';
+
 		container.className += ' leaflet-container';
-		
+
 		if (this.options.fadeAnimation) {
 			container.className += ' leaflet-fade-anim';
 		}
-		
+
 		var position = L.DomUtil.getStyle(container, 'position');
-		if (position != 'absolute' && position != 'relative') {
+		if (position !== 'absolute' && position !== 'relative') {
 			container.style.position = 'relative';
 		}
-		
+
 		this._initPanes();
-		
-		if (this._initControlPos) this._initControlPos();
+
+		if (this._initControlPos) {
+			this._initControlPos();
+		}
 	},
-	
-	_initPanes: function() {
+
+	_initPanes: function () {
 		var panes = this._panes = {};
-		
+
 		this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);
-		
+
 		this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);
 		this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);
-		
+
 		panes.shadowPane = this._createPane('leaflet-shadow-pane');
 		panes.overlayPane = this._createPane('leaflet-overlay-pane');
 		panes.markerPane = this._createPane('leaflet-marker-pane');
 		panes.popupPane = this._createPane('leaflet-popup-pane');
 	},
-	
-	_createPane: function(className, container) {
+
+	_createPane: function (className, container) {
 		return L.DomUtil.create('div', className, container || this._objectsPane);
 	},
-	
-	_resetView: function(center, zoom, preserveMapOffset) {
-		var zoomChanged = (this._zoom != zoom);
-		
-		this.fire('movestart');
-		
+
+	_resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {
+		var zoomChanged = (this._zoom !== zoom);
+
+		if (!afterZoomAnim) {
+			this.fire('movestart');
+
+			if (zoomChanged) {
+				this.fire('zoomstart');
+			}
+		}
+
 		this._zoom = zoom;
-		
+
 		this._initialTopLeftPoint = this._getNewTopLeftPoint(center);
-		
+
 		if (!preserveMapOffset) {
 			L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
 		} else {
 			var offset = L.DomUtil.getPosition(this._mapPane);
 			this._initialTopLeftPoint._add(offset);
 		}
-		
+
 		this._tileLayersToLoad = this._tileLayersNum;
-		this.fire('viewreset');
+		this.fire('viewreset', {hard: !preserveMapOffset});
 
 		this.fire('move');
-		if (zoomChanged) { this.fire('zoomend'); }
+		if (zoomChanged || afterZoomAnim) {
+			this.fire('zoomend');
+		}
 		this.fire('moveend');
-		
+
 		if (!this._loaded) {
 			this._loaded = true;
 			this.fire('load');
 		}
 	},
-	
-	_initLayers: function(layers) {
+
+	_initLayers: function (layers) {
 		this._layers = {};
-		for (var i = 0, len = layers.length; i < len; i++) {
+
+		var i, len;
+
+		for (i = 0, len = layers.length; i < len; i++) {
 			this.addLayer(layers[i]);
 		}
 	},
-	
-	_initControls: function() {
+
+	_initControls: function () {
 		if (this.options.zoomControl) {
 			this.addControl(new L.Control.Zoom());
 		}
@@ -379,61 +499,87 @@ L.Map = L.Class.extend({
 		}
 	},
 
-	_rawPanBy: function(offset) {
+	_rawPanBy: function (offset) {
 		var mapPaneOffset = L.DomUtil.getPosition(this._mapPane);
 		L.DomUtil.setPosition(this._mapPane, mapPaneOffset.subtract(offset));
 	},
-	
-	
+
+
 	// map events
-	
-	_initEvents: function() {
+
+	_initEvents: function () {
 		L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
-		
-		var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove'];
-		for (var i = 0; i < events.length; i++) {
+
+		var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu'];
+
+		var i, len;
+
+		for (i = 0, len = events.length; i < len; i++) {
 			L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
 		}
-		
+
 		if (this.options.trackResize) {
-			L.DomEvent.addListener(window, 'resize', this.invalidateSize, this);
+			L.DomEvent.addListener(window, 'resize', this._onResize, this);
 		}
 	},
-	
-	_onMouseClick: function(e) {
-		if (this.dragging && this.dragging.moved()) { return; }
-		
+
+	_onResize: function () {
+		L.Util.requestAnimFrame(this.invalidateSize, this, false, this._container);
+	},
+
+	_onMouseClick: function (e) {
+		if (!this._loaded || (this.dragging && this.dragging.moved())) {
+			return;
+		}
+
 		this.fire('pre' + e.type);
 		this._fireMouseEvent(e);
 	},
-	
-	_fireMouseEvent: function(e) {
+
+	_fireMouseEvent: function (e) {
+		if (!this._loaded) {
+			return;
+		}
+
 		var type = e.type;
-		type = (type == 'mouseenter' ? 'mouseover' : (type == 'mouseleave' ? 'mouseout' : type));
-		if (!this.hasEventListeners(type)) { return; }
+		type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
+
+		if (!this.hasEventListeners(type)) {
+			return;
+		}
+
+		if (type === 'contextmenu') {
+			L.DomEvent.preventDefault(e);
+		}
+		
 		this.fire(type, {
 			latlng: this.mouseEventToLatLng(e),
 			layerPoint: this.mouseEventToLayerPoint(e)
 		});
 	},
-	
-	_initInteraction: function() {
+
+	_initInteraction: function () {
 		var handlers = {
-			dragging: L.Handler.MapDrag,
-			touchZoom: L.Handler.TouchZoom,
-			doubleClickZoom: L.Handler.DoubleClickZoom,
-			scrollWheelZoom: L.Handler.ScrollWheelZoom,
-			shiftDragZoom: L.Handler.ShiftDragZoom
+			dragging: L.Map.Drag,
+			touchZoom: L.Map.TouchZoom,
+			doubleClickZoom: L.Map.DoubleClickZoom,
+			scrollWheelZoom: L.Map.ScrollWheelZoom,
+			boxZoom: L.Map.BoxZoom
 		};
-		for (var i in handlers) {
+
+		var i;
+		for (i in handlers) {
 			if (handlers.hasOwnProperty(i) && handlers[i]) {
 				this[i] = new handlers[i](this);
-				if (this.options[i]) this[i].enable();
+				if (this.options[i]) {
+					this[i].enable();
+				}
+				// TODO move enabling to handler contructor
 			}
 		}
 	},
-	
-	_onTileLayerLoad: function() {
+
+	_onTileLayerLoad: function () {
 		// clear scaled tiles after all new tiles are loaded (for performance)
 		this._tileLayersToLoad--;
 		if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {
@@ -441,24 +587,27 @@ L.Map = L.Class.extend({
 			this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500);
 		}
 	},
-	
+
 
 	// private methods for getting map state
-	
-	_getTopLeftPoint: function() {
-		if (!this._loaded) throw new Error('Set map center and zoom first.');
+
+	_getTopLeftPoint: function () {
+		if (!this._loaded) {
+			throw new Error('Set map center and zoom first.');
+		}
+
 		var offset = L.DomUtil.getPosition(this._mapPane);
 		return this._initialTopLeftPoint.subtract(offset);
 	},
-	
-	_getNewTopLeftPoint: function(center) {
+
+	_getNewTopLeftPoint: function (center) {
 		var viewHalf = this.getSize().divideBy(2);
 		return this.project(center).subtract(viewHalf).round();
 	},
-	
-	_limitZoom: function(zoom) {
+
+	_limitZoom: function (zoom) {
 		var min = this.getMinZoom();
 		var max = this.getMaxZoom();
 		return Math.max(min, Math.min(max, zoom));
 	}
-});
+});
diff --git a/src/map/ext/Map.PanAnimation.js b/src/map/anim/Map.PanAnimation.js
similarity index 64%
rename from src/map/ext/Map.PanAnimation.js
rename to src/map/anim/Map.PanAnimation.js
index 02ccfd1..56467b3 100644
--- a/src/map/ext/Map.PanAnimation.js
+++ b/src/map/anim/Map.PanAnimation.js
@@ -1,62 +1,70 @@
-L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
-	setView: function(center, zoom, forceReset) {
-		zoom = this._limitZoom(zoom);
-		var zoomChanged = (this._zoom != zoom);
-
-		if (this._loaded && !forceReset && this._layers) {
-			// difference between the new and current centers in pixels
-			var offset = this._getNewTopLeftPoint(center).subtract(this._getTopLeftPoint()); 
-			
-			var done = (zoomChanged ? 
-						!!this._zoomToIfCenterInView && this._zoomToIfCenterInView(center, zoom, offset) : 
-						this._panByIfClose(offset));
-			
-			// exit if animated pan or zoom started
-			if (done) { return this; }
-		}
-		
-		// reset the map view 
-		this._resetView(center, zoom);
-		
-		return this;
-	},
-	
-	panBy: function(offset) {
-		if (!this._panTransition) {
-			this._panTransition = new L.Transition(this._mapPane, {duration: 0.3});
-			
-			this._panTransition.on('step', this._onPanTransitionStep, this);
-			this._panTransition.on('end', this._onPanTransitionEnd, this);
-		}
-		this.fire(this, 'movestart');
-		
-		this._panTransition.run({
-			position: L.DomUtil.getPosition(this._mapPane).subtract(offset)
-		});
-		
-		return this;
-	},
-	
-	_onPanTransitionStep: function() {
-		this.fire('move');
-	},
-	
-	_onPanTransitionEnd: function() {
-		this.fire('moveend');
-	},
-
-	_panByIfClose: function(offset) {
-		if (this._offsetIsWithinView(offset)) {
-			this.panBy(offset);
-			return true;
-		}
-		return false;
-	},
-
-	_offsetIsWithinView: function(offset, multiplyFactor) {
-		var m = multiplyFactor || 1,
-			size = this.getSize();
-		return (Math.abs(offset.x) <= size.x * m) && 
-				(Math.abs(offset.y) <= size.y * m);
-	}
-});
\ No newline at end of file
+L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
+	setView: function (center, zoom, forceReset) {
+		zoom = this._limitZoom(zoom);
+		var zoomChanged = (this._zoom !== zoom);
+
+		if (this._loaded && !forceReset && this._layers) {
+			// difference between the new and current centers in pixels
+			var offset = this._getNewTopLeftPoint(center).subtract(this._getTopLeftPoint());
+
+			center = new L.LatLng(center.lat, center.lng);
+
+			var done = (zoomChanged ?
+						!!this._zoomToIfCenterInView && this._zoomToIfCenterInView(center, zoom, offset) :
+						this._panByIfClose(offset));
+
+			// exit if animated pan or zoom started
+			if (done) {
+				return this;
+			}
+		}
+
+		// reset the map view
+		this._resetView(center, zoom);
+
+		return this;
+	},
+
+	panBy: function (offset) {
+		if (!(offset.x || offset.y)) {
+			return this;
+		}
+
+		if (!this._panTransition) {
+			this._panTransition = new L.Transition(this._mapPane, {duration: 0.3});
+
+			this._panTransition.on('step', this._onPanTransitionStep, this);
+			this._panTransition.on('end', this._onPanTransitionEnd, this);
+		}
+		this.fire('movestart');
+
+		this._panTransition.run({
+			position: L.DomUtil.getPosition(this._mapPane).subtract(offset)
+		});
+
+		return this;
+	},
+
+	_onPanTransitionStep: function () {
+		this.fire('move');
+	},
+
+	_onPanTransitionEnd: function () {
+		this.fire('moveend');
+	},
+
+	_panByIfClose: function (offset) {
+		if (this._offsetIsWithinView(offset)) {
+			this.panBy(offset);
+			return true;
+		}
+		return false;
+	},
+
+	_offsetIsWithinView: function (offset, multiplyFactor) {
+		var m = multiplyFactor || 1,
+			size = this.getSize();
+		return (Math.abs(offset.x) <= size.x * m) &&
+				(Math.abs(offset.y) <= size.y * m);
+	}
+});
diff --git a/src/map/ext/Map.ZoomAnimation.js b/src/map/anim/Map.ZoomAnimation.js
similarity index 77%
rename from src/map/ext/Map.ZoomAnimation.js
rename to src/map/anim/Map.ZoomAnimation.js
index 4bf7b9b..bdaa8bf 100644
--- a/src/map/ext/Map.ZoomAnimation.js
+++ b/src/map/anim/Map.ZoomAnimation.js
@@ -1,124 +1,138 @@
-L.Map.include(!L.DomUtil.TRANSITION ? {} : {
-	_zoomToIfCenterInView: function(center, zoom, centerOffset) {
-		
-		if (this._animatingZoom) { return true; }
-		if (!this.options.zoomAnimation) { return false; }
-		
-		var zoomDelta = zoom - this._zoom,
-			scale = Math.pow(2, zoomDelta),
-			offset = centerOffset.divideBy(1 - 1/scale);
-		
-		//if offset does not exceed half of the view
-		if (!this._offsetIsWithinView(offset, 1)) { return false; }
-		
-		this._mapPane.className += ' leaflet-zoom-anim';
-
-		var centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
-			origin = centerPoint.add(offset);
-		
-		this._prepareTileBg();
-	
-		this._runAnimation(center, zoom, scale, origin);
-		
-		return true;
-	},
-	
-	
-	_runAnimation: function(center, zoom, scale, origin) {
-		this._animatingZoom = true;
-
-		this._animateToCenter = center;
-		this._animateToZoom = zoom;
-		
-		var transform = L.DomUtil.TRANSFORM;
-		
-		//dumb FireFox hack, I have no idea why this magic zero translate fixes the scale transition problem
-		if (L.Browser.gecko || window.opera) {
-			this._tileBg.style[transform] += ' translate(0,0)';
-		}
-		
-		var scaleStr;
-		
-		// Android doesn't like translate/scale chains, transformOrigin + scale works better but 
-		// it breaks touch zoom which Anroid doesn't support anyway, so that's a really ugly hack
-		// TODO work around this prettier
-		if (L.Browser.android) {
-			this._tileBg.style[transform + 'Origin'] = origin.x + 'px ' + origin.y + 'px';
-			scaleStr = 'scale(' + scale + ')';
-		} else {
-			scaleStr = L.DomUtil.getScaleString(scale, origin);
-		}
-		
-		L.Util.falseFn(this._tileBg.offsetWidth); //hack to make sure transform is updated before running animation
-		
-		var options = {};
-		options[transform] = this._tileBg.style[transform] + ' ' + scaleStr;
-		this._tileBg.transition.run(options);
-	},
-	
-	_prepareTileBg: function() {
-		if (!this._tileBg) {
-			this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
-			this._tileBg.style.zIndex = 1;
-		}
-
-		var tilePane = this._tilePane,
-			tileBg = this._tileBg;
-		
-		// prepare the background pane to become the main tile pane
-		//tileBg.innerHTML = '';
-		tileBg.style[L.DomUtil.TRANSFORM] = '';
-		tileBg.style.visibility = 'hidden';
-		
-		// tells tile layers to reinitialize their containers
-		tileBg.empty = true;
-		tilePane.empty = false;
-
-		this._tilePane = this._panes.tilePane = tileBg;
-		this._tileBg = tilePane;
-		
-		if (!this._tileBg.transition) {
-			this._tileBg.transition = new L.Transition(this._tileBg, {duration: 0.3, easing: 'cubic-bezier(0.25,0.1,0.25,0.75)'});
-			this._tileBg.transition.on('end', this._onZoomTransitionEnd, this);
-		}
-		
-		this._stopLoadingBgTiles();
-	},
-	
-	// stops loading all tiles in the background layer
-	_stopLoadingBgTiles: function() {
-		var tiles = [].slice.call(this._tileBg.getElementsByTagName('img'));
-		
-		for (var i = 0, len = tiles.length; i < len; i++) {
-			if (!tiles[i].complete) {
-				tiles[i].src = '';
-				tiles[i].parentNode.removeChild(tiles[i]);
-			}
-		}
-	},
-	
-	_onZoomTransitionEnd: function() {
-		this._restoreTileFront();
-		
-		L.Util.falseFn(this._tileBg.offsetWidth);
-		this._resetView(this._animateToCenter, this._animateToZoom, true);
-		
-		//TODO clear tileBg on map layersload
-		
-		this._mapPane.className = this._mapPane.className.replace(' leaflet-zoom-anim', ''); //TODO toggleClass util
-		this._animatingZoom = false;
-	},
-	
-	_restoreTileFront: function() {
-		this._tilePane.innerHTML = '';
-		this._tilePane.style.visibility = '';
-		this._tilePane.style.zIndex = 2;
-		this._tileBg.style.zIndex = 1;
-	},
-	
-	_clearTileBg: function() {
-		if (!this._animatingZoom && !this.touchZoom._zooming) {
-			this._tileBg.innerHTML = '';
-		}
-	}
-});
\ No newline at end of file
+L.Map.include(!L.DomUtil.TRANSITION ? {} : {
+	_zoomToIfCenterInView: function (center, zoom, centerOffset) {
+
+		if (this._animatingZoom) {
+			return true;
+		}
+		if (!this.options.zoomAnimation) {
+			return false;
+		}
+
+		var zoomDelta = zoom - this._zoom,
+			scale = Math.pow(2, zoomDelta),
+			offset = centerOffset.divideBy(1 - 1 / scale);
+
+		//if offset does not exceed half of the view
+		if (!this._offsetIsWithinView(offset, 1)) {
+			return false;
+		}
+
+		this._mapPane.className += ' leaflet-zoom-anim';
+
+        this
+			.fire('movestart')
+			.fire('zoomstart');
+
+		var centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
+			origin = centerPoint.add(offset);
+
+		this._prepareTileBg();
+
+		this._runAnimation(center, zoom, scale, origin);
+
+		return true;
+	},
+
+
+	_runAnimation: function (center, zoom, scale, origin) {
+		this._animatingZoom = true;
+
+		this._animateToCenter = center;
+		this._animateToZoom = zoom;
+
+		var transform = L.DomUtil.TRANSFORM;
+
+		clearTimeout(this._clearTileBgTimer);
+
+		//dumb FireFox hack, I have no idea why this magic zero translate fixes the scale transition problem
+		if (L.Browser.gecko || window.opera) {
+			this._tileBg.style[transform] += ' translate(0,0)';
+		}
+
+		var scaleStr;
+
+		// Android doesn't like translate/scale chains, transformOrigin + scale works better but
+		// it breaks touch zoom which Anroid doesn't support anyway, so that's a really ugly hack
+		// TODO work around this prettier
+		if (L.Browser.android) {
+			this._tileBg.style[transform + 'Origin'] = origin.x + 'px ' + origin.y + 'px';
+			scaleStr = 'scale(' + scale + ')';
+		} else {
+			scaleStr = L.DomUtil.getScaleString(scale, origin);
+		}
+
+		L.Util.falseFn(this._tileBg.offsetWidth); //hack to make sure transform is updated before running animation
+
+		var options = {};
+		options[transform] = this._tileBg.style[transform] + ' ' + scaleStr;
+		this._tileBg.transition.run(options);
+	},
+
+	_prepareTileBg: function () {
+		if (!this._tileBg) {
+			this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
+			this._tileBg.style.zIndex = 1;
+		}
+
+		var tilePane = this._tilePane,
+			tileBg = this._tileBg;
+
+		// prepare the background pane to become the main tile pane
+		//tileBg.innerHTML = '';
+		tileBg.style[L.DomUtil.TRANSFORM] = '';
+		tileBg.style.visibility = 'hidden';
+
+		// tells tile layers to reinitialize their containers
+		tileBg.empty = true;
+		tilePane.empty = false;
+
+		this._tilePane = this._panes.tilePane = tileBg;
+		this._tileBg = tilePane;
+
+		if (!this._tileBg.transition) {
+			this._tileBg.transition = new L.Transition(this._tileBg, {duration: 0.3, easing: 'cubic-bezier(0.25,0.1,0.25,0.75)'});
+			this._tileBg.transition.on('end', this._onZoomTransitionEnd, this);
+		}
+
+		this._stopLoadingBgTiles();
+	},
+
+	// stops loading all tiles in the background layer
+	_stopLoadingBgTiles: function () {
+		var tiles = [].slice.call(this._tileBg.getElementsByTagName('img'));
+
+		for (var i = 0, len = tiles.length; i < len; i++) {
+			if (!tiles[i].complete) {
+				tiles[i].onload = L.Util.falseFn;
+				tiles[i].onerror = L.Util.falseFn;
+				tiles[i].src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
+
+				tiles[i].parentNode.removeChild(tiles[i]);
+				tiles[i] = null;
+			}
+		}
+	},
+
+	_onZoomTransitionEnd: function () {
+		this._restoreTileFront();
+
+		L.Util.falseFn(this._tileBg.offsetWidth);
+		this._resetView(this._animateToCenter, this._animateToZoom, true, true);
+
+		this._mapPane.className = this._mapPane.className.replace(' leaflet-zoom-anim', ''); //TODO toggleClass util
+		this._animatingZoom = false;
+	},
+
+	_restoreTileFront: function () {
+		this._tilePane.innerHTML = '';
+		this._tilePane.style.visibility = '';
+		this._tilePane.style.zIndex = 2;
+		this._tileBg.style.zIndex = 1;
+	},
+
+	_clearTileBg: function () {
+		if (!this._animatingZoom && !this.touchZoom._zooming) {
+			this._tileBg.innerHTML = '';
+		}
+	}
+});
diff --git a/src/map/ext/Map.Control.js b/src/map/ext/Map.Control.js
index 46711a8..59c688e 100644
--- a/src/map/ext/Map.Control.js
+++ b/src/map/ext/Map.Control.js
@@ -1,35 +1,35 @@
 L.Map.include({
-	addControl: function(control) {
+	addControl: function (control) {
 		control.onAdd(this);
 
 		var pos = control.getPosition(),
 			corner = this._controlCorners[pos],
 			container = control.getContainer();
-		
+
 		L.DomUtil.addClass(container, 'leaflet-control');
-		
-		if (pos.indexOf('bottom') != -1) {
+
+		if (pos.indexOf('bottom') !== -1) {
 			corner.insertBefore(container, corner.firstChild);
 		} else {
 			corner.appendChild(container);
 		}
 		return this;
 	},
-	
-	removeControl: function(control) {
+
+	removeControl: function (control) {
 		var pos = control.getPosition(),
 			corner = this._controlCorners[pos],
 			container = control.getContainer();
-		
+
 		corner.removeChild(container);
-		
+
 		if (control.onRemove) {
 			control.onRemove(this);
 		}
 		return this;
 	},
-	
-	_initControlPos: function() {
+
+	_initControlPos: function () {
 		var corners = this._controlCorners = {},
 			classPart = 'leaflet-',
 			top = classPart + 'top',
@@ -37,14 +37,14 @@ L.Map.include({
 			left = classPart + 'left',
 			right = classPart + 'right',
 			controlContainer = L.DomUtil.create('div', classPart + 'control-container', this._container);
-		
-		if (L.Browser.mobileWebkit) {
+
+		if (L.Browser.touch) {
 			controlContainer.className += ' ' + classPart + 'big-buttons';
 		}
-		
+
 		corners.topLeft = L.DomUtil.create('div', top + ' ' + left, controlContainer);
 		corners.topRight = L.DomUtil.create('div', top + ' ' + right, controlContainer);
 		corners.bottomLeft = L.DomUtil.create('div', bottom + ' ' + left, controlContainer);
 		corners.bottomRight = L.DomUtil.create('div', bottom + ' ' + right, controlContainer);
 	}
-});
\ No newline at end of file
+});
diff --git a/src/map/ext/Map.Geolocation.js b/src/map/ext/Map.Geolocation.js
index 328662b..567222c 100644
--- a/src/map/ext/Map.Geolocation.js
+++ b/src/map/ext/Map.Geolocation.js
@@ -3,67 +3,84 @@
  */
 
 L.Map.include({
-	locate: function(/*Object*/ options) {
-		// W3C Geolocation API Spec position options, http://dev.w3.org/geo/api/spec-source.html#position-options
-		var opts = {timeout: 10000};
-		L.Util.extend(opts, options);
-		
-		if (navigator.geolocation) {
-			navigator.geolocation.getCurrentPosition(
-					L.Util.bind(this._handleGeolocationResponse, this),
-					L.Util.bind(this._handleGeolocationError, this),
-					opts);
-		} else {
-			this.fire('locationerror', {
+	locate: function (/*Object*/ options) {
+
+		this._locationOptions = options = L.Util.extend({
+			watch: false,
+			setView: false,
+			maxZoom: Infinity,
+			timeout: 10000,
+			maximumAge: 0,
+			enableHighAccuracy: false
+		}, options);
+
+		if (!navigator.geolocation) {
+			return this.fire('locationerror', {
 				code: 0,
 				message: "Geolocation not supported."
 			});
 		}
+
+		var onResponse = L.Util.bind(this._handleGeolocationResponse, this),
+			onError = L.Util.bind(this._handleGeolocationError, this);
+
+		if (options.watch) {
+			this._locationWatchId = navigator.geolocation.watchPosition(onResponse, onError, options);
+		} else {
+			navigator.geolocation.getCurrentPosition(onResponse, onError, options);
+		}
 		return this;
 	},
-	
-	locateAndSetView: function(maxZoom, options) {
-		this._setViewOnLocate = true;
-		this._maxLocateZoom = maxZoom || Infinity;
+
+	stopLocate: function () {
+		if (navigator.geolocation) {
+			navigator.geolocation.clearWatch(this._locationWatchId);
+		}
+	},
+
+	locateAndSetView: function (maxZoom, options) {
+		options = L.Util.extend({
+			maxZoom: maxZoom || Infinity,
+			setView: true
+		}, options);
 		return this.locate(options);
 	},
-	
-	_handleGeolocationError: function(error) {
+
+	_handleGeolocationError: function (error) {
 		var c = error.code,
-			message = (c == 1 ? "permission denied" : 
-				(c == 2 ? "position unavailable" : "timeout"));
-		
-		if (this._setViewOnLocate) {
+			message = (c === 1 ? "permission denied" :
+				(c === 2 ? "position unavailable" : "timeout"));
+
+		if (this._locationOptions.setView && !this._loaded) {
 			this.fitWorld();
-			this._setViewOnLocate = false;
 		}
-		
+
 		this.fire('locationerror', {
 			code: c,
-			message: "Geolocation error: " + message + "." 
+			message: "Geolocation error: " + message + "."
 		});
 	},
-	
-	_handleGeolocationResponse: function(pos) {
+
+	_handleGeolocationResponse: function (pos) {
 		var latAccuracy = 180 * pos.coords.accuracy / 4e7,
 			lngAccuracy = latAccuracy * 2,
 			lat = pos.coords.latitude,
-			lng = pos.coords.longitude;
-		
+			lng = pos.coords.longitude,
+			latlng = new L.LatLng(lat, lng);
+
 		var sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy),
 			ne = new L.LatLng(lat + latAccuracy, lng + lngAccuracy),
 			bounds = new L.LatLngBounds(sw, ne);
-		
-		if (this._setViewOnLocate) {
-			var zoom = Math.min(this.getBoundsZoom(bounds), this._maxLocateZoom);
-			this.setView(bounds.getCenter(), zoom);
-			this._setViewOnLocate = false;
+
+		if (this._locationOptions.setView) {
+			var zoom = Math.min(this.getBoundsZoom(bounds), this._locationOptions.maxZoom);
+			this.setView(latlng, zoom);
 		}
-		
+
 		this.fire('locationfound', {
-			latlng: new L.LatLng(lat, lng), 
+			latlng: latlng,
 			bounds: bounds,
 			accuracy: pos.coords.accuracy
 		});
 	}
-});
\ No newline at end of file
+});
diff --git a/src/map/ext/Map.Popup.js b/src/map/ext/Map.Popup.js
index 8b8de93..4d19f0d 100644
--- a/src/map/ext/Map.Popup.js
+++ b/src/map/ext/Map.Popup.js
@@ -1,15 +1,20 @@
 
 L.Map.include({
-	openPopup: function(popup) {
+	openPopup: function (popup) {
 		this.closePopup();
 		this._popup = popup;
-		return this.addLayer(popup);
-	},
+		this.addLayer(popup);
+		this.fire('popupopen', { popup: this._popup });
 	
-	closePopup: function() {
+		return this;
+	},
+
+	closePopup: function () {
 		if (this._popup) {
 			this.removeLayer(this._popup);
+			this.fire('popupclose', { popup: this._popup });
+			this._popup = null;
 		}
 		return this;
 	}
-});
\ No newline at end of file
+});
diff --git a/src/handler/ShiftDragZoom.js b/src/map/handler/Map.BoxZoom.js
similarity index 77%
rename from src/handler/ShiftDragZoom.js
rename to src/map/handler/Map.BoxZoom.js
index ba21610..9b74aea 100644
--- a/src/handler/ShiftDragZoom.js
+++ b/src/map/handler/Map.BoxZoom.js
@@ -1,79 +1,73 @@
-/*
- * L.Handler.ShiftDragZoom is used internally by L.Map to add shift-drag zoom (zoom to a selected bounding box).
- */
-
-L.Handler.ShiftDragZoom = L.Handler.extend({
-	initialize: function(map) {
-		this._map = map;
-		this._container = map._container;
-		this._pane = map._panes.overlayPane;
-	},
-	
-	enable: function() {
-		if (this._enabled) { return; }
-		
-		L.DomEvent.addListener(this._container, 'mousedown', this._onMouseDown, this);
-		
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		
-		L.DomEvent.removeListener(this._container, 'mousedown', this._onMouseDown);
-		
-		this._enabled = false;
-	},
-	
-	_onMouseDown: function(e) {
-		if (!e.shiftKey || ((e.which != 1) && (e.button != 1))) { return false; }
-		
-		L.DomUtil.disableTextSelection();
-		
-		this._startLayerPoint = this._map.mouseEventToLayerPoint(e);
-		
-		this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane);
-		L.DomUtil.setPosition(this._box, this._startLayerPoint);
-		
-		//TODO move cursor to styles
-		this._container.style.cursor = 'crosshair';
-		
-		L.DomEvent.addListener(document, 'mousemove', this._onMouseMove, this);
-		L.DomEvent.addListener(document, 'mouseup', this._onMouseUp, this);
-		
-		L.DomEvent.preventDefault(e);
-	},
-	
-	_onMouseMove: function(e) {
-		var layerPoint = this._map.mouseEventToLayerPoint(e),
-			dx = layerPoint.x - this._startLayerPoint.x,
-			dy = layerPoint.y - this._startLayerPoint.y;
-		
-		var newX = Math.min(layerPoint.x, this._startLayerPoint.x),
-			newY = Math.min(layerPoint.y, this._startLayerPoint.y),
-			newPos = new L.Point(newX, newY);
-		
-		L.DomUtil.setPosition(this._box, newPos);
-		
-		this._box.style.width = (Math.abs(dx) - 4) + 'px';
-		this._box.style.height = (Math.abs(dy) - 4) + 'px';
-	},
-	
-	_onMouseUp: function(e) {
-		this._pane.removeChild(this._box);
-		this._container.style.cursor = '';
-		
-		L.DomUtil.enableTextSelection();
-
-		L.DomEvent.removeListener(document, 'mousemove', this._onMouseMove);
-		L.DomEvent.removeListener(document, 'mouseup', this._onMouseUp);
-		
-		var layerPoint = this._map.mouseEventToLayerPoint(e);
-		
-		var bounds = new L.LatLngBounds(
-				this._map.layerPointToLatLng(this._startLayerPoint),
-				this._map.layerPointToLatLng(layerPoint));
-		
-		this._map.fitBounds(bounds);
-	}
-});
\ No newline at end of file
+/*
+ * L.Handler.ShiftDragZoom is used internally by L.Map to add shift-drag zoom (zoom to a selected bounding box).
+ */
+
+L.Map.BoxZoom = L.Handler.extend({
+	initialize: function (map) {
+		this._map = map;
+		this._container = map._container;
+		this._pane = map._panes.overlayPane;
+	},
+
+	addHooks: function () {
+		L.DomEvent.addListener(this._container, 'mousedown', this._onMouseDown, this);
+	},
+
+	removeHooks: function () {
+		L.DomEvent.removeListener(this._container, 'mousedown', this._onMouseDown);
+	},
+
+	_onMouseDown: function (e) {
+		if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) {
+			return false;
+		}
+
+		L.DomUtil.disableTextSelection();
+
+		this._startLayerPoint = this._map.mouseEventToLayerPoint(e);
+
+		this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane);
+		L.DomUtil.setPosition(this._box, this._startLayerPoint);
+
+		//TODO move cursor to styles
+		this._container.style.cursor = 'crosshair';
+
+		L.DomEvent.addListener(document, 'mousemove', this._onMouseMove, this);
+		L.DomEvent.addListener(document, 'mouseup', this._onMouseUp, this);
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_onMouseMove: function (e) {
+		var layerPoint = this._map.mouseEventToLayerPoint(e),
+			dx = layerPoint.x - this._startLayerPoint.x,
+			dy = layerPoint.y - this._startLayerPoint.y;
+
+		var newX = Math.min(layerPoint.x, this._startLayerPoint.x),
+			newY = Math.min(layerPoint.y, this._startLayerPoint.y),
+			newPos = new L.Point(newX, newY);
+
+		L.DomUtil.setPosition(this._box, newPos);
+
+		this._box.style.width = (Math.abs(dx) - 4) + 'px';
+		this._box.style.height = (Math.abs(dy) - 4) + 'px';
+	},
+
+	_onMouseUp: function (e) {
+		this._pane.removeChild(this._box);
+		this._container.style.cursor = '';
+
+		L.DomUtil.enableTextSelection();
+
+		L.DomEvent.removeListener(document, 'mousemove', this._onMouseMove);
+		L.DomEvent.removeListener(document, 'mouseup', this._onMouseUp);
+
+		var layerPoint = this._map.mouseEventToLayerPoint(e);
+
+		var bounds = new L.LatLngBounds(
+				this._map.layerPointToLatLng(this._startLayerPoint),
+				this._map.layerPointToLatLng(layerPoint));
+
+		this._map.fitBounds(bounds);
+	}
+});
diff --git a/src/map/handler/Map.DoubleClickZoom.js b/src/map/handler/Map.DoubleClickZoom.js
new file mode 100644
index 0000000..2edf794
--- /dev/null
+++ b/src/map/handler/Map.DoubleClickZoom.js
@@ -0,0 +1,18 @@
+/*
+ * L.Handler.DoubleClickZoom is used internally by L.Map to add double-click zooming.
+ */
+
+L.Map.DoubleClickZoom = L.Handler.extend({
+	addHooks: function () {
+		this._map.on('dblclick', this._onDoubleClick);
+		// TODO remove 3d argument?
+	},
+
+	removeHooks: function () {
+		this._map.off('dblclick', this._onDoubleClick);
+	},
+
+	_onDoubleClick: function (e) {
+		this.setView(e.latlng, this._zoom + 1);
+	}
+});
diff --git a/src/map/handler/Map.Drag.js b/src/map/handler/Map.Drag.js
new file mode 100644
index 0000000..fab6cd4
--- /dev/null
+++ b/src/map/handler/Map.Drag.js
@@ -0,0 +1,81 @@
+/*
+ * L.Handler.MapDrag is used internally by L.Map to make the map draggable.
+ */
+
+L.Map.Drag = L.Handler.extend({
+	addHooks: function () {
+		if (!this._draggable) {
+			this._draggable = new L.Draggable(this._map._mapPane, this._map._container);
+
+			this._draggable
+				.on('dragstart', this._onDragStart, this)
+				.on('drag', this._onDrag, this)
+				.on('dragend', this._onDragEnd, this);
+
+			var options = this._map.options;
+
+			if (options.worldCopyJump && !options.continuousWorld) {
+				this._draggable.on('predrag', this._onPreDrag, this);
+				this._map.on('viewreset', this._onViewReset, this);
+			}
+		}
+		this._draggable.enable();
+	},
+
+	removeHooks: function () {
+		this._draggable.disable();
+	},
+
+	moved: function () {
+		return this._draggable && this._draggable._moved;
+	},
+
+	_onDragStart: function () {
+		this._map
+			.fire('movestart')
+			.fire('dragstart');
+	},
+
+	_onDrag: function () {
+		this._map
+			.fire('move')
+			.fire('drag');
+	},
+
+	_onViewReset: function () {
+		var pxCenter = this._map.getSize().divideBy(2),
+			pxWorldCenter = this._map.latLngToLayerPoint(new L.LatLng(0, 0));
+
+		this._initialWorldOffset = pxWorldCenter.subtract(pxCenter);
+	},
+
+	_onPreDrag: function () {
+		var map = this._map,
+			worldWidth = map.options.scale(map.getZoom()),
+			halfWidth = Math.round(worldWidth / 2),
+			dx = this._initialWorldOffset.x,
+			x = this._draggable._newPos.x,
+			newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
+			newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
+			newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
+
+		this._draggable._newPos.x = newX;
+	},
+
+	_onDragEnd: function () {
+		var map = this._map;
+
+		map
+			.fire('moveend')
+			.fire('dragend');
+
+		if (map.options.maxBounds) {
+			// TODO predrag validation instead of animation
+			L.Util.requestAnimFrame(this._panInsideMaxBounds, map, true, map._container);
+		}
+	},
+
+	_panInsideMaxBounds: function () {
+		this.panInsideBounds(this.options.maxBounds);
+	}
+});
diff --git a/src/map/handler/Map.ScrollWheelZoom.js b/src/map/handler/Map.ScrollWheelZoom.js
new file mode 100644
index 0000000..58da770
--- /dev/null
+++ b/src/map/handler/Map.ScrollWheelZoom.js
@@ -0,0 +1,55 @@
+/*
+ * L.Handler.ScrollWheelZoom is used internally by L.Map to enable mouse scroll wheel zooming on the map.
+ */
+
+L.Map.ScrollWheelZoom = L.Handler.extend({
+	addHooks: function () {
+		L.DomEvent.addListener(this._map._container, 'mousewheel', this._onWheelScroll, this);
+		this._delta = 0;
+	},
+
+	removeHooks: function () {
+		L.DomEvent.removeListener(this._map._container, 'mousewheel', this._onWheelScroll);
+	},
+
+	_onWheelScroll: function (e) {
+		var delta = L.DomEvent.getWheelDelta(e);
+		this._delta += delta;
+		this._lastMousePos = this._map.mouseEventToContainerPoint(e);
+
+		clearTimeout(this._timer);
+		this._timer = setTimeout(L.Util.bind(this._performZoom, this), 50);
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_performZoom: function () {
+		var map = this._map,
+			delta = Math.round(this._delta),
+			zoom = map.getZoom();
+
+		delta = Math.max(Math.min(delta, 4), -4);
+		delta = map._limitZoom(zoom + delta) - zoom;
+
+		this._delta = 0;
+
+		if (!delta) {
+			return;
+		}
+
+		var newCenter = this._getCenterForScrollWheelZoom(this._lastMousePos, delta),
+			newZoom = zoom + delta;
+
+		map.setView(newCenter, newZoom);
+	},
+
+	_getCenterForScrollWheelZoom: function (mousePos, delta) {
+		var map = this._map,
+			centerPoint = map.getPixelBounds().getCenter(),
+			viewHalf = map.getSize().divideBy(2),
+			centerOffset = mousePos.subtract(viewHalf).multiplyBy(1 - Math.pow(2, -delta)),
+			newCenterPoint = centerPoint.add(centerOffset);
+
+		return map.unproject(newCenterPoint, map._zoom, true);
+	}
+});
diff --git a/src/handler/TouchZoom.js b/src/map/handler/Map.TouchZoom.js
similarity index 71%
rename from src/handler/TouchZoom.js
rename to src/map/handler/Map.TouchZoom.js
index cc2ec73..daae66e 100644
--- a/src/handler/TouchZoom.js
+++ b/src/map/handler/Map.TouchZoom.js
@@ -1,87 +1,93 @@
-/*
- * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
- */
-
-L.Handler.TouchZoom = L.Handler.extend({
-	enable: function() {
-		if (!L.Browser.mobileWebkit || this._enabled) { return; }
-		L.DomEvent.addListener(this._map._container, 'touchstart', this._onTouchStart, this);
-		this._enabled = true;
-	},
-	
-	disable: function() {
-		if (!this._enabled) { return; }
-		L.DomEvent.removeListener(this._map._container, 'touchstart', this._onTouchStart, this);
-		this._enabled = false;
-	},
-	
-	_onTouchStart: function(e) {
-		if (!e.touches || e.touches.length != 2 || this._map._animatingZoom) { return; }
-		
-		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
-			p2 = this._map.mouseEventToLayerPoint(e.touches[1]),
-			viewCenter = this._map.containerPointToLayerPoint(this._map.getSize().divideBy(2));
-		
-		this._startCenter = p1.add(p2).divideBy(2, true);
-		this._startDist = p1.distanceTo(p2);
-		//this._startTransform = this._map._mapPane.style.webkitTransform;
-		
-		this._moved = false;
-		this._zooming = true;
-
-		this._centerOffset = viewCenter.subtract(this._startCenter);
-
-		L.DomEvent.addListener(document, 'touchmove', this._onTouchMove, this);
-		L.DomEvent.addListener(document, 'touchend', this._onTouchEnd, this);
-		
-		L.DomEvent.preventDefault(e);
-	},
-	
-	_onTouchMove: function(e) {
-		if (!e.touches || e.touches.length != 2) { return; }
-		
-		if (!this._moved) {
-			this._map._mapPane.className += ' leaflet-zoom-anim';
-			this._map._prepareTileBg();
-			this._moved = true;
-		}
-		
-		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
-			p2 = this._map.mouseEventToLayerPoint(e.touches[1]);
-		
-		this._scale = p1.distanceTo(p2) / this._startDist;
-		this._delta = p1.add(p2).divideBy(2, true).subtract(this._startCenter);
-
-		/*
-		 * Used 2 translates instead of transform-origin because of a very strange bug - 
-		 * it didn't count the origin on the first touch-zoom but worked correctly afterwards 
-		 */
-		this._map._tileBg.style.webkitTransform = [
-            L.DomUtil.getTranslateString(this._delta),
-            L.DomUtil.getScaleString(this._scale, this._startCenter)
-        ].join(" ");
-		
-		L.DomEvent.preventDefault(e);
-	},
-	
-	_onTouchEnd: function(e) {
-		if (!this._moved || !this._zooming) { return; }
-		this._zooming = false;
-		
-		var oldZoom = this._map.getZoom(),
-			floatZoomDelta = Math.log(this._scale)/Math.LN2,
-			roundZoomDelta = (floatZoomDelta > 0 ? Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
-			zoom = this._map._limitZoom(oldZoom + roundZoomDelta),
-			zoomDelta = zoom - oldZoom,
-			centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
-			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
-			center = this._map.unproject(centerPoint);
-		
-		L.DomEvent.removeListener(document, 'touchmove', this._onTouchMove);
-		L.DomEvent.removeListener(document, 'touchend', this._onTouchEnd);
-
-		var finalScale = Math.pow(2, zoomDelta);
-		
-		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset));
-	}
-});
\ No newline at end of file
+/*
+ * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
+ */
+
+L.Map.TouchZoom = L.Handler.extend({
+	addHooks: function () {
+		L.DomEvent.addListener(this._map._container, 'touchstart', this._onTouchStart, this);
+	},
+
+	removeHooks: function () {
+		L.DomEvent.removeListener(this._map._container, 'touchstart', this._onTouchStart, this);
+	},
+
+	_onTouchStart: function (e) {
+		if (!e.touches || e.touches.length !== 2 || this._map._animatingZoom) {
+			return;
+		}
+
+		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = this._map.mouseEventToLayerPoint(e.touches[1]),
+			viewCenter = this._map.containerPointToLayerPoint(this._map.getSize().divideBy(2));
+
+		this._startCenter = p1.add(p2).divideBy(2, true);
+		this._startDist = p1.distanceTo(p2);
+		//this._startTransform = this._map._mapPane.style.webkitTransform;
+
+		this._moved = false;
+		this._zooming = true;
+
+		this._centerOffset = viewCenter.subtract(this._startCenter);
+
+		L.DomEvent.addListener(document, 'touchmove', this._onTouchMove, this);
+		L.DomEvent.addListener(document, 'touchend', this._onTouchEnd, this);
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_onTouchMove: function (e) {
+		if (!e.touches || e.touches.length !== 2) {
+			return;
+		}
+
+		if (!this._moved) {
+			this._map._mapPane.className += ' leaflet-zoom-anim';
+
+			this._map
+				.fire('zoomstart')
+				.fire('movestart')
+				._prepareTileBg();
+
+			this._moved = true;
+		}
+
+		var p1 = this._map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = this._map.mouseEventToLayerPoint(e.touches[1]);
+
+		this._scale = p1.distanceTo(p2) / this._startDist;
+		this._delta = p1.add(p2).divideBy(2, true).subtract(this._startCenter);
+
+		// Used 2 translates instead of transform-origin because of a very strange bug -
+		// it didn't count the origin on the first touch-zoom but worked correctly afterwards
+
+		this._map._tileBg.style.webkitTransform = [
+            L.DomUtil.getTranslateString(this._delta),
+            L.DomUtil.getScaleString(this._scale, this._startCenter)
+        ].join(" ");
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_onTouchEnd: function (e) {
+		if (!this._moved || !this._zooming) {
+			return;
+		}
+		this._zooming = false;
+
+		var oldZoom = this._map.getZoom(),
+			floatZoomDelta = Math.log(this._scale) / Math.LN2,
+			roundZoomDelta = (floatZoomDelta > 0 ? Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
+			zoom = this._map._limitZoom(oldZoom + roundZoomDelta),
+			zoomDelta = zoom - oldZoom,
+			centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
+			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
+			center = this._map.unproject(centerPoint);
+
+		L.DomEvent.removeListener(document, 'touchmove', this._onTouchMove);
+		L.DomEvent.removeListener(document, 'touchend', this._onTouchEnd);
+
+		var finalScale = Math.pow(2, zoomDelta);
+
+		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset));
+	}
+});

-- 
JavaScript library for displaying map data in web browsers



More information about the Pkg-osm-commits mailing list