[Pkg-javascript-commits] [leaflet] 01/01: Imported Upstream version 0.4~20120621

Jonas Smedegaard js at moszumanska.debian.org
Mon Jan 27 20:09:09 UTC 2014


This is an automated email from the git hooks/post-receive script.

js pushed a commit to annotated tag upstream/0.4_20120621
in repository leaflet.

commit d156b9685e6cc23b6a52e9a32c676e138ba6d248
Author: Jonas Smedegaard <dr at jones.dk>
Date:   Thu Jun 21 18:52:02 2012 +0200

    Imported Upstream version 0.4~20120621
---
 .gitignore                                         |    1 +
 CHANGELOG.md                                       |  104 +-
 Jakefile.js                                        |   26 +-
 README.md                                          |   14 +-
 build/build.html                                   |   10 +-
 build/build.js                                     |   10 +-
 build/deps.js                                      |   26 +
 build/hintrc.js                                    |    9 +-
 debug/control/map-control.html                     |   29 -
 debug/leaflet-include.js                           |    9 +-
 .../control-layers.html => map/controls.html}      |   24 +-
 debug/map/geolocation.html                         |   35 +
 debug/map/map-mobile.html                          |   36 +-
 debug/map/map.html                                 |    6 +-
 debug/vector/bounds-extend.html                    |   88 +
 debug/vector/editable.html                         |   56 +-
 .../feature-group-bounds.html}                     |   42 +-
 debug/{geojson => vector}/geojson-sample.js        |    0
 debug/{geojson => vector}/geojson.html             |    0
 debug/vector/rectangle.html                        |   53 +
 debug/vector/touchzoomemu.html                     |  121 +
 debug/vector/vector-simple.html                    |   16 +-
 dist/images/{marker.png => marker-icon.png}        |  Bin
 dist/leaflet-src.js                                | 6664 ++++++++++++++++++++
 dist/leaflet.css                                   |  128 +-
 dist/leaflet.ie.css                                |    4 -
 dist/leaflet.js                                    |    6 +
 spec/jasmine/jasmine-html.js                       |  182 +
 spec/jasmine/jasmine.css                           |  166 +
 spec/jasmine/jasmine.js                            | 2421 +++++++
 spec/runner.html                                   |   20 +-
 spec/suites/core/ClassSpec.js                      |    5 +-
 src/Leaflet.js                                     |   43 +-
 src/control/Control.Attribution.js                 |   75 +-
 src/control/Control.Layers.js                      |   52 +-
 src/control/Control.Scale.js                       |   94 +
 src/control/Control.Zoom.js                        |   51 +-
 src/control/Control.js                             |   68 +-
 src/core/Browser.js                                |    7 +-
 src/core/Class.js                                  |   27 +-
 src/core/Util.js                                   |   64 +-
 src/dom/DomEvent.js                                |   21 +-
 src/dom/DomUtil.js                                 |   17 +-
 src/dom/Draggable.js                               |   14 +-
 src/dom/transition/Transition.Native.js            |   11 +-
 src/geo/LatLngBounds.js                            |   43 +-
 src/geo/crs/CRS.EPSG3395.js                        |    1 +
 src/geo/crs/CRS.EPSG3857.js                        |    2 +-
 src/geo/crs/CRS.Simple.js                          |    5 +
 src/geo/crs/CRS.js                                 |   16 +-
 src/geo/projection/Projection.Mercator.js          |    4 +-
 src/geo/projection/Projection.SphericalMercator.js |    4 +-
 src/layer/FeatureGroup.js                          |   20 +-
 src/layer/GeoJSON.js                               |   39 +-
 src/layer/ImageOverlay.js                          |   14 +-
 src/layer/LayerGroup.js                            |   11 +-
 src/layer/Popup.js                                 |  148 +-
 src/layer/marker/DivIcon.js                        |   20 +
 src/layer/marker/Icon.js                           |  109 +-
 src/layer/marker/Marker.Drag.js                    |    4 +-
 src/layer/marker/Marker.Popup.js                   |   16 +-
 src/layer/marker/Marker.js                         |  110 +-
 src/layer/tile/TileLayer.Canvas.js                 |   16 +-
 src/layer/tile/TileLayer.WMS.js                    |   35 +-
 src/layer/tile/TileLayer.js                        |  197 +-
 src/layer/vector/Circle.js                         |   46 +-
 src/layer/vector/CircleMarker.js                   |    3 +-
 src/layer/vector/MultiPoly.js                      |    2 +
 src/layer/vector/Path.SVG.js                       |   76 +-
 src/layer/vector/Path.VML.js                       |  114 +-
 src/layer/vector/Path.js                           |   25 +-
 src/layer/vector/Polyline.Edit.js                  |  210 +
 src/layer/vector/Polyline.js                       |   43 +-
 src/layer/vector/Rectangle.js                      |   23 +
 src/layer/vector/canvas/Circle.Canvas.js           |    2 +-
 src/layer/vector/canvas/Path.Canvas.js             |   66 +-
 src/map/Map.js                                     |  375 +-
 src/map/anim/Map.PanAnimation.js                   |   17 +-
 src/map/anim/Map.ZoomAnimation.js                  |  117 +-
 src/map/ext/Map.Control.js                         |   48 +-
 src/map/ext/Map.Geolocation.js                     |   43 +-
 src/map/ext/Map.Popup.js                           |   15 +-
 src/map/handler/Map.BoxZoom.js                     |   63 +-
 src/map/handler/Map.DoubleClickZoom.js             |    7 +-
 src/map/handler/Map.Drag.js                        |   84 +-
 src/map/handler/Map.ScrollWheelZoom.js             |   11 +-
 src/map/handler/Map.TouchZoom.js                   |   97 +-
 87 files changed, 12047 insertions(+), 1109 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7af2171..6dfa113 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,22 +5,111 @@ Leaflet Changelog
 
 ## 0.4 (master)
 
+An in-progress version being developed on the master branch.
+
+### Notable new features
+
+ * Added configurable **panning inertia** - after a quick pan, the map slows down in the same direction.
+ * Added **polyline and polygon editing**. [#174](https://github.com/CloudMade/Leaflet/issues/174)
+ * Added an unobtrusive **scale control**.
+ * Added `DivIcon` class that easily allows you to create lightweight div-based markers.
+ * Added `Rectangle` vector layer (by [@JasonSanford](https://github.com/JasonSanford)). [#504](https://github.com/CloudMade/Leaflet/pull/504)
+
 ### Improvements
 
- * Added `setPosition` and `getPosition` to all controls, as well as ability to pass certain position as an option when creating a control.
+#### Usability improvements
+
+ * Added smooth **zoom animation of markers, vector layers and popups** (by [@danzel](https://github.com/danzel)). [#740](https://github.com/CloudMade/Leaflet/pull/740)
+ * Improved zooming so that you don't get a blank map when you zoom in or out twice quickly (by [@danzel](https://github.com/danzel)). [#7](https://github.com/CloudMade/Leaflet/issues/7) [#729](https://github.com/CloudMade/Leaflet/pull/729)
+ * Drag-panning now works even when there are markers in the starting point (helps on maps with lots of markers). [#506](https://github.com/CloudMade/Leaflet/issues/506)
+ * Improved panning performance even more (there are no wasted frames now).
+ * Replaced box-shadow with border on controls for mobile devices to improve performance.
+ * Slightly improved default popup styling.
+ * Added `TileLayer` `detectRetina` option (`false` by default) that makes tiles show in a higher resolution on iOS retina displays (by [Mithgol](https://github.com/Mithgol)). [#586](https://github.com/CloudMade/Leaflet/pull/586)
+
+#### Breaking API changes
+
+ * Converted `Icon` properties (like `iconUrl`) to options, changed constructor signature to `Icon(options)`.
+ * Moved default marker icon options to `L.Icon.Default` class (which extends from `L.Icon`).
+ * Improved `TileLayer` constructor to interpolate URL template values from options (removed third `urlParams` argument).
  * Replaced ugly control position constants (e.g. L.Control.Position.TOP_LEFT) with light strings ('topleft', 'bottomright', etc.)
+ * Removed `Map` `locateAndSetView` method (use `locate` with `setView: true` option)
+ * Changed popup `minWidth` and `maxWidth` options to be applied to content element, not the whole popup.
+ * Moved `prefix` argument to `options` in `Control.Attribution` constructor.
+ * Renamed `L.VERSION` to `L.version`.
+
+#### Other API improvements
+
+ * Added `Icon` `className` option to assign a custom class to an icon.
+ * Added `Icon` `shadowOffset` option to set the position of shadow relative to the icon.
+ * Made all `Icon` options except `iconUrl` optional (if not specified, they'll be chosen automatically or implemented using CSS). Anchor is centered by default (if size is specified), and otherwise can be set through CSS using negative margins.
+ * Moved all default marker icon options from `L.Icon` to `L.Icon.Default`.
+ * Added `originalEvent` property to `MouseEvent` (by [@k4](https://github.com/k4)). [#521](https://github.com/CloudMade/Leaflet/pull/521)
+ * Added `Circle` `getBounds` method. [#440](https://github.com/CloudMade/Leaflet/issues/440)
+ * Added `Circle` `getLatLng` and `getRadius` methods (by [Guiswa](https://github.com/Guiswa)). [#655](https://github.com/CloudMade/Leaflet/pull/655)
+ * Added `Map` `getContainer` method (by [Guiswa](https://github.com/Guiswa)). [#654](https://github.com/CloudMade/Leaflet/pull/654)
+ * Added `Marker` `opacity` option.
+ * Added public `redraw` method to vector layers (useful if you manipulate their `LatLng` points directly).
+ * Added `setPosition` and `getPosition` to all controls, as well as ability to pass certain position as an option when creating a control.
+ * Added `Popup` `maxHeight` option that makes content inside the popup scrolled if it doesn't fit the specified max height.
  * Made controls implementation easier (now more magic happens under the hood).
+ * Added `Map` `containerPointToLatLng` and `latLngToContainerPoint` methods. [#474](https://github.com/CloudMade/Leaflet/issues/474)
+ * Added `containerPoint` property to `MouseEvent`. [#413](https://github.com/CloudMade/Leaflet/issues/413)
+ * Added `LatLngBounds` `pad` method that returns bounds extended by a percentage (by [@jacobtoye](https://github.com/jacobtoye)). [#492](https://github.com/CloudMade/Leaflet/pull/492)
+ * Added `contextmenu` event to vector layers (by [@ErrorProne](https://github.com/ErrorProne)). [#500](https://github.com/CloudMade/Leaflet/pull/500)
+ * Added chaining to `DomEvent` methods.
+ * Added `Map` `addHandler` method.
+ * Moved dragging cursor styles from JS code to CSS.
+ * Improved `Marker` `openPopup` not to raise an error if it doesn't have a popup. [#507](https://github.com/CloudMade/Leaflet/issues/507)
+ * Added `geometry` property to `GeoJSON` `featureparse` event (by [@twinbit](https://github.com/twinbit)). [#716](https://github.com/CloudMade/Leaflet/pull/716)
 
 ### Bug fixes
 
+#### General bugfixes
+
+ * Fixed a bug where the map was zooming incorrectly inside a `position: fixed` container (by [chx007](https://github.com/chx007)). [#602](https://github.com/CloudMade/Leaflet/pull/602)
+ * Fixed a bug where scaled tiles weren't cleared up after zoom in some cases (by [cfis](https://github.com/cfis)) [#683](https://github.com/CloudMade/Leaflet/pull/683)
+
+#### API bugfixes
+
+ * Fixed a regression where removeLayer would not remove corresponding attribution. [#488](https://github.com/CloudMade/Leaflet/issues/488)
+ * Fixed a bug where popup close button wouldn't work on manually added popups. [#423](https://github.com/CloudMade/Leaflet/issues/423)
  * Fixed a bug where `TileLayer.WMS` wouldn't take `insertAtTheBottom` option into account (by [@bmcbride](https://github.com/bmcbride)). [#478](https://github.com/CloudMade/Leaflet/pull/478)
+ * Fixed a bug where marker click event would stop working if you dragged it and then disabled dragging. [#434](https://github.com/CloudMade/Leaflet/issues/434)
+ * Fixed a bug where `TileLayer` `setOpacity` wouldn't work when setting it back to 1.
+ * Fixed a bug where vector layer `setStyle({stroke: false})` wouldn't remove stroke and the same for fill. [#441](https://github.com/CloudMade/Leaflet/issues/441)
+ * Fixed a bug where `Marker` `bindPopup` method wouldn't take `offset` option into account.
+ * Fixed a bug where `TileLayer` `load` event wasn't fired if some tile didn't load (by [cfis](https://github.com/cfis)) [#682](https://github.com/CloudMade/Leaflet/pull/682)
+ * Fixed error when removing `GeoJSON` layer. [#685](https://github.com/CloudMade/Leaflet/issues/685)
+ * Fixed error when calling `GeoJSON` `clearLayer` (by [runderwood](https://github.com/runderwood)). [#617](https://github.com/CloudMade/Leaflet/pull/617)
+ * Fixed a bug where polygons/polylines sometimes throwed an error when making them editable manually (by [cfis](https://github.com/cfis)). [#669](https://github.com/CloudMade/Leaflet/pull/669)
+ * Fixed a bug where `Control` `setPosition` wasn't always working correctly (by [ericmmartinez](https://github.com/ericmmartinez)). [#657](https://github.com/CloudMade/Leaflet/pull/657)
+ * Fixed a bug with `Util.bind` sometimes losing arguments (by [johtso](https://github.com/johtso)). [#588](https://github.com/CloudMade/Leaflet/pull/588)
+
+#### Browser bugfixes
+
+ * Fixed inability to use scrolled content inside popup due to mouse wheel propagation.
+ * Fixed a bug that caused jumping/stuttering of panning animation in some cases.
+ * Fixed a bug where popup size was calculated incorrectly in IE.
+ * Fixed a bug where cursor would flicker when dragging a marker.
+ * Fixed a bug where clickable paths on IE9 didn't have a hand cursor (by [naehrstoff](https://github.com/naehrstoff)). [#671](https://github.com/CloudMade/Leaflet/pull/671)
+ * Fixed a bug in IE with disappearing icons when changing opacity (by [tagliala](https://github.com/tagliala) and [DamonOehlman](https://github.com/DamonOehlman)). [#667](https://github.com/CloudMade/Leaflet/pull/667) [#600](https://github.com/CloudMade/Leaflet/pull/600)
+ * Fixed a bug where `Control.Layers` didn't work on IE7. [#652](https://github.com/CloudMade/Leaflet/issues/652)
+ * Fixed a bug that caused popups to be empty in IE when passing a DOM node as the content (by [@nrenner](https://github.com/nrenner)). [#472](https://github.com/CloudMade/Leaflet/pull/472)
 
-## 0.3.1 (14.02.2012)
+#### Mobile browser bugfixes
+
+ * Fixed a bug with false map click events on pinch-zoom and zoom/layers controls click. [#485](https://github.com/CloudMade/Leaflet/issues/485)
+ * Fixed a bug where touching the map with two or more fingers simultaneously would raise an error.
+ * Fixed a bug where zoom control wasn't always visible on Android 3. [#335](https://github.com/CloudMade/Leaflet/issues/335)
+ * Fixed a bug where opening the layers control would propagate a click to the map (by [jacobtoye](https://github.com/jacobtoye)). [#638](https://github.com/CloudMade/Leaflet/pull/638)
+
+## 0.3.1 (February 14, 2012)
 
  * Fixed a regression where default marker icons wouldn't work if Leaflet include url contained a query string.
  * Fixed a regression where tiles sometimes flickered with black on panning in iOS.
 
-## 0.3 (13.02.2012)
+## 0.3 (February 13, 2012)
 
 ### Major features
 
@@ -56,6 +145,7 @@ Leaflet Changelog
  * 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 `getBounds` method to `FeatureGroup` (by [@JasonSanford](https://github.com/JasonSanford)). [#557](https://github.com/CloudMade/Leaflet/pull/557)
  * 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)
@@ -64,12 +154,20 @@ Leaflet Changelog
  * 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)
+ * Made `LatLngBounds` `extend` method to accept other `LatLngBounds` in addition to `LatLng` (by [@JasonSanford](https://github.com/JasonSanford)). [#553](https://github.com/CloudMade/Leaflet/pull/553)
  * 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)
+ * Added `Map` `boxzoomstart` and `boxzoomend` events (by [zedd45](https://github.com/zedd45)). [#554](https://github.com/CloudMade/Leaflet/pull/554)
+ * Added `Popup` `contentupdate` event (by [mehmeta](https://github.com/mehmeta)). [#548](https://github.com/CloudMade/Leaflet/pull/548)
+
+#### Breaking API changes
+
+ * `shiftDragZoom` map option/property renamed to `boxZoom`.
+ * Removed `mouseEventToLatLng` method (bringed back in 0.4).
 
 #### Development workflow improvements
 
diff --git a/Jakefile.js b/Jakefile.js
index 43eab8b..01acdbc 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -1,19 +1,18 @@
 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;
+var COPYRIGHT = '/*\n Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin\n' +
+                ' Leaflet is a modern open-source JavaScript library for interactive maps.\n' + 
+                ' http://leaflet.cloudmade.com\n*/\n';
 
 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();
@@ -31,27 +30,28 @@ task('build', ['lint'], function (compsBase32, buildName) {
 	var files = build.getFiles(compsBase32);
 
 	console.log('Concatenating ' + files.length + ' files...');
-	var content = build.combineFiles(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) {
@@ -62,4 +62,4 @@ task('build', ['lint'], function (compsBase32, buildName) {
 	}
 });
 
-task('default', ['build']);
\ No newline at end of file
+task('default', ['build']);
diff --git a/README.md b/README.md
index db9238a..01e7340 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
 <img src="http://leaflet.cloudmade.com/docs/images/logo.png" alt="Leaflet" />
 
-Leaflet is a modern, lightweight BSD-licensed JavaScript library for making tile-based interactive maps for both desktop and mobile web browsers, developed by [CloudMade](http://cloudmade.com) to form the core of its next generation JavaScript API.
+Leaflet is a modern, lightweight open-source JavaScript library for mobile-friendly interactive maps, developed by [CloudMade](http://cloudmade.com) to form the core of its next generation JavaScript API. Weighting just about 21kb of gzipped JS code, it still has all the [features](http://leaflet.cloudmade.com/features.html) you will ever need for your web mapping needs while providing a fast, smooth, pleasant user experience.
 
-It is built from the ground up to work efficiently and smoothly on both platforms, utilizing cutting-edge technologies included in HTML5. Its top priorities are usability, performance and small size, [A-grade](http://developer.yahoo.com/yui/articles/gbs/) browser support, flexibility and easy to use API. The OOP-based code of the library is designed to be modular, extensible and very easy to understand.
+It is built from the ground up to work efficiently and smoothly on both desktop and mobile platforms like iOS and Android, utilizing cutting-edge technologies included in HTML5 and CSS3, focusing on usability, performance, small size, [A-grade](http://developer.yahoo.com/yui/articles/gbs/) browser support, flexibility and [easy to use API](http://leaflet.cloudmade.com/reference.html). The OOP-based code of the library is designed to be modular, extensible and very easy to understand.
 
 Check out the website for more information: [leaflet.cloudmade.com](http://leaflet.cloudmade.com)
 
 ## Contributing to Leaflet
-Let's make the best open-source library for maps that can possibly exist! 
+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.
 
@@ -18,15 +18,15 @@ Leaflet build system is powered by the Node.js platform and Jake, JSHint and Ugl
 
  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
+ npm install jshint
+ npm install 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
+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!
diff --git a/build/build.html b/build/build.html
index b216002..9e5a233 100644
--- a/build/build.html
+++ b/build/build.html
@@ -86,13 +86,13 @@
 			<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>
+npm install jshint
+npm install 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><a href="http://closure-compiler.googlecode.com/files/compiler-latest.zip">Download Closure Compiler</a>, extract it into <code>closure-compiler</code> directory</li>
 			<li>Run this command in the root Leaflet directory: <br /><input type="text" id="command" /></li>
 		</ol>
 	</div>
@@ -139,7 +139,7 @@ npm install -g uglify-js</code></pre></li>
 				}
 			}
 
-			var command = 'java -jar lib/closure-compiler/compiler.jar ';
+			var command = 'java -jar closure-compiler/compiler.jar ';
 			for (var src in files) {
 				command += '--js src/' + src + ' ';
 			}
@@ -154,7 +154,7 @@ npm install -g uglify-js</code></pre></li>
 			this.focus();
 			this.select();
 		};
-		
+
 		commandInput.onclick = inputSelect;
 		commandInput2.onclick = inputSelect;
 
diff --git a/build/build.js b/build/build.js
index 68cb44b..54cadc8 100644
--- a/build/build.js
+++ b/build/build.js
@@ -43,19 +43,19 @@ 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_mangle(ast, {mangle: true});
+	ast = pro.ast_squeeze(ast);
 	ast = pro.ast_squeeze_more(ast);
 
 	return pro.gen_code(ast) + ';';
 };
 
 exports.combineFiles = function (files) {
-	var content = '';
+	var content = '(function () {\n\n';
 	for (var i = 0, len = files.length; i < len; i++) {
-		content += fs.readFileSync(files[i], 'utf8') + '\r\n\r\n';
+		content += fs.readFileSync(files[i], 'utf8') + '\n\n';
 	}
-	return content;
+	return content + '\n\n}());';
 };
 
 exports.save = function (savePath, compressed) {
diff --git a/build/deps.js b/build/deps.js
index e6c5ff0..f89d276 100644
--- a/build/deps.js
+++ b/build/deps.js
@@ -57,6 +57,12 @@ var deps = {
 		desc: 'Markers to put on the map.'
 	},
 
+	DivIcon: {
+		src: ['layer/marker/DivIcon.js'],
+		deps: ['Marker'],
+		desc: 'Lightweight div-based icon for markers.'
+	},
+
 	Popup: {
 		src: ['layer/Popup.js', 'layer/marker/Marker.Popup.js', 'map/ext/Map.Popup.js'],
 		deps: ['Marker'],
@@ -110,6 +116,12 @@ var deps = {
 		desc: 'MultiPolygon and MultyPolyline layers.'
 	},
 
+	Rectangle: {
+		src: ['layer/vector/Rectangle.js'],
+		deps: ['Polygon'],
+		desc: ['Rectangle overlays.']
+	},
+
 	Circle: {
 		src: ['layer/vector/Circle.js'],
 		deps: ['Path'],
@@ -170,9 +182,16 @@ var deps = {
 
 	MarkerDrag: {
 		src: ['layer/marker/Marker.Drag.js'],
+		deps: ['Marker'],
 		desc: 'Makes markers draggable (by mouse or touch).'
 	},
 
+	PolyEdit: {
+		src: ['layer/vector/Polyline.Edit.js'],
+		deps: ['Polyline', 'DivIcon'],
+		desc: 'Polyline and polygon editing.'
+	},
+
 
 	ControlZoom: {
 		src: ['control/Control.js',
@@ -189,6 +208,13 @@ var deps = {
 		desc: 'Attribution control.'
 	},
 
+	ControlScale: {
+		src: ['control/Control.js',
+		      'map/ext/Map.Control.js',
+		      'control/Control.Scale.js'],
+		desc: 'Scale control.'
+	},
+
 	ControlLayers: {
 		src: ['control/Control.js',
 		      'map/ext/Map.Control.js',
diff --git a/build/hintrc.js b/build/hintrc.js
index b9f37ac..d05d406 100644
--- a/build/hintrc.js
+++ b/build/hintrc.js
@@ -1,5 +1,6 @@
 exports.config = {
 	"browser": true,
+	"node": true,
 	"predef": ["L"],
 
 	"debug": false,
@@ -17,7 +18,7 @@ exports.config = {
 	"eqnull": false,
 	"evil": false,
 	"expr": false,
-	"forin": false,
+	"forin": true,
 	"immed": true,
 	"latedef": true,
 	"loopfunc": false,
@@ -28,6 +29,7 @@ exports.config = {
 	"shadow": false,
 	"supernew": false,
 	"undef": true,
+	"funcscope": false,
 
 	"newcap": true,
 	"noempty": true,
@@ -40,5 +42,6 @@ exports.config = {
 
 	"eqeqeq": true,
 	"trailing": true,
-	"white": true
-};
\ No newline at end of file
+	"white": true,
+	"smarttabs": true
+};
diff --git a/debug/control/map-control.html b/debug/control/map-control.html
deleted file mode 100644
index 119d194..0000000
--- a/debug/control/map-control.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!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">
-
-		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
-			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18}),
-			latlng = new L.LatLng(50.5, 30.51);
-	
-		var map = new L.Map('map').addLayer(cloudmade).setView(latlng, 15);
-		
-		var zoomControl = new L.Control.Zoom();
-		map.addControl(zoomControl);
-	</script>
-</body>
-</html>
\ No newline at end of file
diff --git a/debug/leaflet-include.js b/debug/leaflet-include.js
index 48eab66..d83fd92 100644
--- a/debug/leaflet-include.js
+++ b/debug/leaflet-include.js
@@ -62,6 +62,7 @@
 		'layer/Popup.js',
 
 		'layer/marker/Icon.js',
+		'layer/marker/DivIcon.js',
 		'layer/marker/Marker.js',
 		'layer/marker/Marker.Popup.js',
 		'layer/marker/Marker.Drag.js',
@@ -72,8 +73,10 @@
 		'layer/vector/Path.VML.js',
 		'layer/vector/canvas/Path.Canvas.js',
 		'layer/vector/Polyline.js',
+		'layer/vector/Polyline.Edit.js',
 		'layer/vector/canvas/Polyline.Canvas.js',
 		'layer/vector/Polygon.js',
+		'layer/vector/Rectangle.js',
 		'layer/vector/canvas/Polygon.Canvas.js',
 		'layer/vector/MultiPoly.js',
 		'layer/vector/Circle.js',
@@ -86,6 +89,7 @@
 		'control/Control.Zoom.js',
 		'control/Control.Attribution.js',
 		'control/Control.Layers.js',
+		'control/Control.Scale.js'
 	];
 
 	function getSrcUrl() {
@@ -102,9 +106,10 @@
 	}
 
 	var path = getSrcUrl();
-	for (var i = 0; i < scripts.length; i++) {
-		document.writeln("<script type='text/javascript' src='" + path + "../src/" + scripts[i] + "'></script>");
+    for (var i = 0; i < scripts.length; i++) {
+		document.writeln("<script src='" + path + scripts[i] + "'></script>");
 	}
+    document.writeln('<script>L.Icon.Default.imagePath = "' + path + '../dist/images";</script>');
 })();
 
 function getRandomLatLng(map) {
diff --git a/debug/control/control-layers.html b/debug/map/controls.html
similarity index 88%
rename from debug/control/control-layers.html
rename to debug/map/controls.html
index 32d89c3..ce04dd9 100644
--- a/debug/control/control-layers.html
+++ b/debug/map/controls.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>
@@ -15,23 +15,24 @@
 	<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});
-		
+			cloudmade2 = new L.TileLayer(getCloudMadeUrl(998), {attribution: 'Hello world'});
+
 		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);
-		
+		marker.bindPopup("Hello World").openPopup();
+
 		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
@@ -39,9 +40,10 @@
 			'Some marker': marker,
 			'Another marker': marker2
 		});
-		
+
 		map.addControl(layersControl);
-		
+		map.addControl(new L.Control.Scale());
+
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/debug/map/geolocation.html b/debug/map/geolocation.html
new file mode 100644
index 0000000..7f5b084
--- /dev/null
+++ b/debug/map/geolocation.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Leaflet geolocation debug page</title>
+
+    <link rel="stylesheet" href="../../dist/leaflet.css" />
+    <!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
+
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="stylesheet" href="../css/screen.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', {zoom: 15, layers: [cloudmade]});
+
+        function logEvent(e) { console.log(e.type); }
+        map.on('locationerror', logEvent);
+        map.on('locationfound', logEvent);
+
+        map.locate({setView: true});
+
+    </script>
+</body>
+</html>
diff --git a/debug/map/map-mobile.html b/debug/map/map-mobile.html
index 27d12ed..df516ec 100644
--- a/debug/map/map-mobile.html
+++ b/debug/map/map-mobile.html
@@ -3,13 +3,13 @@
 <head>
 	<title>Leaflet debug page</title>
 
-	<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
-	
+	<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>
@@ -20,23 +20,15 @@
 
 		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();
+			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 marker = new L.Marker(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>");
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/debug/map/map.html b/debug/map/map.html
index 0acc769..0b3cc1b 100644
--- a/debug/map/map.html
+++ b/debug/map/map.html
@@ -6,13 +6,15 @@
 	<link rel="stylesheet" href="../../dist/leaflet.css" />
 	<!--[if lte IE 8]><link rel="stylesheet" href="../../dist/leaflet.ie.css" /><![endif]-->
 
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
 	<link rel="stylesheet" href="../css/screen.css" />
 
 	<script src="../leaflet-include.js"></script>
 </head>
 <body>
 
-	<div id="map" style="width: 600px; height: 600px; border: 1px solid #ccc"></div>
+	<div id="map"></div>
 	<button id="populate">Populate with 10 markers</button>
 
 	<script type="text/javascript">
@@ -24,6 +26,8 @@
 
 		var map = new L.Map('map', {center: latlng, zoom: 15, layers: [cloudmade]});
 
+		//map.on('click', function () { alert('hi'); });
+
 		var markers = new L.FeatureGroup();
 
 		function populate() {
diff --git a/debug/vector/bounds-extend.html b/debug/vector/bounds-extend.html
new file mode 100644
index 0000000..d9f7ef6
--- /dev/null
+++ b/debug/vector/bounds-extend.html
@@ -0,0 +1,88 @@
+<!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="boundsExtendBounds();">Extend the bounds of the center rectangle with the upper right rectangle</button>
+    <button onclick="boundsExtendLatLng()">Extend the bounds of the center rectangle with the lower left marker</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 latLng = new L.LatLng(54.18815548107151, -7.657470703124999);
+
+		var bounds1 = new L.LatLngBounds(new L.LatLng(54.559322, -5.767822), new L.LatLng(56.1210604, -3.021240));
+        var bounds2 = new L.LatLngBounds(new L.LatLng(56.56023925701561, -2.076416015625), new L.LatLng(57.01158038001565, -0.9777832031250001));
+        var bounds3;
+
+    	var map = new L.Map('map', {
+		    layers: [cloudmade],
+		    center: bounds1.getCenter(),
+		    zoom: 7
+		});
+
+        var rectangle1 = new L.Rectangle(bounds1);
+        var rectangle2 = new L.Rectangle(bounds2);
+        var rectangle3;
+
+        var marker = new L.Marker(latLng);
+
+    	map.addLayer(rectangle1).addLayer(rectangle2).addLayer(marker);
+        
+        
+        
+        
+
+        function boundsExtendBounds() {
+        	if  (rectangle3) {
+        		map.removeLayer(rectangle3);
+        		rectangle3 = null;
+        	}
+        	if (bounds3) {
+        		bounds3 = null;
+        	}
+        	bounds3 = new L.LatLngBounds(bounds1.getSouthWest(), bounds1.getNorthEast());
+        	bounds3.extend(bounds2);
+        	rectangle3 = new L.Rectangle(bounds3, {
+	            color: "#ff0000",
+	            weight: 1,
+	            opacity: 1,
+	            fillOpacity: 0
+	        });
+	        
+	        map.addLayer(rectangle3);
+        }
+
+        function boundsExtendLatLng() {
+        	if  (rectangle3) {
+        		map.removeLayer(rectangle3);
+        		rectangle3 = null;
+        	}
+        	if (bounds3) {
+        		bounds3 = null;
+        	}
+        	bounds3 = new L.LatLngBounds(bounds1.getSouthWest(), bounds1.getNorthEast());
+        	bounds3.extend(marker.getLatLng());
+        	rectangle3 = new L.Rectangle(bounds3, {
+	            color: "#ff0000",
+	            weight: 1,
+	            opacity: 1,
+	            fillOpacity: 0
+	        });
+	        
+	        map.addLayer(rectangle3);
+        }
+        
+	</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/debug/vector/editable.html b/debug/vector/editable.html
index dc7cb97..7ab77fb 100644
--- a/debug/vector/editable.html
+++ b/debug/vector/editable.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>
@@ -17,27 +17,35 @@
 	<script>
 		var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
 			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18}),
-			map = new L.Map('map', {layers: [cloudmade], center: new L.LatLng(50.5, 30.5), zoom: 15});
-	
-		
-		var latlngs = [];
-		latlngs.push(getRandomLatLng(map));
-		latlngs.push(getRandomLatLng(map));
-		latlngs.push(getRandomLatLng(map));
-		
-		var path = new L.Polygon(latlngs);
-
-		console.log(latlngs);
-		
-		var marker = new L.Marker(latlngs[0], {draggable: true});
-		map.addLayer(marker);
-		
-		marker.on('drag', function() {
-			latlngs[0] = marker.getLatLng();
-			path.setLatLngs(latlngs);
-		});
-		
-		map.addLayer(path);
+			map = new L.Map('map', {layers: [cloudmade], center: new L.LatLng(51.505, -0.04), zoom: 13});
+
+
+		var polygon = new L.Polygon([
+			new L.LatLng(51.51, -0.1),
+			new L.LatLng(51.5, -0.06),
+			new L.LatLng(51.52, -0.03)
+		]);
+
+		polygon.editing.enable();
+
+		map.addLayer(polygon);
+
+		var polyline = new L.Polyline([
+			new L.LatLng(51.49, -0.02),
+			new L.LatLng(51.51, 0),
+			new L.LatLng(51.52, -0.02)
+		]);
+
+		polyline.editing.enable();
+
+		map.addLayer(polyline);
+
+                polygon.on('edit', function() {
+                    console.log('Polygon was edited!');
+                });
+                polyline.on('edit', function() {
+                    console.log('Polyline was edited!');
+                });
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/debug/geojson/geojson.html b/debug/vector/feature-group-bounds.html
similarity index 50%
copy from debug/geojson/geojson.html
copy to debug/vector/feature-group-bounds.html
index 3bdd7f9..527fd05 100644
--- a/debug/geojson/geojson.html
+++ b/debug/vector/feature-group-bounds.html
@@ -13,14 +13,17 @@
 <body>
 
 	<div id="map" style="width: 600px; height: 600px; border: 1px solid #ccc"></div>
-	<button id="populate">Populate with 10 markers</button>
+	<button onclick="geojsonLayerBounds();">Show GeoJSON layer bounds</button>
+	<button onclick="featureGroupBounds();">Show feature group bounds</button>
 
 	<script type="text/javascript" src="geojson-sample.js"></script>
 	<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});
+			cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution}),
+			rectangle,
+			featureGroup;
 
 		var map = new L.Map('map', {
 			center: new L.LatLng(0.78, 102.37),
@@ -49,8 +52,43 @@
 		});
 
 		geojson.addGeoJSON(geojsonSample);
+		geojson.addLayer(new L.Marker(new L.LatLng(2.745530718801952, 105.194091796875)))
+
+		var eye1 = new L.Marker(new L.LatLng(-0.7250783020332547, 101.8212890625));
+		var eye2 = new L.Marker(new L.LatLng(-0.7360637370492077, 103.2275390625));
+		var nose = new L.Marker(new L.LatLng(-1.3292264529974207, 102.5463867187));
+		var mouth = new L.Polyline([
+			new L.LatLng(-1.3841426927920029, 101.7333984375),
+			new L.LatLng(-1.6037944300589726, 101.964111328125),
+			new L.LatLng(-1.6806671337507222, 102.249755859375),
+			new L.LatLng(-1.7355743631421197, 102.67822265625),
+			new L.LatLng(-1.5928123762763, 103.0078125),
+			new L.LatLng(-1.3292264529974207, 103.3154296875)
+		]);
+		map.addLayer(eye1).addLayer(eye2).addLayer(nose).addLayer(mouth);
+		featureGroup = new L.FeatureGroup([eye1, eye2, nose, mouth]);
 
 		map.addLayer(geojson);
+		map.addLayer(featureGroup);
+
+		function geojsonLayerBounds() {
+			if (rectangle) {
+				rectangle.setBounds(geojson.getBounds());
+			} else {
+				rectangle = new L.Rectangle(geojson.getBounds());
+				map.addLayer(rectangle);
+			}
+		}
+
+		function featureGroupBounds() {
+			if (rectangle) {
+				rectangle.setBounds(featureGroup.getBounds());
+			} else {
+				rectangle = new L.Rectangle(featureGroup.getBounds());
+				map.addLayer(rectangle);
+			}
+
+		}
 	</script>
 </body>
 </html>
diff --git a/debug/geojson/geojson-sample.js b/debug/vector/geojson-sample.js
similarity index 100%
rename from debug/geojson/geojson-sample.js
rename to debug/vector/geojson-sample.js
diff --git a/debug/geojson/geojson.html b/debug/vector/geojson.html
similarity index 100%
rename from debug/geojson/geojson.html
rename to debug/vector/geojson.html
diff --git a/debug/vector/rectangle.html b/debug/vector/rectangle.html
new file mode 100644
index 0000000..2e508ef
--- /dev/null
+++ b/debug/vector/rectangle.html
@@ -0,0 +1,53 @@
+<!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]-->
+	
+	<script src="../leaflet-include.js"></script>
+</head>
+<body>
+
+	<div id="map" style="width: 500px; height: 500px;"></div>
+	<input type="button" value="Set blue rectangle bounds as current map extent." onclick="resetBounds();" />
+
+	<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 bounds = new L.LatLngBounds(new L.LatLng(54.559322, -5.767822), new L.LatLng(56.1210604, -3.021240));
+		var bounds2 = new L.LatLngBounds(new L.LatLng(56.2124322195806, -3.427734375), new L.LatLng(56.307776937156945, -3.2560729980468746));
+
+		var rectangle = new L.Rectangle(bounds);
+		var styledRectangle = new L.Rectangle(bounds2, {
+			fillColor: "#ff7800",
+			color: "#000000",
+			opacity: 1,
+			weight: 2
+		});
+
+		rectangle.on("click", function () {
+			alert("you clicked a rectangle.")
+		});
+	
+		var map = new L.Map('map', {
+			center: bounds.getCenter(),
+			zoom: 7,
+			layers: [cloudmade]
+		});
+
+		map.addLayer(rectangle).addLayer(styledRectangle);
+
+		function resetBounds() {
+			rectangle.setBounds(map.getBounds());
+		}
+
+	</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/debug/vector/touchzoomemu.html b/debug/vector/touchzoomemu.html
new file mode 100644
index 0000000..b6ed0dd
--- /dev/null
+++ b/debug/vector/touchzoomemu.html
@@ -0,0 +1,121 @@
+<!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>
+	<div style="background-color:chartreuse; width: 100px; height:100px; position: absolute; left: 850px; top: 10px" onclick="Hack1()">Hack1Touch</div>
+	<div style="background-color:coral; width: 100px; height:100px; position: absolute; left: 850px; top: 120px" onclick="Hack2()">Hack2Touch</div>
+	<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}),
+			map = new L.Map('map', {layers: [cloudmade], center: new L.LatLng(51.505, -0.04), zoom: 13});
+
+
+		var polygon = new L.Polygon([
+			new L.LatLng(51.51, -0.1),
+			new L.LatLng(51.5, -0.06),
+			new L.LatLng(51.52, -0.03)
+		]);
+
+		polygon.editing.enable();
+
+		map.addLayer(polygon);
+
+		var polyline = new L.Polyline([
+			new L.LatLng(51.49, -0.02),
+			new L.LatLng(51.51, 0),
+			new L.LatLng(51.52, -0.02)
+		]);
+
+		polyline.editing.enable();
+
+		map.addLayer(polyline);
+
+		polygon.on('edit', function() {
+			console.log('Polygon was edited!');
+		});
+		polyline.on('edit', function() {
+			console.log('Polyline was edited!');
+		});
+
+		var _timerAt, _timer;
+		function Hack1() {
+			_timerAt = 0;
+			clearInterval(_timer);
+			_timer = setInterval(Hack1Timer, 1000);
+		}
+		function Hack1Timer() {
+			switch (_timerAt) {
+				case 0:
+					map.touchZoom._onTouchStart({ touches: [{ pageX: 405, pageY: 312 }, { pageX: 233, pageY: 321 }] });
+					break;
+				case 1:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 412, pageY: 312 }, { pageX: 236, pageY: 322 }] });
+					break;
+				case 2:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 423, pageY: 313 }, { pageX: 243, pageY: 321 }] });
+					break;
+				case 3:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 476, pageY: 326 }, { pageX: 299, pageY: 321 }] });
+					break;
+				case 4:
+					map.touchZoom._onTouchEnd();
+					break;
+				case 5:
+					clearInterval(_timer);
+					break;
+			}
+			_timerAt++;
+		}
+		function Hack2() {
+			map.touchZoom._onTouchStart({ touches: [{ pageX: 405, pageY: 312 }, { pageX: 233, pageY: 321 }] });
+			map.touchZoom._onTouchMove({ touches: [{ pageX: 476, pageY: 326 }, { pageX: 299, pageY: 321 }] });
+			//_timerAt = 0;
+			//clearInterval(_timer);
+			//_timer = setInterval(Hack2Timer, 100);
+		}
+		function Hack2Timer() {
+			switch (_timerAt) {
+				case 0:
+					map.touchZoom._onTouchStart({ touches: [{ pageX: 100, pageY: 100 }, { pageX: 50, pageY: 100}] });
+					break;
+				case 1:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 100, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 2:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 110, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 3:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 120, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 4:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 130, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 5:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 140, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 6:
+					map.touchZoom._onTouchMove({ touches: [{ pageX: 150, pageY: 100 }, { pageX: 50, pageY: 100 }] });
+					break;
+				case 7:
+					map.touchZoom._onTouchEnd();
+					break;
+				case 8:
+					clearInterval(_timer);
+					break;
+			}
+			_timerAt++;
+		}
+	</script>
+</body>
+</html>
diff --git a/debug/vector/vector-simple.html b/debug/vector/vector-simple.html
index a9d77d5..df6923d 100644
--- a/debug/vector/vector-simple.html
+++ b/debug/vector/vector-simple.html
@@ -17,14 +17,14 @@
 
 	<script>
 
-		var map = new L.Map('map', {fadeAnimation: false});
+		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
-			});
+		var cloudmade = new L.TileLayer('http://{s}.tile.cloudmade.com/{key}/{styleId}/256/{z}/{x}/{y}.png', {
+			maxZoom: 18,
+			attribution: 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
+			key: 'BC9A493B41014CAABB98F0471D759707',
+			styleId: 997
+		});
 
 		map.setView(new L.LatLng(51.505, -0.09), 13).addLayer(cloudmade);
 
@@ -60,4 +60,4 @@
 		map.addLayer(polygon);
 	</script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/dist/images/marker.png b/dist/images/marker-icon.png
similarity index 100%
rename from dist/images/marker.png
rename to dist/images/marker-icon.png
diff --git a/dist/leaflet-src.js b/dist/leaflet-src.js
new file mode 100644
index 0000000..9be37c8
--- /dev/null
+++ b/dist/leaflet-src.js
@@ -0,0 +1,6664 @@
+/*
+ Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin
+ Leaflet is a modern open-source JavaScript library for interactive maps.
+ http://leaflet.cloudmade.com
+*/
+(function () {
+
+var L, originalL;
+
+if (typeof exports !== 'undefined') {
+	L = exports;
+} else {
+	L = {};
+	
+	originalL = window.L;
+
+	L.noConflict = function () {
+		window.L = originalL;
+		return L;
+	};
+
+	window.L = L;
+}
+
+L.version = '0.4';
+
+
+/*
+ * L.Util is a namespace for various utility functions.
+ */
+
+L.Util = {
+	extend: function (/*Object*/ dest) /*-> Object*/ {	// merge src properties into dest
+		var sources = Array.prototype.slice.call(arguments, 1);
+		for (var j = 0, len = sources.length, src; j < len; j++) {
+			src = sources[j] || {};
+			for (var i in src) {
+				if (src.hasOwnProperty(i)) {
+					dest[i] = src[i];
+				}
+			}
+		}
+		return dest;
+	},
+
+	bind: function (fn, obj) { // (Function, Object) -> Function
+		var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null;
+		return function () {
+			return fn.apply(obj, args || arguments);
+		};
+	},
+
+	stamp: (function () {
+		var lastId = 0, key = '_leaflet_id';
+		return function (/*Object*/ obj) {
+			obj[key] = obj[key] || ++lastId;
+			return obj[key];
+		};
+	}()),
+
+
+	// TODO refactor: remove repetition
+
+	requestAnimFrame: (function () {
+		function timeoutDefer(callback) {
+			window.setTimeout(callback, 1000 / 60);
+		}
+
+		var requestFn = window.requestAnimationFrame ||
+			window.webkitRequestAnimationFrame ||
+			window.mozRequestAnimationFrame ||
+			window.oRequestAnimationFrame ||
+			window.msRequestAnimationFrame ||
+			timeoutDefer;
+
+		return function (callback, context, immediate, contextEl) {
+			callback = context ? L.Util.bind(callback, context) : callback;
+			if (immediate && requestFn === timeoutDefer) {
+				callback();
+			} else {
+				return requestFn.call(window, callback, contextEl);
+			}
+		};
+	}()),
+
+	cancelAnimFrame: (function () {
+		var requestFn = window.cancelAnimationFrame ||
+			window.webkitCancelRequestAnimationFrame ||
+			window.mozCancelRequestAnimationFrame ||
+			window.oCancelRequestAnimationFrame ||
+			window.msCancelRequestAnimationFrame ||
+			clearTimeout;
+
+		return function (handle) {
+			if (!handle) { return; }
+			return requestFn.call(window, handle);
+		};
+	}()),
+
+	limitExecByInterval: function (fn, time, context) {
+		var lock, execOnUnlock;
+		
+		return function wrapperFn() {
+			var args = arguments;
+
+			if (lock) {
+				execOnUnlock = true;
+				return;
+			}
+
+			lock = true;
+			
+			setTimeout(function () {
+				lock = false;
+				
+				if (execOnUnlock) {
+					wrapperFn.apply(context, args);
+					execOnUnlock = false;
+				}
+			}, time);
+
+			fn.apply(context, args);
+		};
+	},
+
+	falseFn: function () {
+		return false;
+	},
+
+	formatNum: function (num, digits) {
+		var pow = Math.pow(10, digits || 5);
+		return Math.round(num * pow) / pow;
+	},
+
+	setOptions: function (obj, options) {
+		obj.options = L.Util.extend({}, obj.options, options);
+		return obj.options;
+	},
+
+	getParamString: function (obj) {
+		var params = [];
+		for (var i in obj) {
+			if (obj.hasOwnProperty(i)) {
+				params.push(i + '=' + obj[i]);
+			}
+		}
+		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;
+		});
+	},
+
+	emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
+};
+
+
+/*
+ * Class powers the OOP facilities of the library. Thanks to John Resig and Dean Edwards for inspiration!
+ */
+
+L.Class = function () {};
+
+L.Class.extend = function (/*Object*/ props) /*-> Class*/ {
+
+	// extended class with the new prototype
+	var NewClass = function () {
+		if (this.initialize) {
+			this.initialize.apply(this, arguments);
+		}
+	};
+
+	// instantiate class without calling constructor
+	var F = function () {};
+	F.prototype = this.prototype;
+
+	var proto = new F();
+	proto.constructor = NewClass;
+
+	NewClass.prototype = proto;
+
+	//inherit parent's statics
+	for (var i in this) {
+		if (this.hasOwnProperty(i) && i !== 'prototype') {
+			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);
+	}
+
+	// mix given properties into the prototype
+	L.Util.extend(proto, props);
+
+	return NewClass;
+};
+
+
+// method for adding properties to prototype
+L.Class.include = function (props) {
+	L.Util.extend(this.prototype, props);
+};
+
+L.Class.mergeOptions = function (options) {
+	L.Util.extend(this.prototype.options, options);
+};
+
+/*
+ * L.Mixin.Events adds custom events functionality to Leaflet classes
+ */
+
+L.Mixin = {};
+
+L.Mixin.Events = {
+	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 || this
+		});
+		return this;
+	},
+
+	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;
+		}
+
+		for (var i = 0, events = this._leaflet_events, len = events[type].length; i < len; i++) {
+			if (
+				(events[type][i].action === fn) &&
+				(!context || (events[type][i].context === context))
+			) {
+				events[type].splice(i, 1);
+				return this;
+			}
+		}
+		return this;
+	},
+
+	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;
+
+
+(function () {
+	var ua = navigator.userAgent.toLowerCase(),
+		ie = !!window.ActiveXObject,
+		webkit = ua.indexOf("webkit") !== -1,
+		gecko = ua.indexOf("gecko") !== -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 window.WebKitCSSMatrix()),
+
+		gecko: gecko,
+		gecko3d: gecko && ('MozPerspective' in document.createElement('div').style),
+
+		opera: opera,
+		opera3d: opera && ('OTransition' in document.createElement('div').style),
+
+		android: android,
+		mobileWebkit: mobile && webkit,
+		mobileOpera: mobile && opera,
+
+		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;
+		}())
+	};
+	L.Browser.any3d = !!L.Browser.webkit3d || !!L.Browser.gecko3d || !!L.Browser.opera3d;
+		
+}());
+
+
+/*
+ * L.Point represents a point with x and y coordinates.
+ */
+
+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) {
+		return this.clone()._add(point);
+	},
+
+	_add: function (point) {
+		this.x += point.x;
+		this.y += point.y;
+		return this;
+	},
+
+	subtract: function (point) {
+		return this.clone()._subtract(point);
+	},
+
+	// destructive subtract (faster)
+	_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);
+	},
+
+	multiplyBy: function (num) {
+		return new L.Point(this.x * num, this.y * num);
+	},
+
+	distanceTo: function (point) {
+		var x = point.x - this.x,
+			y = point.y - this.y;
+		return Math.sqrt(x * x + y * y);
+	},
+
+	round: function () {
+		return this.clone()._round();
+	},
+
+	// destructive round
+	_round: function () {
+		this.x = Math.round(this.x);
+		this.y = Math.round(this.y);
+		return this;
+	},
+
+	clone: function () {
+		return new L.Point(this.x, this.y);
+	},
+
+	toString: function () {
+		return 'Point(' +
+				L.Util.formatNum(this.x) + ', ' +
+				L.Util.formatNum(this.y) + ')';
+	}
+};
+
+
+/*
+ * L.Bounds represents a rectangular area on the screen in pixel coordinates.
+ */
+
+L.Bounds = L.Class.extend({
+	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]);
+		}
+	},
+
+	// extend the bounds to contain the given 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);
+		} else {
+			this.min.x = Math.min(point.x, this.min.x);
+			this.max.x = Math.max(point.x, this.max.x);
+			this.min.y = Math.min(point.y, this.min.y);
+			this.max.y = Math.max(point.y, this.max.y);
+		}
+	},
+
+	getCenter: function (round)/*->Point*/ {
+		return new L.Point(
+				(this.min.x + this.max.x) / 2,
+				(this.min.y + this.max.y) / 2, round);
+	},
+
+	contains: function (/*Bounds or Point*/ obj)/*->Boolean*/ {
+		var min, max;
+
+		if (obj instanceof L.Bounds) {
+			min = obj.min;
+			max = obj.max;
+		} else {
+			min = max = obj;
+		}
+
+		return (min.x >= this.min.x) &&
+				(max.x <= this.max.x) &&
+				(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;
+	}
+
+});
+
+
+/*
+ * 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) {
+		this._a = a;
+		this._b = b;
+		this._c = c;
+		this._d = d;
+	},
+
+	transform: function (point, scale) {
+		return this._transform(point.clone(), scale);
+	},
+
+	// destructive transform (faster)
+	_transform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
+		scale = scale || 1;
+		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*/ {
+		scale = scale || 1;
+		return new L.Point(
+			(point.x / scale - this._b) / this._a,
+			(point.y / scale - this._d) / this._c);
+	}
+});
+
+
+/*
+ * L.DomUtil contains various utility functions for working with DOM
+ */
+
+L.DomUtil = {
+	get: function (id) {
+		return (typeof id === 'string' ? document.getElementById(id) : id);
+	},
+
+	getStyle: function (el, style) {
+		var value = el.style[style];
+		if (!value && el.currentStyle) {
+			value = el.currentStyle[style];
+		}
+		if (!value || value === 'auto') {
+			var css = document.defaultView.getComputedStyle(el, null);
+			value = css ? css[style] : null;
+		}
+		return (value === 'auto' ? null : value);
+	},
+
+	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;
+			}
+			if (L.DomUtil.getStyle(el, 'position') === 'fixed') {
+				top += docBody.scrollTop || 0;
+				left += docBody.scrollLeft || 0;
+				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) {
+		var el = document.createElement(tagName);
+		el.className = className;
+		if (container) {
+			container.appendChild(el);
+		}
+		return el;
+	},
+
+	disableTextSelection: function () {
+		if (document.selection && document.selection.empty) {
+			document.selection.empty();
+		}
+		if (!this._onselectstart) {
+			this._onselectstart = document.onselectstart;
+			document.onselectstart = L.Util.falseFn;
+		}
+	},
+
+	enableTextSelection: function () {
+		document.onselectstart = this._onselectstart;
+		this._onselectstart = null;
+	},
+
+	hasClass: function (el, name) {
+		return (el.className.length > 0) &&
+				new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);
+	},
+
+	addClass: function (el, name) {
+		if (!L.DomUtil.hasClass(el, name)) {
+			el.className += (el.className ? ' ' : '') + name;
+		}
+	},
+
+	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 += value !== 1 ? 'alpha(opacity=' + Math.round(value * 100) + ')' : '';
+		} else {
+			el.style.opacity = value;
+		}
+	},
+
+	//TODO refactor away this ugly translate/position mess
+
+	testProp: function (props) {
+		var style = document.documentElement.style;
+
+		for (var i = 0; i < props.length; i++) {
+			if (props[i] in style) {
+				return props[i];
+			}
+		}
+		return false;
+	},
+
+	getTranslateString: function (point) {
+		return L.DomUtil.TRANSLATE_OPEN +
+				point.x + 'px,' + point.y + 'px' +
+				L.DomUtil.TRANSLATE_CLOSE;
+	},
+
+	getScaleString: function (scale, origin) {
+		var preTranslateStr = L.DomUtil.getTranslateString(origin),
+			scaleStr = ' scale(' + scale + ') ',
+			postTranslateStr = L.DomUtil.getTranslateString(origin.multiplyBy(-1));
+
+		return preTranslateStr + scaleStr + postTranslateStr;
+	},
+
+	setPosition: function (el, point) {
+		el._leaflet_pos = point;
+		if (L.Browser.any3d) {
+			el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
+			el.style[L.DomUtil.BACKFACEVISIBILITY] = 'hidden';
+		} else {
+			el.style.left = point.x + 'px';
+			el.style.top = point.y + 'px';
+		}
+	},
+
+	getPosition: function (el) {
+		return el._leaflet_pos;
+	}
+};
+
+L.Util.extend(L.DomUtil, {
+	TRANSITION: L.DomUtil.testProp(['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']),
+	TRANSFORM: L.DomUtil.testProp(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']),
+	BACKFACEVISIBILITY: L.DomUtil.testProp(['backfaceVisibility', 'WebkitBackfaceVisibility', 'OBackfaceVisibility', 'MozBackfaceVisibility', 'msBackfaceVisibility']),
+
+	TRANSLATE_OPEN: 'translate' + (L.Browser.webkit3d ? '3d(' : '('),
+	TRANSLATE_CLOSE: L.Browser.webkit3d ? ',0)' : ')'
+});
+
+
+/*
+	CM.LatLng represents a geographical point with latitude and longtitude coordinates.
+*/
+
+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 || 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,
+	MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check
+});
+
+L.LatLng.prototype = {
+	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) + ', ' +
+				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));
+	}
+};
+
+
+/*
+ * L.LatLngBounds represents a rectangular area on the map in geographical coordinates.
+ */
+
+L.LatLngBounds = L.Class.extend({
+	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 or bounds
+	extend: function (/*LatLng or LatLngBounds*/ obj) {
+		if (obj instanceof L.LatLng) {
+			if (!this._southWest && !this._northEast) {
+				this._southWest = new L.LatLng(obj.lat, obj.lng, true);
+				this._northEast = new L.LatLng(obj.lat, obj.lng, true);
+			} else {
+				this._southWest.lat = Math.min(obj.lat, this._southWest.lat);
+				this._southWest.lng = Math.min(obj.lng, this._southWest.lng);
+				this._northEast.lat = Math.max(obj.lat, this._northEast.lat);
+				this._northEast.lng = Math.max(obj.lng, this._northEast.lng);
+			}
+		} else if (obj instanceof L.LatLngBounds) {
+			this.extend(obj._southWest);
+            this.extend(obj._northEast);
+		}
+		return this;
+	},
+
+	// extend the bounds by a percentage
+	pad: function (bufferRatio) { // (Number) -> LatLngBounds
+		var sw = this._southWest,
+			ne = this._northEast,
+			heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
+			widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
+
+		return new L.LatLngBounds(
+			new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
+			new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
+	},
+
+	getCenter: function () /*-> LatLng*/ {
+		return new L.LatLng(
+				(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, 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(',');
+	},
+
+	equals: function (/*LatLngBounds*/ bounds) {
+		return bounds ? this._southWest.equals(bounds.getSouthWest()) &&
+		                this._northEast.equals(bounds.getNorthEast()) : false;
+	}
+});
+
+//TODO International date line?
+
+
+/*
+ * L.Projection contains various geographical projections used by CRS classes.
+ */
+
+L.Projection = {};
+
+
+
+L.Projection.SphericalMercator = {
+	MAX_LATITUDE: 85.0511287798,
+
+	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)));
+
+		return new L.Point(x, y);
+	},
+
+	unproject: function (point, unbounded) { // (Point, Boolean) -> LatLng
+		var d = L.LatLng.RAD_TO_DEG,
+			lng = point.x * d,
+			lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;
+
+		return new L.LatLng(lat, lng, unbounded);
+	}
+};
+
+
+
+L.Projection.LonLat = {
+	project: function (latlng) {
+		return new L.Point(latlng.lng, latlng.lat);
+	},
+
+	unproject: function (point, unbounded) {
+		return new L.LatLng(point.y, point.x, unbounded);
+	}
+};
+
+
+
+L.CRS = {
+	latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point
+		var projectedPoint = this.projection.project(latlng),
+		    scale = this.scale(zoom);
+
+		return this.transformation._transform(projectedPoint, scale);
+	},
+
+	pointToLatLng: function (point, zoom, unbounded) { // (Point, Number[, Boolean]) -> LatLng
+		var scale = this.scale(zoom),
+		    untransformedPoint = this.transformation.untransform(point, scale);
+
+		return this.projection.unproject(untransformedPoint, unbounded);
+		//TODO get rid of 'unbounded' everywhere
+	},
+
+	project: function (latlng) {
+		return this.projection.project(latlng);
+	},
+
+	scale: function (zoom) {
+		return 256 * Math.pow(2, zoom);
+	}
+};
+
+
+
+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
+		var projectedPoint = this.projection.project(latlng),
+			earthRadius = 6378137;
+		return projectedPoint.multiplyBy(earthRadius);
+	}
+});
+
+L.CRS.EPSG900913 = L.Util.extend({}, L.CRS.EPSG3857, {
+	code: 'EPSG:900913'
+});
+
+
+
+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)
+});
+
+
+/*
+ * L.Map is the central class of the API - it is used to create a map.
+ */
+
+L.Map = L.Class.extend({
+	includes: L.Mixin.Events,
+
+	options: {
+		crs: L.CRS.EPSG3857,
+
+		/*
+		center: LatLng,
+		zoom: Number,
+		layers: Array,
+		*/
+
+		fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android,
+		trackResize: true,
+		animateMarkerZoom: true
+	},
+
+	initialize: function (id, options) { // (HTMLElement or String, Object)
+		options = L.Util.setOptions(this, options);
+
+		this._initContainer(id);
+		this._initLayout();
+		this._initHooks();
+		this._initEvents();
+
+		if (options.maxBounds) {
+			this.setMaxBounds(options.maxBounds);
+		}
+
+		if (options.center && typeof options.zoom !== 'undefined') {
+			this.setView(options.center, options.zoom, true);
+		}
+
+		this._initLayers(options.layers);
+	},
+
+
+	// public methods that modify map state
+
+	// replaced by animation-powered implementation in Map.PanAnimation.js
+	setView: function (center, zoom) {
+		this._resetView(center, this._limitZoom(zoom));
+		return this;
+	},
+
+	setZoom: function (zoom) { // (Number)
+		return this.setView(this.getCenter(), zoom);
+	},
+
+	zoomIn: function () {
+		return this.setZoom(this._zoom + 1);
+	},
+
+	zoomOut: function () {
+		return this.setZoom(this._zoom - 1);
+	},
+
+	fitBounds: function (bounds) { // (LatLngBounds)
+		var zoom = this.getBoundsZoom(bounds);
+		return this.setView(bounds.getCenter(), zoom);
+	},
+
+	fitWorld: function () {
+		var sw = new L.LatLng(-60, -170),
+		    ne = new L.LatLng(85, 179);
+
+		return this.fitBounds(new L.LatLngBounds(sw, ne));
+	},
+
+	panTo: function (center) { // (LatLng)
+		return this.setView(center, this._zoom);
+	},
+
+	panBy: function (offset) { // (Point)
+		// replaced with animated panBy in Map.Animation.js
+		this.fire('movestart');
+
+		this._rawPanBy(offset);
+
+		this.fire('move');
+		return this.fire('moveend');
+	},
+
+	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, insertAtTheBottom) {
+		// TODO method is too big, refactor
+
+		var id = L.Util.stamp(layer);
+
+		if (this._layers[id]) { return this; }
+
+		this._layers[id] = layer;
+
+		// TODO getMaxZoom, getMinZoom in ILayer (instead of options)
+		if (layer.options && !isNaN(layer.options.maxZoom)) {
+			this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);
+		}
+		if (layer.options && !isNaN(layer.options.minZoom)) {
+			this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);
+		}
+
+		// TODO looks ugly, refactor!!!
+		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
+			this._tileLayersNum++;
+            this._tileLayersToLoad++;
+            layer.on('load', this._onTileLayerLoad, this);
+		}
+
+		var onMapLoad = function () {
+			layer.onAdd(this, insertAtTheBottom);
+			this.fire('layeradd', {layer: layer});
+		};
+
+		if (this._loaded) {
+			onMapLoad.call(this);
+		} else {
+			this.on('load', onMapLoad, this);
+		}
+
+		return this;
+	},
+
+	removeLayer: function (layer) {
+		var id = L.Util.stamp(layer);
+
+		if (!this._layers[id]) { return; }
+
+		layer.onRemove(this);
+
+		delete this._layers[id];
+
+		// TODO looks ugly, refactor
+		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
+			this._tileLayersNum--;
+            this._tileLayersToLoad--;
+            layer.off('load', this._onTileLayerLoad, this);
+		}
+
+		return this.fire('layerremove', {layer: layer});
+	},
+
+	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; }
+
+		var offset = oldSize.subtract(this.getSize()).divideBy(2, true);
+		this._rawPanBy(offset);
+
+		this.fire('move');
+
+		clearTimeout(this._sizeTimer);
+		this._sizeTimer = setTimeout(L.Util.bind(this.fire, this, 'moveend'), 200);
+
+		return this;
+	},
+
+	// TODO handler.addTo
+	addHandler: function (name, HandlerClass) {
+		if (!HandlerClass) { return; }
+
+		this[name] = new HandlerClass(this);
+
+		if (this.options[name]) {
+			this[name].enable();
+		}
+
+		return this;
+	},
+
+
+	// public methods for getting map state
+
+	getCenter: function (unbounded) { // (Boolean) -> LatLng
+		var viewHalf = this.getSize().divideBy(2),
+		    centerPoint = this._getTopLeftPoint().add(viewHalf);
+
+		return this.unproject(centerPoint, this._zoom, unbounded);
+	},
+
+	getZoom: function () {
+		return this._zoom;
+	},
+
+	getBounds: function () {
+		var bounds = this.getPixelBounds(),
+		    sw = this.unproject(new L.Point(bounds.min.x, bounds.max.y), 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 () {
+		var z1 = this.options.minZoom || 0,
+		    z2 = this._layersMinZoom || 0,
+		    z3 = this._boundsMinZoom || 0;
+
+		return Math.max(z1, z2, z3);
+	},
+
+	getMaxZoom: function () {
+		var z1 = typeof this.options.maxZoom === 'undefined' ? Infinity : this.options.maxZoom,
+		    z2 = typeof this._layersMaxZoom  === 'undefined' ? Infinity : this._layersMaxZoom;
+
+		return Math.min(z1, z2);
+	},
+
+	getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number
+		var size = this.getSize(),
+		    zoom = this.options.minZoom || 0,
+		    maxZoom = this.getMaxZoom(),
+		    ne = bounds.getNorthEast(),
+		    sw = bounds.getSouthWest(),
+		    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);
+
+			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 () {
+		if (!this._size || this._sizeChanged) {
+			this._size = new L.Point(
+				this._container.clientWidth,
+				this._container.clientHeight);
+
+			this._sizeChanged = false;
+		}
+		return this._size;
+	},
+
+	getPixelBounds: function () {
+		var topLeftPoint = this._getTopLeftPoint();
+		return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
+	},
+
+	getPixelOrigin: function () {
+		return this._initialTopLeftPoint;
+	},
+
+	getPanes: function () {
+		return this._panes;
+	},
+	
+	getContainer: function () {
+		return this._container;
+	},
+
+
+	// conversion methods
+
+	mouseEventToContainerPoint: function (e) { // (MouseEvent)
+		return L.DomEvent.getMousePosition(e, this._container);
+	},
+
+	mouseEventToLayerPoint: function (e) { // (MouseEvent)
+		return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
+	},
+
+	mouseEventToLatLng: function (e) { // (MouseEvent)
+		return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
+	},
+
+	containerPointToLayerPoint: function (point) { // (Point)
+		return point.subtract(L.DomUtil.getPosition(this._mapPane));
+	},
+
+	layerPointToContainerPoint: function (point) { // (Point)
+		return point.add(L.DomUtil.getPosition(this._mapPane));
+	},
+
+	layerPointToLatLng: function (point) { // (Point)
+		return this.unproject(point.add(this._initialTopLeftPoint));
+	},
+
+	latLngToLayerPoint: function (latlng) { // (LatLng)
+		return this.project(latlng)._round()._subtract(this._initialTopLeftPoint);
+	},
+
+	containerPointToLatLng: function (point) {
+		return this.layerPointToLatLng(this.containerPointToLayerPoint(point));
+	},
+
+	latLngToContainerPoint: function (latlng) {
+		return this.layerPointToContainerPoint(this.latLngToLayerPoint(latlng));
+	},
+
+	project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
+		zoom = typeof zoom === 'undefined' ? this._zoom : zoom;
+		return this.options.crs.latLngToPoint(latlng, zoom);
+	},
+
+	unproject: function (point, zoom, unbounded) { // (Point[, Number, Boolean]) -> LatLng
+		// TODO remove unbounded, making it true all the time?
+		zoom = typeof zoom === 'undefined' ? this._zoom : zoom;
+		return this.options.crs.pointToLatLng(point, zoom, unbounded);
+	},
+
+
+	// private methods that modify map state
+
+	_initContainer: function (id) {
+		var container = this._container = L.DomUtil.get(id);
+
+		if (container._leaflet) {
+			throw new Error("Map container is already initialized.");
+		}
+
+		container._leaflet = true;
+	},
+
+	_initLayout: function () {
+		var container = this._container;
+
+		container.innerHTML = '';
+		container.className += ' leaflet-container';
+
+		if (L.Browser.touch) {
+			container.className += ' leaflet-touch';
+		}
+
+		if (this.options.fadeAnimation) {
+			container.className += ' leaflet-fade-anim';
+		}
+
+		var position = L.DomUtil.getStyle(container, 'position');
+
+		if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
+			container.style.position = 'relative';
+		}
+
+		this._initPanes();
+
+		if (this._initControlPos) {
+			this._initControlPos();
+		}
+	},
+
+	_initPanes: function () {
+		var panes = this._panes = {};
+
+		this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);
+
+		this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);
+		this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);
+
+		panes.shadowPane = this._createPane('leaflet-shadow-pane');
+		panes.overlayPane = this._createPane('leaflet-overlay-pane');
+		panes.markerPane = this._createPane('leaflet-marker-pane');
+		panes.popupPane = this._createPane('leaflet-popup-pane');
+
+		if (!this.options.animateMarkerZoom) {
+			panes.markerPane.className += ' leaflet-zoom-hide';
+			panes.shadowPane.className += ' leaflet-zoom-hide';
+			panes.popupPane.className += ' leaflet-zoom-hide';
+		}
+	},
+
+	_createPane: function (className, container) {
+		return L.DomUtil.create('div', className, container || this._objectsPane);
+	},
+
+	_initializers: [],
+
+	_initHooks: function () {
+		var i, len;
+		for (i = 0, len = this._initializers.length; i < len; i++) {
+			this._initializers[i].call(this);
+		}
+	},
+
+	_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 {
+			this._initialTopLeftPoint._add(L.DomUtil.getPosition(this._mapPane));
+		}
+
+		this._tileLayersToLoad = this._tileLayersNum;
+
+		this.fire('viewreset', {hard: !preserveMapOffset});
+
+		this.fire('move');
+
+		if (zoomChanged || afterZoomAnim) {
+			this.fire('zoomend');
+		}
+
+		this.fire('moveend');
+
+		if (!this._loaded) {
+			this._loaded = true;
+			this.fire('load');
+		}
+	},
+
+	_initLayers: function (layers) {
+		layers = layers ? (layers instanceof Array ? layers : [layers]) : [];
+
+		this._layers = {};
+		this._tileLayersNum = 0;
+
+		var i, len;
+
+		for (i = 0, len = layers.length; i < len; i++) {
+			this.addLayer(layers[i]);
+		}
+	},
+
+	_rawPanBy: function (offset) {
+		var newPos = L.DomUtil.getPosition(this._mapPane).subtract(offset);
+		L.DomUtil.setPosition(this._mapPane, newPos);
+	},
+
+
+	// map events
+
+	_initEvents: function () {
+		if (!L.DomEvent) { return; }
+
+		L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
+
+		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._onResize, this);
+		}
+	},
+
+	_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) {
+		if (!this._loaded) { return; }
+
+		var type = e.type;
+
+		type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
+
+		if (!this.hasEventListeners(type)) { return; }
+
+		if (type === 'contextmenu') {
+			L.DomEvent.preventDefault(e);
+		}
+
+		var containerPoint = this.mouseEventToContainerPoint(e),
+			layerPoint = this.containerPointToLayerPoint(containerPoint),
+			latlng = this.layerPointToLatLng(layerPoint);
+
+		this.fire(type, {
+			latlng: latlng,
+			layerPoint: layerPoint,
+			containerPoint: containerPoint,
+			originalEvent: e
+		});
+	},
+
+	_onTileLayerLoad: function () {
+		// TODO super-ugly, refactor!!!
+		// clear scaled tiles after all new tiles are loaded (for performance)
+		this._tileLayersToLoad--;
+		if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {
+			clearTimeout(this._clearTileBgTimer);
+			this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500);
+		}
+	},
+
+
+	// private methods for getting map state
+
+	_getTopLeftPoint: function () {
+		if (!this._loaded) {
+			throw new Error('Set map center and zoom first.');
+		}
+
+		var mapPanePos = L.DomUtil.getPosition(this._mapPane);
+		return this._initialTopLeftPoint.subtract(mapPanePos);
+	},
+
+	_getNewTopLeftPoint: function (center, zoom) {
+		var viewHalf = this.getSize().divideBy(2);
+		// TODO round on display, not calculation to increase precision?
+		return this.project(center, zoom)._subtract(viewHalf)._round();
+	},
+
+	_latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {
+		var mapPaneOffset = L.DomUtil.getPosition(this._mapPane),
+			topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(mapPaneOffset);
+
+		return this.project(latlng, newZoom)._round()._subtract(topLeft);
+	},
+
+	_limitZoom: function (zoom) {
+		var min = this.getMinZoom(),
+			max = this.getMaxZoom();
+
+		return Math.max(min, Math.min(max, zoom));
+	}
+});
+
+L.Map.addInitHook = function (fn) {
+	var args = Array.prototype.slice.call(arguments, 1);
+
+	var init = typeof fn === 'function' ? fn : function () {
+		this[fn].apply(this, args);
+	};
+
+	this.prototype._initializers.push(init);
+};
+
+
+L.Projection.Mercator = {
+	MAX_LATITUDE: 85.0840591556,
+
+	R_MINOR: 6356752.3142,
+	R_MAJOR: 6378137,
+
+	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 = r2 / r,
+			eccent = Math.sqrt(1.0 - tmp * tmp),
+			con = eccent * Math.sin(y);
+
+		con = Math.pow((1 - con) / (1 + con), eccent * 0.5);
+
+		var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con;
+		y = -r2 * Math.log(ts);
+
+		return new L.Point(x, y);
+	},
+
+	unproject: function (point, unbounded) { // (Point, Boolean) -> LatLng
+		var d = L.LatLng.RAD_TO_DEG,
+			r = this.R_MAJOR,
+			r2 = this.R_MINOR,
+			lng = point.x * d / r,
+			tmp = r2 / r,
+			eccent = Math.sqrt(1 - (tmp * tmp)),
+			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;
+			phi += dphi;
+		}
+
+		return new L.LatLng(phi * d, lng, unbounded);
+	}
+};
+
+
+
+L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, {
+	code: 'EPSG:3395',
+
+	projection: L.Projection.Mercator,
+
+	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);
+	}())
+});
+
+
+/*
+ * L.TileLayer is used for standard xyz-numbered tile layers.
+ */
+
+L.TileLayer = L.Class.extend({
+	includes: L.Mixin.Events,
+
+	options: {
+		minZoom: 0,
+		maxZoom: 18,
+		tileSize: 256,
+		subdomains: 'abc',
+		errorTileUrl: '',
+		attribution: '',
+		opacity: 1,
+		scheme: 'xyz',
+		continuousWorld: false,
+		noWrap: false,
+		zoomOffset: 0,
+		zoomReverse: false,
+		detectRetina: false,
+
+		unloadInvisibleTiles: L.Browser.mobile,
+		updateWhenIdle: L.Browser.mobile,
+		reuseTiles: false
+	},
+
+	initialize: function (url, options) {
+		options = L.Util.setOptions(this, options);
+
+		// detecting retina displays, adjusting tileSize and zoom levels
+		if (options.detectRetina && window.devicePixelRatio > 1 && options.maxZoom > 0) {
+
+			options.tileSize = Math.floor(options.tileSize / 2);
+			options.zoomOffset++;
+
+			if (options.minZoom > 0) {
+				options.minZoom--;
+			}
+			this.options.maxZoom--;
+		}
+
+		this._url = url;
+
+		var subdomains = this.options.subdomains;
+
+		if (typeof subdomains === 'string') {
+			this.options.subdomains = subdomains.split('');
+		}
+	},
+
+	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._resetCallback, this);
+		map.on('moveend', this._update, this);
+
+		if (!this.options.updateWhenIdle) {
+			this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
+			map.on('move', this._limitedUpdate, this);
+		}
+
+		this._reset();
+		this._update();
+	},
+
+	onRemove: function (map) {
+		map._panes.tilePane.removeChild(this._container);
+
+		map.off('viewreset', this._resetCallback, this);
+		map.off('moveend', this._update, this);
+
+		if (!this.options.updateWhenIdle) {
+			map.off('move', this._limitedUpdate, this);
+		}
+
+		this._container = null;
+		this._map = null;
+	},
+
+	getAttribution: function () {
+		return this.options.attribution;
+	},
+
+	setOpacity: function (opacity) {
+		this.options.opacity = opacity;
+
+		if (this._map) {
+			this._updateOpacity();
+		}
+
+		// stupid webkit hack to force redrawing of tiles
+		var i,
+			tiles = this._tiles;
+
+		if (L.Browser.webkit) {
+			for (i in tiles) {
+				if (tiles.hasOwnProperty(i)) {
+					tiles[i].style.webkitTransform += ' translate(0,0)';
+				}
+			}
+		}
+	},
+
+	_updateOpacity: function () {
+		L.DomUtil.setOpacity(this._container, this.options.opacity);
+	},
+
+	_initContainer: function () {
+		var tilePane = this._map._panes.tilePane,
+			first = tilePane.firstChild;
+
+		if (!this._container || tilePane.empty) {
+			this._container = L.DomUtil.create('div', 'leaflet-layer');
+
+			if (this._insertAtTheBottom && first) {
+				tilePane.insertBefore(this._container, first);
+			} else {
+				tilePane.appendChild(this._container);
+			}
+
+			if (this.options.opacity < 1) {
+				this._updateOpacity();
+			}
+		}
+	},
+
+	_resetCallback: function (e) {
+		this._reset(e.hard);
+	},
+
+	_reset: function (clearOldContainer) {
+		var key,
+			tiles = this._tiles;
+
+		for (key in tiles) {
+			if (tiles.hasOwnProperty(key)) {
+				this.fire('tileunload', {tile: tiles[key]});
+			}
+		}
+
+		this._tiles = {};
+
+		if (this.options.reuseTiles) {
+			this._unusedTiles = [];
+		}
+
+		if (clearOldContainer && this._container) {
+			this._container.innerHTML = "";
+		}
+
+		this._initContainer();
+	},
+
+	_update: function (e) {
+		if (this._map._panTransition && this._map._panTransition._inProgress) { return; }
+
+		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)),
+			seTilePoint = new L.Point(
+				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 || this.options.reuseTiles) {
+			this._removeOtherTiles(tileBounds);
+		}
+	},
+
+	_addTilesFromCenterOut: function (bounds) {
+		var queue = [],
+			center = bounds.getCenter();
+
+		var j, i;
+		for (j = bounds.min.y; j <= bounds.max.y; j++) {
+			for (i = bounds.min.x; i <= bounds.max.x; i++) {
+				if (!((i + ':' + j) in this._tiles)) {
+					queue.push(new L.Point(i, j));
+				}
+			}
+		}
+
+		// load tiles in order of their distance to center
+		queue.sort(function (a, b) {
+			return a.distanceTo(center) - b.distanceTo(center);
+		});
+
+		var fragment = document.createDocumentFragment();
+
+		this._tilesToLoad = queue.length;
+
+		var k, len;
+		for (k = 0, len = this._tilesToLoad; k < len; k++) {
+			this._addTile(queue[k], fragment);
+		}
+
+		this._container.appendChild(fragment);
+	},
+
+	_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._removeTile(key);
+				}
+			}
+		}
+	},
+
+	_removeTile: function (key) {
+		var 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(tile);
+		}
+
+		tile.src = L.Util.emptyImageUrl;
+
+		delete this._tiles[key];
+	},
+
+	_addTile: function (tilePoint, container) {
+		var tilePos = this._getTilePos(tilePoint),
+			zoom = this._map.getZoom(),
+		    key = tilePoint.x + ':' + tilePoint.y,
+		    limit = Math.pow(2, this._getOffsetZoom(zoom));
+
+		// wrap tile coordinates
+		if (!this.options.continuousWorld) {
+			if (!this.options.noWrap) {
+				tilePoint.x = ((tilePoint.x % limit) + limit) % limit;
+			} else if (tilePoint.x < 0 || tilePoint.x >= limit) {
+				this._tilesToLoad--;
+				return;
+			}
+
+			if (tilePoint.y < 0 || tilePoint.y >= limit) {
+				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') {
+			tilePoint.y = limit - tilePoint.y - 1;
+		}
+
+		this._loadTile(tile, tilePoint, zoom);
+
+		container.appendChild(tile);
+	},
+
+	_getOffsetZoom: function (zoom) {
+		var options = this.options;
+		zoom = options.zoomReverse ? options.maxZoom - zoom : zoom;
+		return zoom + options.zoomOffset;
+	},
+
+	_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) {
+		var subdomains = this.options.subdomains,
+			index = (tilePoint.x + tilePoint.y) % subdomains.length,
+			s = this.options.subdomains[index];
+
+		return L.Util.template(this._url, L.Util.extend({
+			s: s,
+			z: this._getOffsetZoom(zoom),
+			x: tilePoint.x,
+			y: tilePoint.y
+		}, this.options));
+	},
+
+	_createTileProto: function () {
+		var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile');
+		img.galleryimg = 'no';
+
+		var tileSize = this.options.tileSize;
+		img.style.width = tileSize + 'px';
+		img.style.height = tileSize + 'px';
+	},
+
+	_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) {
+		tile._layer  = this;
+		tile.onload  = this._tileOnLoad;
+		tile.onerror = this._tileOnError;
+
+		tile.src     = this.getTileUrl(tilePoint, zoom);
+	},
+
+    _tileLoaded: function () {
+        this._tilesToLoad--;
+        if (!this._tilesToLoad) {
+            this.fire('load');
+        }
+    },
+
+	_tileOnLoad: function (e) {
+		var layer = this._layer;
+
+		this.className += ' leaflet-tile-loaded';
+
+		layer.fire('tileload', {
+			tile: this,
+			url: this.src
+		});
+
+        layer._tileLoaded();
+	},
+
+	_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;
+		}
+
+        layer._tileLoaded();
+    }
+});
+
+
+L.TileLayer.WMS = L.TileLayer.extend({
+	defaultWmsParams: {
+		service: 'WMS',
+		request: 'GetMap',
+		version: '1.1.1',
+		layers: '',
+		styles: '',
+		format: 'image/jpeg',
+		transparent: false
+	},
+
+	initialize: function (url, options) { // (String, Object)
+		this._url = url;
+
+		var wmsParams = L.Util.extend({}, this.defaultWmsParams);
+		wmsParams.width = 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)) {
+				wmsParams[i] = options[i];
+			}
+		}
+
+		this.wmsParams = wmsParams;
+
+		L.Util.setOptions(this, options);
+	},
+
+	onAdd: function (map, insertAtTheBottom) {
+		var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs';
+		this.wmsParams[projectionKey] = map.options.crs.code;
+
+		L.TileLayer.prototype.onAdd.call(this, map, insertAtTheBottom);
+	},
+
+	getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String
+		var map = this._map,
+			crs = map.options.crs,
+
+			tileSize = this.options.tileSize,
+
+			nwPoint = tilePoint.multiplyBy(tileSize),
+			sePoint = nwPoint.add(new L.Point(tileSize, tileSize)),
+
+			nwMap = map.unproject(nwPoint, zoom, true),
+			seMap = map.unproject(sePoint, zoom, true),
+
+			nw = crs.project(nwMap),
+			se = crs.project(seMap),
+
+			bbox = [nw.x, se.y, se.x, nw.y].join(',');
+
+		return this._url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox;
+	}
+});
+
+
+L.TileLayer.Canvas = L.TileLayer.extend({
+	options: {
+		async: false
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+	},
+
+	redraw: function () {
+		var i,
+			tiles = this._tiles;
+
+		for (i in tiles) {
+			if (tiles.hasOwnProperty(i)) {
+				this._redrawTile(tiles[i]);
+			}
+		}
+	},
+
+	_redrawTile: function (tile) {
+		this.drawTile(tile, tile._tilePoint, tile._zoom);
+	},
+
+	_createTileProto: function () {
+		var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');
+
+		var tileSize = this.options.tileSize;
+		proto.width = tileSize;
+		proto.height = tileSize;
+	},
+
+	_createTile: function () {
+		var tile = this._canvasProto.cloneNode(false);
+		tile.onselectstart = tile.onmousemove = L.Util.falseFn;
+		return tile;
+	},
+
+	_loadTile: function (tile, tilePoint, zoom) {
+		tile._layer = this;
+		tile._tilePoint = tilePoint;
+		tile._zoom = zoom;
+
+		this.drawTile(tile, tilePoint, zoom);
+
+		if (!this.options.async) {
+			this.tileDrawn(tile);
+		}
+	},
+
+	drawTile: function (tile, tilePoint, zoom) {
+		// override with rendering code
+	},
+
+	tileDrawn: function (tile) {
+		this._tileOnLoad.call(tile);
+	}
+});
+
+
+L.ImageOverlay = L.Class.extend({
+	includes: L.Mixin.Events,
+
+	initialize: function (/*String*/ url, /*LatLngBounds*/ bounds) {
+		this._url = url;
+		this._bounds = bounds;
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		if (!this._image) {
+			this._initImage();
+		}
+
+		map._panes.overlayPane.appendChild(this._image);
+
+		map.on('viewreset', this._reset, this);
+		this._reset();
+	},
+
+	onRemove: function (map) {
+		map.getPanes().overlayPane.removeChild(this._image);
+		map.off('viewreset', this._reset, this);
+	},
+
+	_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: L.Util.bind(this._onImageLoad, this),
+			src: this._url
+		});
+	},
+
+	_reset: function () {
+		var image   = this._image,
+		    topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
+		    size    = this._map.latLngToLayerPoint(this._bounds.getSouthEast()).subtract(topLeft);
+
+		L.DomUtil.setPosition(image, topLeft);
+
+		image.style.width  = size.x + 'px';
+		image.style.height = size.y + 'px';
+	},
+
+	_onImageLoad: function () {
+		this._image.style.visibility = '';
+		this.fire('load');
+	}
+});
+
+
+L.Icon = L.Class.extend({
+	options: {
+		/*
+		iconUrl: (String) (required)
+		iconSize: (Point) (can be set through CSS)
+		iconAnchor: (Point) (centered by default if size is specified, can be set in CSS with negative margins)
+		popupAnchor: (Point) (if not specified, popup opens in the anchor point)
+		shadowUrl: (Point) (no shadow by default)
+		shadowSize: (Point)
+		*/
+		className: ''
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+	},
+
+	createIcon: function () {
+		return this._createIcon('icon');
+	},
+
+	createShadow: function () {
+		return this._createIcon('shadow');
+	},
+
+	_createIcon: function (name) {
+		var src = this._getIconUrl(name);
+
+		if (!src) { return null; }
+		
+		var img = this._createImg(src);
+		this._setIconStyles(img, name);
+
+		return img;
+	},
+
+	_setIconStyles: function (img, name) {
+		var options = this.options,
+			size = options[name + 'Size'],
+			anchor = options.iconAnchor;
+
+		if (!anchor && size) {
+			anchor = size.divideBy(2, true);
+		}
+
+		if (name === 'shadow' && anchor && options.shadowOffset) {
+			anchor._add(options.shadowOffset);
+		}
+
+		img.className = 'leaflet-marker-' + name + ' ' + options.className + ' leaflet-zoom-animated';
+
+		if (anchor) {
+			img.style.marginLeft = (-anchor.x) + 'px';
+			img.style.marginTop  = (-anchor.y) + 'px';
+		}
+
+		if (size) {
+			img.style.width  = size.x + 'px';
+			img.style.height = size.y + 'px';
+		}
+	},
+
+	_createImg: function (src) {
+		var el;
+
+		if (!L.Browser.ie6) {
+			el = document.createElement('img');
+			el.src = src;
+		} else {
+			el = document.createElement('div');
+			el.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + src + '")';
+		}
+		return el;
+	},
+
+	_getIconUrl: function (name) {
+		return this.options[name + 'Url'];
+	}
+});
+
+
+// TODO move to a separate file
+
+L.Icon.Default = L.Icon.extend({
+	options: {
+		iconSize: new L.Point(25, 41),
+		iconAnchor: new L.Point(13, 41),
+		popupAnchor: new L.Point(0, -33),
+
+		shadowSize: new L.Point(41, 41)
+	},
+
+	_getIconUrl: function (name) {
+		var path = L.Icon.Default.imagePath;
+		if (!path) {
+			throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually.");
+		}
+
+		return path + '/marker-' + name + '.png';
+	}
+});
+
+L.Icon.Default.imagePath = (function () {
+	var scripts = document.getElementsByTagName('script'),
+	    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) {
+			return src.split(leafletRe)[0] + '/images';
+		}
+	}
+}());
+
+/*
+ * L.Marker is used to display clickable/draggable icons on the map.
+ */
+
+L.Marker = L.Class.extend({
+
+	includes: L.Mixin.Events,
+
+	options: {
+		icon: new L.Icon.Default(),
+		title: '',
+		clickable: true,
+		draggable: false,
+		zIndexOffset: 0,
+		opacity: 1
+	},
+
+	initialize: function (latlng, options) {
+		L.Util.setOptions(this, options);
+		this._latlng = latlng;
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		map.on('viewreset', this._reset, this);
+
+		if (map.options.zoomAnimation && map.options.animateMarkerZoom) {
+			map.on('zoomanim', this._zoomAnimation, this);
+		}
+
+		this._initIcon();
+		this._reset();
+	},
+
+	onRemove: function (map) {
+		this._removeIcon();
+
+		// TODO move to Marker.Popup.js
+		if (this.closePopup) {
+			this.closePopup();
+		}
+
+		map.off('viewreset', this._reset, this)
+		   .off('zoomanim', this._zoomAnimation, this);
+
+		this._map = null;
+	},
+
+	getLatLng: function () {
+		return this._latlng;
+	},
+
+	setLatLng: function (latlng) {
+		this._latlng = latlng;
+
+		this._reset();
+
+		if (this._popup) {
+			this._popup.setLatLng(latlng);
+		}
+	},
+
+	setZIndexOffset: function (offset) {
+		this.options.zIndexOffset = offset;
+		this._reset();
+	},
+
+	setIcon: function (icon) {
+		if (this._map) {
+			this._removeIcon();
+		}
+
+		this.options.icon = icon;
+
+		if (this._map) {
+			this._initIcon();
+			this._reset();
+		}
+	},
+
+	_initIcon: function () {
+		var options = this.options;
+
+		if (!this._icon) {
+			this._icon = options.icon.createIcon();
+
+			if (options.title) {
+				this._icon.title = options.title;
+			}
+
+			this._initInteraction();
+			this._updateOpacity();
+		}
+		if (!this._shadow) {
+			this._shadow = options.icon.createShadow();
+		}
+
+		var panes = this._map._panes;
+
+		panes.markerPane.appendChild(this._icon);
+
+		if (this._shadow) {
+			panes.shadowPane.appendChild(this._shadow);
+		}
+	},
+
+	_removeIcon: function () {
+		var panes = this._map._panes;
+
+		panes.markerPane.removeChild(this._icon);
+
+		if (this._shadow) {
+			panes.shadowPane.removeChild(this._shadow);
+		}
+
+		this._icon = this._shadow = null;
+	},
+
+	_reset: function () {
+		if (!this._icon) { return; }
+
+		var pos = this._map.latLngToLayerPoint(this._latlng).round();
+		this._setPos(pos);
+	},
+
+	_setPos: function (pos) {
+		L.DomUtil.setPosition(this._icon, pos);
+
+		if (this._shadow) {
+			L.DomUtil.setPosition(this._shadow, pos);
+		}
+
+		this._icon.style.zIndex = pos.y + this.options.zIndexOffset;
+	},
+
+	_zoomAnimation: function (opt) {
+		var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
+
+		this._setPos(pos);
+	},
+
+	_initInteraction: function () {
+		if (!this.options.clickable) {
+			return;
+		}
+
+		var icon = this._icon,
+			events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
+
+		icon.className += ' leaflet-clickable';
+		L.DomEvent.addListener(icon, 'click', this._onMouseClick, this);
+
+		for (var i = 0; i < events.length; i++) {
+			L.DomEvent.addListener(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) {
+		L.DomEvent.stopPropagation(e);
+		if (this.dragging && this.dragging.moved()) { return; }
+		if (this._map.dragging && this._map.dragging.moved()) { return; }
+		this.fire(e.type, {
+			originalEvent: e
+		});
+	},
+
+	_fireMouseEvent: function (e) {
+		this.fire(e.type, {
+			originalEvent: e
+		});
+		if (e.type !== 'mousedown') {
+			L.DomEvent.stopPropagation(e);
+		}
+	},
+
+	setOpacity: function (opacity) {
+		this.options.opacity = opacity;
+		if (this._map) {
+			this._updateOpacity();
+		}
+	},
+
+	_updateOpacity: function (opacity) {
+		L.DomUtil.setOpacity(this._icon, this.options.opacity);
+	}
+});
+
+
+L.DivIcon = L.Icon.extend({
+	options: {
+		iconSize: new L.Point(12, 12), // also can be set through CSS
+		/*
+		iconAnchor: (Point)
+		popupAnchor: (Point)
+		*/
+		className: 'leaflet-div-icon'
+	},
+
+	createIcon: function () {
+		var div = document.createElement('div');
+		this._setIconStyles(div, 'icon');
+		return div;
+	},
+
+	createShadow: function () {
+		return null;
+	}
+});
+
+
+
+L.Map.mergeOptions({
+	closePopupOnClick: true
+});
+
+L.Popup = L.Class.extend({
+	includes: L.Mixin.Events,
+
+	options: {
+		minWidth: 50,
+		maxWidth: 300,
+		maxHeight: null,
+		autoPan: true,
+		closeButton: true,
+		offset: new L.Point(0, 2),
+		autoPanPadding: new L.Point(5, 5),
+		className: ''
+	},
+
+	initialize: function (options, source) {
+		L.Util.setOptions(this, options);
+
+		this._source = source;
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		if (!this._container) {
+			this._initLayout();
+		}
+		this._updateContent();
+
+		this._container.style.opacity = '0';
+		map._panes.popupPane.appendChild(this._container);
+
+		map.on('viewreset', this._updatePosition, this);
+
+		if (L.Browser.any3d) {
+			map.on('zoomanim', this._zoomAnimation, this);
+		}
+
+		if (map.options.closePopupOnClick) {
+			map.on('preclick', this._close, this);
+		}
+
+		this._update();
+
+		this._container.style.opacity = '1'; //TODO fix ugly opacity hack
+	},
+
+	onRemove: function (map) {
+		map._panes.popupPane.removeChild(this._container);
+
+		L.Util.falseFn(this._container.offsetWidth);
+
+		map.off('viewreset', this._updatePosition, this)
+		   .off('preclick', this._close, this)
+		   .off('zoomanim', this._zoomAnimation, this);
+
+		this._container.style.opacity = '0';
+
+		this._map = null;
+	},
+
+	setLatLng: function (latlng) {
+		this._latlng = latlng;
+		this._update();
+		return this;
+	},
+
+	setContent: function (content) {
+		this._content = content;
+		this._update();
+		return this;
+	},
+
+	_close: function () {
+		var map = this._map;
+
+		if (map) {
+			map._popup = null;
+
+			map
+				.removeLayer(this)
+				.fire('popupclose', {popup: this});
+		}
+	},
+
+	_initLayout: function () {
+		var prefix = 'leaflet-popup',
+			container = this._container = L.DomUtil.create('div', prefix + ' ' + this.options.className + ' leaflet-zoom-animated'),
+			closeButton;
+
+		if (this.options.closeButton) {
+			closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container);
+			closeButton.href = '#close';
+
+			L.DomEvent.addListener(closeButton, 'click', this._onCloseButtonClick, this);
+		}
+
+		var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container);
+		L.DomEvent.disableClickPropagation(wrapper);
+
+		this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
+		L.DomEvent.addListener(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation);
+
+		this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
+		this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
+	},
+
+	_update: function () {
+		if (!this._map) { return; }
+
+		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') {
+			this._contentNode.innerHTML = this._content;
+		} else {
+			while (this._contentNode.hasChildNodes()) {
+				this._contentNode.removeChild(this._contentNode.firstChild);
+			}
+			this._contentNode.appendChild(this._content);
+		}
+		this.fire('contentupdate');
+	},
+
+	_updateLayout: function () {
+		var container = this._contentNode;
+
+		container.style.width = '';
+		container.style.whiteSpace = 'nowrap';
+
+		var width = container.offsetWidth;
+		width = Math.min(width, this.options.maxWidth);
+		width = Math.max(width, this.options.minWidth);
+
+		container.style.width = (width + 1) + 'px';
+		container.style.whiteSpace = '';
+
+		container.style.height = '';
+
+		var height = container.offsetHeight,
+			maxHeight = this.options.maxHeight,
+			scrolledClass = ' leaflet-popup-scrolled';
+
+		if (maxHeight && height > maxHeight) {
+			container.style.height = maxHeight + 'px';
+			container.className += scrolledClass;
+		} else {
+			container.className = container.className.replace(scrolledClass, '');
+		}
+
+		this._containerWidth = this._container.offsetWidth;
+		this._containerBottom = -this.options.offset.y;
+		this._containerLeft = -Math.round(this._containerWidth / 2) + this.options.offset.x;
+	},
+
+	_updatePosition: function () {
+		var pos = this._map.latLngToLayerPoint(this._latlng);
+
+		this._container.style.bottom = this._containerBottom + 'px';
+		this._container.style.left = this._containerLeft + 'px';
+
+		L.DomUtil.setPosition(this._container, pos);
+	},
+	
+	_zoomAnimation: function (opt) {
+		var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center)._round();
+
+		L.DomUtil.setPosition(this._container, pos);
+	},
+
+	_adjustPan: function () {
+		if (!this.options.autoPan) { return; }
+
+		var map = this._map,
+			containerHeight = this._container.offsetHeight,
+			containerWidth = this._containerWidth,
+
+			layerPos = L.DomUtil.getPosition(this._container).add(
+				new L.Point(this._containerLeft, -containerHeight - this._containerBottom)),
+
+			containerPos = map.layerPointToContainerPoint(layerPos),
+			adjustOffset = new L.Point(0, 0),
+			padding      = this.options.autoPanPadding,
+			size         = map.getSize();
+
+		if (containerPos.x < 0) {
+			adjustOffset.x = containerPos.x - padding.x;
+		}
+		if (containerPos.x + containerWidth > size.x) {
+			adjustOffset.x = containerPos.x + containerWidth - size.x + padding.x;
+		}
+		if (containerPos.y < 0) {
+			adjustOffset.y = containerPos.y - padding.y;
+		}
+		if (containerPos.y + containerHeight > size.y) {
+			adjustOffset.y = containerPos.y + containerHeight - size.y + padding.y;
+		}
+
+		if (adjustOffset.x || adjustOffset.y) {
+			map.panBy(adjustOffset);
+		}
+	},
+
+	_onCloseButtonClick: function (e) {
+		this._close();
+		L.DomEvent.stop(e);
+	}
+});
+
+
+/*
+ * Popup extension to L.Marker, adding openPopup & bindPopup methods.
+ */
+
+L.Marker.include({
+	openPopup: function () {
+		if (this._popup && this._map) {
+			this._popup.setLatLng(this._latlng);
+			this._map.openPopup(this._popup);
+		}
+
+		return this;
+	},
+
+	closePopup: function () {
+		if (this._popup) {
+			this._popup._close();
+		}
+		return this;
+	},
+
+	bindPopup: function (content, options) {
+		var anchor = this.options.icon.options.popupAnchor || new L.Point(0, 0);
+
+		if (options && options.offset) {
+			anchor = anchor.add(options.offset);
+		}
+
+		options = L.Util.extend({offset: anchor}, options);
+
+		if (!this._popup) {
+			this.on('click', this.openPopup, this);
+		}
+
+		this._popup = new L.Popup(options, this)
+			.setContent(content);
+
+		return this;
+	},
+
+	unbindPopup: function () {
+		if (this._popup) {
+			this._popup = null;
+			this.off('click', this.openPopup);
+		}
+		return this;
+	}
+});
+
+
+
+L.Map.include({
+	openPopup: function (popup) {
+		this.closePopup();
+
+		this._popup = popup;
+
+		return this
+			.addLayer(popup)
+			.fire('popupopen', {popup: this._popup});
+	},
+
+	closePopup: function () {
+		if (this._popup) {
+			this._popup._close();
+		}
+		return this;
+	}
+});
+
+/*
+ * L.LayerGroup is a class to combine several layers so you can manipulate the group (e.g. add/remove it) as one layer.
+ */
+
+L.LayerGroup = L.Class.extend({
+	initialize: function (layers) {
+		this._layers = {};
+
+		var i, len;
+
+		if (layers) {
+			for (i = 0, len = layers.length; i < len; i++) {
+				this.addLayer(layers[i]);
+			}
+		}
+	},
+
+	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) {
+		var id = L.Util.stamp(layer);
+
+		delete this._layers[id];
+
+		if (this._map) {
+			this._map.removeLayer(layer);
+		}
+
+		return this;
+	},
+
+	clearLayers: function () {
+		this._iterateLayers(this.removeLayer, this);
+		return this;
+	},
+
+	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) {
+		this._iterateLayers(map.removeLayer, map);
+		this._map = null;
+	},
+
+	_iterateLayers: function (method, context) {
+		for (var i in this._layers) {
+			if (this._layers.hasOwnProperty(i)) {
+				method.call(context, this._layers[i]);
+			}
+		}
+	}
+});
+
+
+/*
+ * 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) {
+		this._initEvents(layer);
+
+		L.LayerGroup.prototype.addLayer.call(this, layer);
+
+		if (this._popupContent && layer.bindPopup) {
+			layer.bindPopup(this._popupContent);
+		}
+	},
+
+	bindPopup: function (content) {
+		this._popupContent = content;
+		return this.invoke('bindPopup', content);
+	},
+
+	setStyle: function (style) {
+		return this.invoke('setStyle', style);
+	},
+
+	getBounds: function () {
+		var bounds = new L.LatLngBounds();
+		this._iterateLayers(function (layer) {
+			bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds());
+		}, this);
+		return bounds;
+	},
+
+	_initEvents: function (layer) {
+		var events = ['click', 'dblclick', 'mouseover', 'mouseout'],
+			i, len;
+
+		for (i = 0, len = events.length; i < len; i++) {
+			layer.on(events[i], this._propagateEvent, this);
+		}
+	},
+
+	_propagateEvent: function (e) {
+		e.layer  = e.target;
+		e.target = this;
+
+		this.fire(e.type, e);
+	}
+});
+
+
+/*
+ * L.Path is a base class for rendering vector paths on a map. It's inherited by Polyline, Circle, etc.
+ */
+
+L.Path = L.Class.extend({
+	includes: [L.Mixin.Events],
+
+	statics: {
+		// 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
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		this._initElements();
+		this._initEvents();
+		this.projectLatlngs();
+		this._updatePath();
+
+		map
+			.on('viewreset', this.projectLatlngs, this)
+			.on('moveend', this._updatePath, this);
+	},
+
+	onRemove: function (map) {
+		this._map = null;
+
+		map._pathRoot.removeChild(this._container);
+
+		map
+			.off('viewreset', this.projectLatlngs, this)
+			.off('moveend', this._updatePath, this);
+	},
+
+	projectLatlngs: function () {
+		// do all projection stuff here
+	},
+
+	setStyle: function (style) {
+		L.Util.setOptions(this, style);
+
+		if (this._container) {
+			this._updateStyle();
+		}
+
+		return this;
+	},
+
+	redraw: function () {
+		if (this._map) {
+			this.projectLatlngs();
+			this._updatePath();
+		}
+		return this;
+	}
+});
+
+L.Map.include({
+	_updatePathViewport: function () {
+		var p = L.Path.CLIP_PADDING,
+			size = this.getSize(),
+			panePos = L.DomUtil.getPosition(this._mapPane),
+			min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)),
+			max = min.add(size.multiplyBy(1 + p * 2));
+
+		this._pathViewport = new L.Bounds(min, max);
+	}
+});
+
+
+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
+	},
+
+	getPathString: function () {
+		// form path string here
+	},
+
+	_createElement: function (name) {
+		return document.createElementNS(L.Path.SVG_NS, name);
+	},
+
+	_initElements: function () {
+		this._map._initPathRoot();
+		this._initPath();
+		this._initStyle();
+	},
+
+	_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');
+		}
+		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);
+		} else {
+			this._path.setAttribute('stroke', 'none');
+		}
+		if (this.options.fill) {
+			this._path.setAttribute('fill', this.options.fillColor || this.options.color);
+			this._path.setAttribute('fill-opacity', this.options.fillOpacity);
+		} else {
+			this._path.setAttribute('fill', 'none');
+		}
+	},
+
+	_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.svg || !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', 'contextmenu'];
+			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;
+		}
+
+		if (e.type === 'contextmenu') {
+			L.DomEvent.preventDefault(e);
+		}
+
+		this._fireMouseEvent(e);
+	},
+
+	_fireMouseEvent: function (e) {
+		if (!this.hasEventListeners(e.type)) {
+			return;
+		}
+		var map = this._map,
+			containerPoint = map.mouseEventToContainerPoint(e),
+			layerPoint = map.containerPointToLayerPoint(containerPoint),
+			latlng = map.layerPointToLatLng(layerPoint);
+
+		this.fire(e.type, {
+			latlng: latlng,
+			layerPoint: layerPoint,
+			containerPoint: containerPoint,
+			originalEvent: e
+		});
+
+		L.DomEvent.stopPropagation(e);
+	}
+});
+
+L.Map.include({
+	_initPathRoot: function () {
+		if (!this._pathRoot) {
+			this._pathRoot = L.Path.prototype._createElement('svg');
+			this._panes.overlayPane.appendChild(this._pathRoot);
+
+			if (this.options.zoomAnimation) {
+				this._pathRoot.setAttribute('class', ' leaflet-zoom-animated');
+				this.on('zoomanim', this._animatePathZoom);
+				this.on('zoomend', this._endPathZoom);
+			}
+
+			this.on('moveend', this._updateSvgViewport);
+			this._updateSvgViewport();
+		}
+	},
+
+	_animatePathZoom: function (opt) {
+		var centerOffset = this._getNewTopLeftPoint(opt.center).subtract(this._getTopLeftPoint()),
+			scale = Math.pow(2, opt.zoom - this._zoom),
+			offset = centerOffset.divideBy(1 - 1 / scale),
+			centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(-2)),
+			origin = centerPoint.add(offset).round(),
+			pathRootStyle = this._pathRoot.style;
+
+		pathRootStyle[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString((origin.multiplyBy(-1).add(L.DomUtil.getPosition(this._pathRoot)).multiplyBy(scale).add(origin))) + ' scale(' + scale + ') ';
+
+		this._pathZooming = true;
+	},
+
+	_endPathZoom: function () {
+		this._pathZooming = false;
+	},
+
+	_updateSvgViewport: function () {
+		if (this._pathZooming) {
+			//Do not update SVGs while a zoom animation is going on otherwise the animation will break.
+			//When the zoom animation ends we will be updated again anyway
+			//This fixes the case where you do a momentum move and zoom while the move is still ongoing.
+			return;
+		}
+
+		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.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);
+		}
+	}
+});
+
+
+/*
+ * Popup extension to L.Path (polylines, polygons, circles), adding bindPopup method.
+ */
+
+L.Path.include({
+	bindPopup: function (content, options) {
+		if (!this._popup || this._popup.options !== 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) {
+		this._popup.setLatLng(e.latlng);
+		this._map.openPopup(this._popup);
+	}
+});
+
+
+/*
+ * Vector rendering for IE6-8 through VML.
+ * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
+ */
+
+L.Browser.vml = (function () {
+	var div = document.createElement('div');
+	div.innerHTML = '<v:shape adj="1"/>';
+
+	var shape = div.firstChild;
+	shape.style.behavior = 'url(#default#VML)';
+
+	return shape && (typeof shape.adj === 'object');
+}());
+
+L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({
+	statics: {
+		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">');
+			};
+		}
+	}()),
+
+	_initPath: function () {
+		var container = this._container = this._createElement('shape');
+		container.className += ' leaflet-vml-shape' +
+				(this.options.clickable ? ' leaflet-clickable' : '');
+		container.coordsize = '1 1';
+
+		this._path = this._createElement('path');
+		container.appendChild(this._path);
+
+		this._map._pathRoot.appendChild(container);
+	},
+
+	_initStyle: function () {
+		var container = this._container,
+			stroke,
+			fill;
+
+		if (this.options.stroke) {
+			stroke = this._stroke = this._createElement('stroke');
+			stroke.endcap = 'round';
+			container.appendChild(stroke);
+		}
+
+		if (this.options.fill) {
+			fill = this._fill = this._createElement('fill');
+			container.appendChild(fill);
+		}
+
+		this._updateStyle();
+	},
+
+	_updateStyle: function () {
+		var stroke = this._stroke,
+			fill = this._fill,
+			options = this.options,
+			container = this._container;
+
+		container.stroked = options.stroke;
+		container.filled = options.fill;
+
+		if (options.stroke) {
+			stroke.weight  = options.weight + 'px';
+			stroke.color   = options.color;
+			stroke.opacity = options.opacity;
+		}
+
+		if (options.fill) {
+			fill.color   = options.fillColor || options.color;
+			fill.opacity = options.fillOpacity;
+		}
+	},
+
+	_updatePath: function () {
+		var style = this._container.style;
+
+		style.display = 'none';
+		this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug
+		style.display = '';
+	}
+});
+
+L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : {
+	_initPathRoot: function () {
+		if (this._pathRoot) { return; }
+
+		var root = this._pathRoot = document.createElement('div');
+		root.className = 'leaflet-vml-container';
+		this._panes.overlayPane.appendChild(root);
+
+		this.on('moveend', this._updatePathViewport);
+		this._updatePathViewport();
+	}
+});
+
+
+/*
+ * 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
+	},
+
+	_initElements: function () {
+		this._map._initPathRoot();
+		this._ctx = this._map._canvasCtx;
+	},
+
+	_updateStyle: function () {
+		var options = this.options;
+
+		if (options.stroke) {
+			this._ctx.lineWidth = options.weight;
+			this._ctx.strokeStyle = options.color;
+		}
+		if (options.fill) {
+			this._ctx.fillStyle = options.fillColor || 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; }
+
+		var ctx = this._ctx,
+			options = this.options;
+
+		this._drawPath();
+		ctx.save();
+		this._updateStyle();
+
+		if (options.fill) {
+			if (options.fillOpacity < 1) {
+				ctx.globalAlpha = options.fillOpacity;
+			}
+			ctx.fill();
+		}
+
+		if (options.stroke) {
+			if (options.opacity < 1) {
+				ctx.globalAlpha = options.opacity;
+			}
+			ctx.stroke();
+		}
+
+		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)
+            .off('moveend', this._updatePath, this)
+            .fire('moveend');
+    }
+});
+
+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);
+
+			if (this.options.zoomAnimation) {
+				this._pathRoot.className = 'leaflet-zoom-animated';
+				this.on('zoomanim', this._animatePathZoom);
+				this.on('zoomend', this._endPathZoom);
+			}
+			this.on('moveend', this._updateCanvasViewport);
+			this._updateCanvasViewport();
+		}
+	},
+
+	_updateCanvasViewport: function () {
+		if (this._pathZooming) {
+			//Don't redraw while zooming. See _updateSvgViewport for more details
+			return;
+		}
+		this._updatePathViewport();
+
+		var vp = this._pathViewport,
+			min = vp.min,
+			size = vp.max.subtract(min),
+			root = this._pathRoot;
+
+		//TODO check if this 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);
+	}
+});
+
+
+/*
+ * 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 || !points.length) {
+			return points.slice();
+		}
+
+		var sqTolerance = tolerance * tolerance;
+
+		// stage 1: vertex reduction
+		points = this._reducePoints(points, sqTolerance);
+
+		// stage 2: Douglas-Peucker simplification
+		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._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, 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;
+				maxSqDist = sqDist;
+			}
+		}
+
+		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, sqTolerance) {
+		var reducedPoints = [points[0]];
+
+		for (var i = 1, prev = 0, len = points.length; i < len; 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;
+	},
+
+	/*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)) {
+				return [a, b];
+			// if a,b is outside the clip window (trivial reject)
+			} else if (codeA & codeB) {
+				return false;
+			// other cases
+			} else {
+				var codeOut = codeA || codeB,
+					p = this._getEdgeIntersection(a, b, codeOut, bounds),
+					newCode = this._getBitCode(p, bounds);
+
+				if (codeOut === codeA) {
+					a = p;
+					codeA = newCode;
+				} else {
+					b = p;
+					codeB = newCode;
+				}
+			}
+		}
+	},
+
+	_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
+			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) {
+		var code = 0;
+
+		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) {
+		var dx = p2.x - p1.x,
+			dy = p2.y - p1.y;
+		return dx * dx + dy * dy;
+	},
+
+	// 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);
+	}
+};
+
+
+L.Polyline = L.Path.extend({
+	initialize: function (latlngs, options) {
+		L.Path.prototype.initialize.call(this, options);
+		this._latlngs = latlngs;
+
+		// TODO refactor: move to Polyline.Edit.js
+		if (L.Handler.PolyEdit) {
+			this.editing = new L.Handler.PolyEdit(this);
+
+			if (this.options.editable) {
+				this.editing.enable();
+			}
+		}
+	},
+
+	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
+	},
+
+	projectLatlngs: function () {
+		this._originalPoints = [];
+
+		for (var i = 0, len = this._latlngs.length; i < len; i++) {
+			this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]);
+		}
+	},
+
+	getPathString: function () {
+		for (var i = 0, len = this._parts.length, str = ''; i < len; i++) {
+			str += this._getPathPartStr(this._parts[i]);
+		}
+		return str;
+	},
+
+	getLatLngs: function () {
+		return this._latlngs;
+	},
+
+	setLatLngs: function (latlngs) {
+		this._latlngs = latlngs;
+		return this.redraw();
+	},
+
+	addLatLng: function (latlng) {
+		this._latlngs.push(latlng);
+		return this.redraw();
+	},
+
+	spliceLatLngs: function (index, howMany) {
+		var removed = [].splice.apply(this._latlngs, arguments);
+		this.redraw();
+		return removed;
+	},
+
+	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;
+	},
+
+	// TODO refactor: move to Polyline.Edit.js
+	onAdd: function (map) {
+		L.Path.prototype.onAdd.call(this, map);
+
+		if (this.editing && this.editing.enabled()) {
+			this.editing.addHooks();
+		}
+	},
+
+	onRemove: function (map) {
+		if (this.editing && this.editing.enabled()) {
+			this.editing.removeHooks();
+		}
+
+		L.Path.prototype.onRemove.call(this, map);
+	},
+
+	_initEvents: function () {
+		L.Path.prototype._initEvents.call(this);
+	},
+
+	_getPathPartStr: function (points) {
+		var round = L.Path.VML;
+
+		for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) {
+			p = points[j];
+			if (round) {
+				p._round();
+			}
+			str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
+		}
+		return str;
+	},
+
+	_clipPoints: function () {
+		var points = this._originalPoints,
+			len = points.length,
+			i, k, segment;
+
+		if (this.options.noClip) {
+			this._parts = [points];
+			return;
+		}
+
+		this._parts = [];
+
+		var parts = this._parts,
+			vp = this._map._pathViewport,
+			lu = L.LineUtil;
+
+		for (i = 0, k = 0; i < len - 1; i++) {
+			segment = lu.clipSegment(points[i], points[i + 1], vp, i);
+			if (!segment) {
+				continue;
+			}
+
+			parts[k] = parts[k] || [];
+			parts[k].push(segment[0]);
+
+			// if segment goes out of screen, or it's the last one, it's the end of the line part
+			if ((segment[1] !== points[i + 1]) || (i === len - 2)) {
+				parts[k].push(segment[1]);
+				k++;
+			}
+		}
+	},
+
+	// simplify each clipped part of the polyline
+	_simplifyPoints: function () {
+		var parts = this._parts,
+			lu = L.LineUtil;
+
+		for (var i = 0, len = parts.length; i < len; i++) {
+			parts[i] = lu.simplify(parts[i], this.options.smoothFactor);
+		}
+	},
+
+	_updatePath: function () {
+		this._clipPoints();
+		this._simplifyPoints();
+
+		L.Path.prototype._updatePath.call(this);
+	}
+});
+
+
+/*
+ * 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) {
+	var min = bounds.min,
+		max = bounds.max,
+		clippedPoints,
+		edges = [1, 4, 2, 8],
+		i, j, k,
+		a, b,
+		len, edge, p,
+		lu = L.LineUtil;
+
+	for (i = 0, len = points.length; i < len; i++) {
+		points[i]._code = lu._getBitCode(points[i], bounds);
+	}
+
+	// for each edge (left, bottom, right, top)
+	for (k = 0; k < 4; k++) {
+		edge = edges[k];
+		clippedPoints = [];
+
+		for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
+			a = points[i];
+			b = points[j];
+
+			// if a is inside the clip window
+			if (!(a._code & edge)) {
+				// if b is outside the clip window (a->b goes out of screen)
+				if (b._code & edge) {
+					p = lu._getEdgeIntersection(b, a, edge, bounds);
+					p._code = lu._getBitCode(p, bounds);
+					clippedPoints.push(p);
+				}
+				clippedPoints.push(a);
+
+			// else if b is inside the clip window (a->b enters the screen)
+			} else if (!(b._code & edge)) {
+				p = lu._getEdgeIntersection(b, a, edge, bounds);
+				p._code = lu._getBitCode(p, bounds);
+				clippedPoints.push(p);
+			}
+		}
+		points = clippedPoints;
+	}
+
+	return points;
+};
+
+/*jshint bitwise:true */
+
+
+/*
+ * L.Polygon is used to display polygons on a map.
+ */
+
+L.Polygon = L.Polyline.extend({
+	options: {
+		fill: true
+	},
+
+	initialize: function (latlngs, options) {
+		L.Polyline.prototype.initialize.call(this, latlngs, options);
+
+		if (latlngs && (latlngs[0] instanceof Array)) {
+			this._latlngs = latlngs[0];
+			this._holes = latlngs.slice(1);
+		}
+	},
+
+	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;
+		}
+
+		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++) {
+				this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]);
+			}
+		}
+	},
+
+	_clipPoints: function () {
+		var points = this._originalPoints,
+			newParts = [];
+
+		this._parts = [points].concat(this._holePoints);
+
+		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;
+			}
+			newParts.push(clipped);
+		}
+
+		this._parts = newParts;
+	},
+
+	_getPathPartStr: function (points) {
+		var str = L.Polyline.prototype._getPathPartStr.call(this, points);
+		return str + (L.Browser.svg ? 'z' : 'x');
+	}
+});
+
+
+/*
+ * Contains L.MultiPolyline and L.MultiPolygon layers.
+ */
+
+(function () {
+	function createMulti(Klass) {
+		return L.FeatureGroup.extend({
+			initialize: function (latlngs, options) {
+				this._layers = {};
+				this._options = options;
+				this.setLatLngs(latlngs);
+			},
+
+			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));
+				}
+
+				return this;
+			}
+		});
+	}
+
+	L.MultiPolyline = createMulti(L.Polyline);
+	L.MultiPolygon = createMulti(L.Polygon);
+}());
+
+
+/*
+ * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds
+ */
+
+L.Rectangle = L.Polygon.extend({
+	initialize: function (latLngBounds, options) {
+		L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
+	},
+
+	setBounds: function (latLngBounds) {
+		this.setLatLngs(this._boundsToLatLngs(latLngBounds));
+	},
+	
+	_boundsToLatLngs: function (latLngBounds) {
+	    return [
+	        latLngBounds.getSouthWest(),
+	        latLngBounds.getNorthWest(),
+	        latLngBounds.getNorthEast(),
+	        latLngBounds.getSouthEast(),
+	        latLngBounds.getSouthWest()
+	    ];
+	}
+});
+
+
+/*
+ * L.Circle is a circle overlay (with a certain radius in meters).
+ */
+
+L.Circle = L.Path.extend({
+	initialize: function (latlng, radius, options) {
+		L.Path.prototype.initialize.call(this, options);
+
+		this._latlng = latlng;
+		this._mRadius = radius;
+	},
+
+	options: {
+		fill: true
+	},
+
+	setLatLng: function (latlng) {
+		this._latlng = latlng;
+		return this.redraw();
+	},
+
+	setRadius: function (radius) {
+		this._mRadius = radius;
+		return this.redraw();
+	},
+
+	projectLatlngs: function () {
+		var lngRadius = this._getLngRadius(),
+			latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngRadius, true),
+			point2 = this._map.latLngToLayerPoint(latlng2);
+
+		this._point = this._map.latLngToLayerPoint(this._latlng);
+		this._radius = Math.max(Math.round(this._point.x - point2.x), 1);
+	},
+
+	getBounds: function () {
+		var map = this._map,
+			delta = this._radius * Math.cos(Math.PI / 4),
+			point = map.project(this._latlng),
+			swPoint = new L.Point(point.x - delta, point.y + delta),
+			nePoint = new L.Point(point.x + delta, point.y - delta),
+			zoom = map.getZoom(),
+			sw = map.unproject(swPoint, zoom, true),
+			ne = map.unproject(nePoint, zoom, true);
+
+		return new L.LatLngBounds(sw, ne);
+	},
+	
+	getLatLng: function () {
+		return this._latlng;
+	},
+
+	getPathString: function () {
+		var p = this._point,
+			r = this._radius;
+
+		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);
+		}
+	},
+	
+	getRadius: function () {
+		return this._mRadius;
+	},
+
+	_getLngRadius: function () {
+		var equatorLength = 40075017,
+			hLength = equatorLength * Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
+
+		return (this._mRadius / hLength) * 360;
+	},
+
+	_checkIfEmpty: function () {
+		if (!this._map) {
+			return false;
+		}
+		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;
+	}
+});
+
+
+/*
+ * L.CircleMarker is a circle overlay with a permanent pixel radius.
+ */
+
+L.CircleMarker = L.Circle.extend({
+	options: {
+		radius: 10,
+		weight: 2
+	},
+
+	initialize: function (latlng, options) {
+		L.Circle.prototype.initialize.call(this, latlng, null, options);
+		this._radius = this.options.radius;
+	},
+
+	projectLatlngs: function () {
+		this._point = this._map.latLngToLayerPoint(this._latlng);
+	},
+
+	setRadius: function (radius) {
+		this._radius = radius;
+		return this.redraw();
+	}
+});
+
+
+
+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;
+	}
+});
+
+
+
+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;
+	}
+});
+
+
+/*
+ * 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, false);
+	},
+
+	_containsPoint: function (p) {
+		var center = this._point,
+			w2 = this.options.stroke ? this.options.weight / 2 : 0;
+
+		return (p.distanceTo(center) <= this._radius + w2);
+	}
+});
+
+
+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) {
+		var features = geojson.features,
+		    i, len;
+
+		if (features) {
+			for (i = 0, len = features.length; i < len; i++) {
+				this.addGeoJSON(features[i]);
+			}
+			return;
+		}
+
+		var isFeature = (geojson.type === 'Feature'),
+		    geometry = isFeature ? geojson.geometry : geojson,
+		    layer = L.GeoJSON.geometryToLayer(geometry, this.options.pointToLayer);
+
+		this.fire('featureparse', {
+			layer: layer,
+			properties: geojson.properties,
+			geometryType: geometry.type,
+			bbox: geojson.bbox,
+			id: geojson.id,
+			geometry: geojson.geometry
+		});
+
+		this.addLayer(layer);
+	}
+});
+
+L.Util.extend(L.GeoJSON, {
+	geometryToLayer: function (geometry, pointToLayer) {
+		var coords = geometry.coordinates,
+		    layers = [],
+		    latlng, latlngs, i, len, layer;
+
+		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], pointToLayer);
+				layers.push(layer);
+			}
+			return new L.FeatureGroup(layers);
+
+		default:
+			throw new Error('Invalid GeoJSON object.');
+		}
+	},
+
+	coordsToLatLng: function (coords, reverse) { // (Array, Boolean) -> LatLng
+		var lat = parseFloat(coords[reverse ? 0 : 1]),
+		    lng = parseFloat(coords[reverse ? 1 : 0]);
+
+		return new L.LatLng(lat, lng, true);
+	},
+
+	coordsToLatLngs: function (coords, levelsDeep, reverse) { // (Array, Number, Boolean) -> Array
+		var latlng,
+		    latlngs = [],
+		    i, len;
+
+		for (i = 0, len = coords.length; i < len; i++) {
+			latlng = levelsDeep ?
+					this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) :
+					this.coordsToLatLng(coords[i], reverse);
+			latlngs.push(latlng);
+		}
+
+		return latlngs;
+	}
+});
+
+
+/*
+ * L.DomEvent contains functions for working with DOM events.
+ */
+
+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),
+			key = '_leaflet_' + type + id;
+
+		if (obj[key]) {
+			return this;
+		}
+
+		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') {
+				obj.addEventListener('DOMMouseScroll', handler, false);
+				obj.addEventListener(type, handler, false);
+			} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
+				var originalHandler = handler,
+					newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
+				handler = function (e) {
+					if (!L.DomEvent._checkMouse(obj, e)) {
+						return;
+					}
+					return originalHandler(e);
+				};
+				obj.addEventListener(newType, handler, false);
+			} else {
+				obj.addEventListener(type, handler, false);
+			}
+		} else if ('attachEvent' in obj) {
+			obj.attachEvent("on" + type, handler);
+		}
+
+		obj[key] = handler;
+
+		return this;
+	},
+
+	removeListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn) {
+		var id = L.Util.stamp(fn),
+			key = '_leaflet_' + type + id,
+			handler = obj[key];
+
+		if (!handler) {
+			return;
+		}
+
+		if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
+			this.removeDoubleTapListener(obj, id);
+		} else if ('removeEventListener' in obj) {
+			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 {
+				obj.removeEventListener(type, handler, false);
+			}
+		} else if ('detachEvent' in obj) {
+			obj.detachEvent("on" + type, handler);
+		}
+		obj[key] = null;
+
+		return this;
+	},
+
+	_checkMouse: function (el, e) {
+		var related = e.relatedTarget;
+
+		if (!related) {
+			return true;
+		}
+
+		try {
+			while (related && (related !== el)) {
+				related = related.parentNode;
+			}
+		} catch (err) {
+			return false;
+		}
+
+		return (related !== el);
+	},
+
+	/*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 && window.Event === e.constructor) {
+					break;
+				}
+				caller = caller.caller;
+			}
+		}
+		return e;
+	},
+	/*jshint noarg:false */
+
+	stopPropagation: function (/*Event*/ e) {
+		if (e.stopPropagation) {
+			e.stopPropagation();
+		} else {
+			e.cancelBubble = true;
+		}
+		return this;
+	},
+
+	disableClickPropagation: function (/*HTMLElement*/ el) {
+		return L.DomEvent
+			.addListener(el, L.Draggable.START, L.DomEvent.stopPropagation)
+			.addListener(el, 'click', L.DomEvent.stopPropagation)
+			.addListener(el, 'dblclick', L.DomEvent.stopPropagation);
+	},
+
+	preventDefault: function (/*Event*/ e) {
+		if (e.preventDefault) {
+			e.preventDefault();
+		} else {
+			e.returnValue = false;
+		}
+		return this;
+	},
+
+	stop: function (e) {
+		return L.DomEvent
+			.preventDefault(e)
+			.stopPropagation(e);
+	},
+
+	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 +
+					document.body.scrollTop + document.documentElement.scrollTop,
+			pos = new L.Point(x, y);
+
+		return (container ?
+					pos.subtract(L.DomUtil.getViewportOffset(container)) : pos);
+	},
+
+	getWheelDelta: function (e) {
+		var delta = 0;
+		if (e.wheelDelta) {
+			delta = e.wheelDelta / 120;
+		}
+		if (e.detail) {
+			delta = -e.detail / 3;
+		}
+		return delta;
+	}
+};
+
+
+
+/*
+ * L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too.
+ */
+
+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) {
+		this._element = element;
+		this._dragStartTarget = dragStartTarget || element;
+	},
+
+	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;
+		}
+		L.DomEvent.removeListener(this._dragStartTarget, L.Draggable.START, this._onDown);
+		this._enabled = false;
+		this._moved = false;
+	},
+
+	_onDown: function (e) {
+		if ((!L.Browser.touch && e.shiftKey) || ((e.which !== 1) && (e.button !== 1) && !e.touches)) {
+			return;
+		}
+
+		this._simulateClick = true;
+
+		if (e.touches && e.touches.length > 1) {
+			this._simulateClick = false;
+			return;
+		}
+
+		var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
+			el = first.target;
+
+		L.DomEvent.preventDefault(e);
+
+		if (L.Browser.touch && el.tagName.toLowerCase() === 'a') {
+			el.className += ' leaflet-active';
+		}
+
+		this._moved = false;
+		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;
+		}
+
+		L.DomEvent.preventDefault(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.cancelAnimFrame(this._animRequest);
+		this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
+	},
+
+	_updatePosition: function () {
+		this.fire('predrag');
+		L.DomUtil.setPosition(this._element, this._newPos);
+		this.fire('drag');
+	},
+
+	_onUp: function (e) {
+		if (this._simulateClick && e.changedTouches) {
+			var first = e.changedTouches[0],
+				el = first.target,
+				dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0;
+
+			if (el.tagName.toLowerCase() === 'a') {
+				el.className = el.className.replace(' leaflet-active', '');
+			}
+
+			if (dist < L.Draggable.TAP_TOLERANCE) {
+				this._simulateEvent('click', first);
+			}
+		}
+
+		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;
+	},
+
+	_setMovingCursor: function () {
+		document.body.className += ' leaflet-dragging';
+	},
+
+	_restoreCursor: function () {
+		document.body.className = document.body.className.replace(/ leaflet-dragging/g, '');
+	},
+
+	_simulateEvent: function (type, e) {
+		var simulatedEvent = document.createEvent('MouseEvents');
+
+		simulatedEvent.initMouseEvent(
+				type, true, true, window, 1,
+				e.screenX, e.screenY,
+				e.clientX, e.clientY,
+				false, false, false, false, 0, null);
+
+		e.target.dispatchEvent(simulatedEvent);
+	}
+});
+
+
+/*
+ * 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;
+	}
+});
+
+
+/*
+ * L.Handler.MapDrag is used internally by L.Map to make the map draggable.
+ */
+
+L.Map.mergeOptions({
+	dragging: true,
+
+	inertia: !L.Browser.android,
+	inertiaDeceleration: L.Browser.touch ? 3000 : 2000, // px/s^2
+	inertiaMaxSpeed:     L.Browser.touch ? 1500 : 1000, // px/s
+	inertiaThreshold:    L.Browser.touch ? 32   : 16, // ms
+
+	// TODO refactor, move to CRS
+	worldCopyJump: true,
+	continuousWorld: false
+});
+
+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 () {
+		var map = this._map;
+
+		map
+			.fire('movestart')
+			.fire('dragstart');
+
+		if (map._panTransition) {
+			map._panTransition._onTransitionEnd(true);
+		}
+
+		if (map.options.inertia) {
+			this._positions = [];
+			this._times = [];
+		}
+	},
+
+	_onDrag: function () {
+		if (this._map.options.inertia) {
+			var time = this._lastTime = +new Date(),
+			    pos = this._lastPos = this._draggable._newPos;
+
+			this._positions.push(pos);
+			this._times.push(time);
+
+			if (time - this._times[0] > 200) {
+				this._positions.shift();
+				this._times.shift();
+			}
+		}
+
+		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.crs.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,
+			options = map.options,
+			delay = +new Date() - this._lastTime,
+
+			noInertia = !options.inertia ||
+					delay > options.inertiaThreshold ||
+					typeof this._positions[0] === 'undefined';
+
+		if (noInertia) {
+			map.fire('moveend');
+
+		} else {
+
+			var direction = this._lastPos.subtract(this._positions[0]),
+				duration = (this._lastTime + delay - this._times[0]) / 1000,
+
+				speedVector = direction.multiplyBy(0.58 / duration),
+				speed = speedVector.distanceTo(new L.Point(0, 0)),
+
+				limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
+				limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
+
+				decelerationDuration = limitedSpeed / options.inertiaDeceleration,
+				offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
+
+			var panOptions = {
+				duration: decelerationDuration,
+				easing: 'ease-out'
+			};
+
+			L.Util.requestAnimFrame(L.Util.bind(function () {
+				this._map.panBy(offset, panOptions);
+			}, this));
+		}
+
+		map.fire('dragend');
+
+		if (options.maxBounds) {
+			// TODO predrag validation instead of animation
+			L.Util.requestAnimFrame(this._panInsideMaxBounds, map, true, map._container);
+		}
+	},
+
+	_panInsideMaxBounds: function () {
+		this.panInsideBounds(this.options.maxBounds);
+	}
+});
+
+L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);
+
+/*
+ * L.Handler.DoubleClickZoom is used internally by L.Map to add double-click zooming.
+ */
+
+L.Map.mergeOptions({
+	doubleClickZoom: true
+});
+
+L.Map.DoubleClickZoom = L.Handler.extend({
+	addHooks: function () {
+		this._map.on('dblclick', this._onDoubleClick);
+	},
+
+	removeHooks: function () {
+		this._map.off('dblclick', this._onDoubleClick);
+	},
+
+	_onDoubleClick: function (e) {
+		this.setView(e.latlng, this._zoom + 1);
+	}
+});
+
+L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom);
+
+/*
+ * L.Handler.ScrollWheelZoom is used internally by L.Map to enable mouse scroll wheel zooming on the map.
+ */
+
+L.Map.mergeOptions({
+	scrollWheelZoom: !L.Browser.touch
+});
+
+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);
+	}
+});
+
+L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);
+
+L.Util.extend(L.DomEvent, {
+	// inspired by Zepto touch code by Thomas Fuchs
+	addDoubleTapListener: function (obj, handler, id) {
+		var last,
+			doubleTap = false,
+			delay = 250,
+			touch,
+			pre = '_leaflet_',
+			touchstart = 'touchstart',
+			touchend = 'touchend';
+
+		function onTouchStart(e) {
+			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;
+		}
+		function onTouchEnd(e) {
+			if (doubleTap) {
+				touch.type = 'dblclick';
+				handler(touch);
+				last = null;
+			}
+		}
+		obj[pre + touchstart + id] = onTouchStart;
+		obj[pre + touchend + id] = onTouchEnd;
+
+		obj.addEventListener(touchstart, onTouchStart, false);
+		obj.addEventListener(touchend, onTouchEnd, false);
+	},
+
+	removeDoubleTapListener: function (obj, id) {
+		var pre = '_leaflet_';
+		obj.removeEventListener(obj, obj[pre + 'touchstart' + id], false);
+		obj.removeEventListener(obj, obj[pre + 'touchend' + id], false);
+	}
+});
+
+
+/*
+ * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
+ */
+
+L.Map.mergeOptions({
+	touchZoom: L.Browser.touch && !L.Browser.android
+});
+
+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) {
+		var map = this._map;
+
+		if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
+
+		var p1 = map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = map.mouseEventToLayerPoint(e.touches[1]),
+			viewCenter = map.containerPointToLayerPoint(map.getSize().divideBy(2));
+
+		this._startCenter = p1.add(p2).divideBy(2, true);
+		this._startDist = p1.distanceTo(p2);
+
+		this._moved = false;
+		this._zooming = true;
+
+		this._centerOffset = viewCenter.subtract(this._startCenter);
+
+		L.DomEvent
+			.addListener(document, 'touchmove', this._onTouchMove, this)
+			.addListener(document, 'touchend', this._onTouchEnd, this);
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_onTouchMove: function (e) {
+		if (!e.touches || e.touches.length !== 2) { return; }
+
+		var map = this._map;
+
+		var p1 = map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = map.mouseEventToLayerPoint(e.touches[1]);
+
+		this._scale = p1.distanceTo(p2) / this._startDist;
+		this._delta = p1.add(p2).divideBy(2, true).subtract(this._startCenter);
+
+		if (this._scale === 1) { return; }
+
+		var zoom = this._map._zoom + Math.log(this._scale) / Math.LN2;
+
+		var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
+			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
+			center = this._map.unproject(centerPoint);
+
+		if (!this._moved) {
+			map._mapPane.className += ' leaflet-zoom-anim leaflet-touching';
+
+			map
+				.fire('movestart')
+				.fire('zoomstart')
+				._prepareTileBg();
+
+			this._moved = true;
+		}
+
+		map.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
+
+		// 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
+
+		map._tileBg.style[L.DomUtil.TRANSFORM] =
+			L.DomUtil.getTranslateString(this._delta) + ' ' +
+            L.DomUtil.getScaleString(this._scale, this._startCenter);
+
+		L.DomEvent.preventDefault(e);
+	},
+
+	_onTouchEnd: function (e) {
+		if (!this._moved || !this._zooming) { return; }
+
+		this._zooming = false;
+		this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-touching', ''); //TODO toggleClass util
+
+		L.DomEvent
+			.removeListener(document, 'touchmove', this._onTouchMove)
+			.removeListener(document, 'touchend', this._onTouchEnd);
+
+		var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
+			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
+			center = this._map.unproject(centerPoint),
+
+			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),
+			finalScale = Math.pow(2, zoom - oldZoom);
+
+		this._map.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
+
+		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset), true);
+	}
+});
+
+L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
+
+/*
+ * L.Handler.ShiftDragZoom is used internally by L.Map to add shift-drag zoom (zoom to a selected bounding box).
+ */
+
+L.Map.mergeOptions({
+	boxZoom: true
+});
+
+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 refactor: move cursor to styles
+		this._container.style.cursor = 'crosshair';
+
+		L.DomEvent
+			.addListener(document, 'mousemove', this._onMouseMove, this)
+			.addListener(document, 'mouseup', this._onMouseUp, this)
+			.preventDefault(e);
+			
+		this._map.fire("boxzoomstart");
+	},
+
+	_onMouseMove: function (e) {
+		var startPoint = this._startLayerPoint,
+			box = this._box,
+
+			layerPoint = this._map.mouseEventToLayerPoint(e),
+			offset = layerPoint.subtract(startPoint),
+
+			newPos = new L.Point(
+				Math.min(layerPoint.x, startPoint.x),
+				Math.min(layerPoint.y, startPoint.y));
+
+		L.DomUtil.setPosition(box, newPos);
+
+		// TODO refactor: remove hardcoded 4 pixels
+		box.style.width  = (Math.abs(offset.x) - 4) + 'px';
+		box.style.height = (Math.abs(offset.y) - 4) + 'px';
+	},
+
+	_onMouseUp: function (e) {
+		this._pane.removeChild(this._box);
+		this._container.style.cursor = '';
+
+		L.DomUtil.enableTextSelection();
+
+		L.DomEvent
+			.removeListener(document, 'mousemove', this._onMouseMove)
+			.removeListener(document, 'mouseup', this._onMouseUp);
+
+		var map = this._map,
+			layerPoint = map.mouseEventToLayerPoint(e);
+
+		var bounds = new L.LatLngBounds(
+				map.layerPointToLatLng(this._startLayerPoint),
+				map.layerPointToLatLng(layerPoint));
+
+		map.fitBounds(bounds);
+		
+		map.fire("boxzoomend", {
+			boxZoomBounds: bounds
+		});
+	}
+});
+
+L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom);
+
+
+/*
+ * 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)
+				.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');
+	}
+});
+
+
+L.Handler.PolyEdit = L.Handler.extend({
+	options: {
+		icon: new L.DivIcon({
+			iconSize: new L.Point(8, 8),
+			className: 'leaflet-div-icon leaflet-editing-icon'
+		})
+	},
+
+	initialize: function (poly, options) {
+		this._poly = poly;
+		L.Util.setOptions(this, options);
+	},
+
+	addHooks: function () {
+		if (this._poly._map) {
+			if (!this._markerGroup) {
+				this._initMarkers();
+			}
+			this._poly._map.addLayer(this._markerGroup);
+		}
+	},
+
+	removeHooks: function () {
+		if (this._poly._map) {
+			this._poly._map.removeLayer(this._markerGroup);
+			delete this._markerGroup;
+			delete this._markers;
+		}
+	},
+
+	updateMarkers: function () {
+		this._markerGroup.clearLayers();
+		this._initMarkers();
+	},
+
+	_initMarkers: function () {
+		if (!this._markerGroup) {
+			this._markerGroup = new L.LayerGroup();
+		}
+		this._markers = [];
+
+		var latlngs = this._poly._latlngs,
+		    i, j, len, marker;
+
+		// TODO refactor holes implementation in Polygon to support it here
+
+		for (i = 0, len = latlngs.length; i < len; i++) {
+
+			marker = this._createMarker(latlngs[i], i);
+			marker.on('click', this._onMarkerClick, this);
+			this._markers.push(marker);
+		}
+
+		var markerLeft, markerRight;
+
+		for (i = 0, j = len - 1; i < len; j = i++) {
+			if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) {
+				continue;
+			}
+
+			markerLeft = this._markers[j];
+			markerRight = this._markers[i];
+
+			this._createMiddleMarker(markerLeft, markerRight);
+			this._updatePrevNext(markerLeft, markerRight);
+		}
+	},
+
+	_createMarker: function (latlng, index) {
+		var marker = new L.Marker(latlng, {
+			draggable: true,
+			icon: this.options.icon
+		});
+
+		marker._origLatLng = latlng;
+		marker._index = index;
+
+		marker.on('drag', this._onMarkerDrag, this);
+		marker.on('dragend', this._fireEdit, this);
+
+		this._markerGroup.addLayer(marker);
+
+		return marker;
+	},
+
+	_fireEdit: function () {
+		this._poly.fire('edit');
+	},
+
+	_onMarkerDrag: function (e) {
+		var marker = e.target;
+
+		L.Util.extend(marker._origLatLng, marker._latlng);
+
+		if (marker._middleLeft) {
+			marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker));
+		}
+		if (marker._middleRight) {
+			marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next));
+		}
+
+		this._poly.redraw();
+	},
+
+	_onMarkerClick: function (e) {
+		// Default action on marker click is to remove that marker, but if we remove the marker when latlng count < 3, we don't have a valid polyline anymore
+		if (this._poly._latlngs.length < 3) {
+			return;
+		}
+		
+		var marker = e.target,
+		    i = marker._index;
+		
+		// Check existence of previous and next markers since they wouldn't exist for edge points on the polyline
+		if (marker._prev && marker._next) {
+			this._createMiddleMarker(marker._prev, marker._next);
+			this._updatePrevNext(marker._prev, marker._next);
+		}
+
+		// The marker itself is guaranteed to exist and present in the layer, since we managed to click on it
+		this._markerGroup.removeLayer(marker);
+		// Check for the existence of middle left or middle right
+		if (marker._middleLeft) {
+			this._markerGroup.removeLayer(marker._middleLeft);
+		}
+		if (marker._middleRight) {
+			this._markerGroup.removeLayer(marker._middleRight);
+		}
+		this._markers.splice(i, 1);
+		this._poly.spliceLatLngs(i, 1);
+		this._updateIndexes(i, -1);
+		this._poly.fire('edit');
+	},
+
+	_updateIndexes: function (index, delta) {
+		this._markerGroup._iterateLayers(function (marker) {
+			if (marker._index > index) {
+				marker._index += delta;
+			}
+		});
+	},
+
+	_createMiddleMarker: function (marker1, marker2) {
+		var latlng = this._getMiddleLatLng(marker1, marker2),
+			marker = this._createMarker(latlng),
+			onClick,
+			onDragStart,
+			onDragEnd;
+
+		marker.setOpacity(0.6);
+
+		marker1._middleRight = marker2._middleLeft = marker;
+
+		onDragStart = function () {
+			var i = marker2._index;
+
+			marker._index = i;
+
+			marker
+				.off('click', onClick)
+				.on('click', this._onMarkerClick, this);
+
+			latlng.lat = marker.getLatLng().lat;
+			latlng.lng = marker.getLatLng().lng;
+			this._poly.spliceLatLngs(i, 0, latlng);
+			this._markers.splice(i, 0, marker);
+
+			marker.setOpacity(1);
+
+			this._updateIndexes(i, 1);
+			marker2._index++;
+			this._updatePrevNext(marker1, marker);
+			this._updatePrevNext(marker, marker2);
+		};
+
+		onDragEnd = function () {
+			marker.off('dragstart', onDragStart, this);
+			marker.off('dragend', onDragEnd, this);
+
+			this._createMiddleMarker(marker1, marker);
+			this._createMiddleMarker(marker, marker2);
+		};
+
+		onClick = function () {
+			onDragStart.call(this);
+			onDragEnd.call(this);
+			this._poly.fire('edit');
+		};
+
+		marker
+			.on('click', onClick, this)
+			.on('dragstart', onDragStart, this)
+			.on('dragend', onDragEnd, this);
+
+		this._markerGroup.addLayer(marker);
+	},
+
+	_updatePrevNext: function (marker1, marker2) {
+		marker1._next = marker2;
+		marker2._prev = marker1;
+	},
+
+	_getMiddleLatLng: function (marker1, marker2) {
+		var map = this._poly._map,
+		    p1 = map.latLngToLayerPoint(marker1.getLatLng()),
+		    p2 = map.latLngToLayerPoint(marker2.getLatLng());
+
+		return map.layerPointToLatLng(p1._add(p2).divideBy(2));
+	}
+});
+
+
+
+L.Control = L.Class.extend({
+	options: {
+		position: 'topright'
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+	},
+
+	getPosition: function () {
+		return this.options.position;
+	},
+
+	setPosition: function (position) {
+		var map = this._map;
+
+		if (map) {
+			map.removeControl(this);
+		}
+
+		this.options.position = position;
+
+		if (map) {
+			map.addControl(this);
+		}
+	},
+
+	addTo: function (map) {
+		this._map = map;
+
+		var container = this._container = this.onAdd(map),
+		    pos = this.getPosition(),
+			corner = map._controlCorners[pos];
+
+		L.DomUtil.addClass(container, 'leaflet-control');
+
+		if (pos.indexOf('bottom') !== -1) {
+			corner.insertBefore(container, corner.firstChild);
+		} else {
+			corner.appendChild(container);
+		}
+
+		return this;
+	},
+
+	removeFrom: function (map) {
+		var pos = this.getPosition(),
+			corner = map._controlCorners[pos];
+
+		corner.removeChild(this._container);
+		this._map = null;
+
+		if (this.onRemove) {
+			this.onRemove(map);
+		}
+
+		return this;
+	}
+});
+
+
+
+L.Map.include({
+	addControl: function (control) {
+		control.addTo(this);
+		return this;
+	},
+
+	removeControl: function (control) {
+		control.removeFrom(this);
+		return this;
+	},
+
+	_initControlPos: function () {
+		var corners = this._controlCorners = {},
+		    l = 'leaflet-',
+		    container = this._controlContainer =
+				L.DomUtil.create('div', l + 'control-container', this._container);
+
+		function createCorner(vSide, hSide) {
+			var className = l + vSide + ' ' + l + hSide;
+
+			corners[vSide + hSide] =
+					L.DomUtil.create('div', className, container);
+		}
+
+		createCorner('top', 'left');
+		createCorner('top', 'right');
+		createCorner('bottom', 'left');
+		createCorner('bottom', 'right');
+	}
+});
+
+
+L.Control.Zoom = L.Control.extend({
+	options: {
+		position: 'topleft'
+	},
+
+	onAdd: function (map) {
+		var className = 'leaflet-control-zoom',
+		    container = L.DomUtil.create('div', className);
+
+		this._createButton('Zoom in', className + '-in', container, map.zoomIn, map);
+		this._createButton('Zoom out', className + '-out', container, map.zoomOut, map);
+
+		return container;
+	},
+
+	_createButton: function (title, className, container, fn, context) {
+		var link = L.DomUtil.create('a', className, container);
+		link.href = '#';
+		link.title = title;
+
+		L.DomEvent
+			.addListener(link, 'click', L.DomEvent.stopPropagation)
+			.addListener(link, 'click', L.DomEvent.preventDefault)
+			.addListener(link, 'click', fn, context);
+
+		return link;
+	}
+});
+
+L.Map.mergeOptions({
+	zoomControl: true
+});
+
+L.Map.addInitHook(function () {
+	if (this.options.zoomControl) {
+		this.zoomControl = new L.Control.Zoom();
+		this.addControl(this.zoomControl);
+	}
+});
+
+L.Control.Attribution = L.Control.extend({
+	options: {
+		position: 'bottomright',
+		prefix: 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>'
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+
+		this._attributions = {};
+	},
+
+	onAdd: function (map) {
+		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
+		L.DomEvent.disableClickPropagation(this._container);
+
+		map
+			.on('layeradd', this._onLayerAdd, this)
+			.on('layerremove', this._onLayerRemove, this);
+
+		this._update();
+
+		return this._container;
+	},
+
+	onRemove: function (map) {
+		map
+			.off('layeradd', this._onLayerAdd)
+			.off('layerremove', this._onLayerRemove);
+
+	},
+
+	setPrefix: function (prefix) {
+		this.options.prefix = prefix;
+		this._update();
+	},
+
+	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; }
+
+		this._attributions[text]--;
+		this._update();
+	},
+
+	_update: function () {
+		if (!this._map) { return; }
+
+		var attribs = [];
+
+		for (var i in this._attributions) {
+			if (this._attributions.hasOwnProperty(i) && this._attributions[i]) {
+				attribs.push(i);
+			}
+		}
+
+		var prefixAndAttribs = [];
+
+		if (this.options.prefix) {
+			prefixAndAttribs.push(this.options.prefix);
+		}
+		if (attribs.length) {
+			prefixAndAttribs.push(attribs.join(', '));
+		}
+
+		this._container.innerHTML = prefixAndAttribs.join(' — ');
+	},
+
+	_onLayerAdd: function (e) {
+		if (e.layer.getAttribution) {
+			this.addAttribution(e.layer.getAttribution());
+		}
+	},
+
+	_onLayerRemove: function (e) {
+		if (e.layer.getAttribution) {
+			this.removeAttribution(e.layer.getAttribution());
+		}
+	}
+});
+
+L.Map.mergeOptions({
+	attributionControl: true
+});
+
+L.Map.addInitHook(function () {
+	if (this.options.attributionControl) {
+		this.attributionControl = (new L.Control.Attribution()).addTo(this);
+	}
+});
+
+L.Control.Scale = L.Control.extend({
+	options: {
+		position: 'bottomleft',
+		maxWidth: 100,
+		metric: true,
+		imperial: true,
+		updateWhenIdle: false
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		var className = 'leaflet-control-scale',
+		    container = L.DomUtil.create('div', className),
+		    options = this.options;
+
+		if (options.metric) {
+			this._mScale = L.DomUtil.create('div', className + '-line', container);
+		}
+		if (options.imperial) {
+			this._iScale = L.DomUtil.create('div', className + '-line', container);
+		}
+
+		map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
+		this._update();
+
+		return container;
+	},
+
+	onRemove: function (map) {
+		map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
+	},
+
+	_update: function () {
+		var bounds = this._map.getBounds(),
+		    centerLat = bounds.getCenter().lat,
+
+		    left = new L.LatLng(centerLat, bounds.getSouthWest().lng),
+		    right = new L.LatLng(centerLat, bounds.getNorthEast().lng),
+
+		    size = this._map.getSize(),
+		    options = this.options,
+
+		    maxMeters = left.distanceTo(right) * (options.maxWidth / size.x);
+
+		if (options.metric) {
+			this._updateMetric(maxMeters);
+		}
+
+		if (options.imperial) {
+			this._updateImperial(maxMeters);
+		}
+	},
+
+	_updateMetric: function (maxMeters) {
+		var meters = this._getRoundNum(maxMeters);
+
+		this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px';
+		this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
+	},
+
+	_updateImperial: function (maxMeters) {
+		var maxFeet = maxMeters * 3.2808399,
+			scale = this._iScale,
+			maxMiles, miles, feet;
+
+		if (maxFeet > 5280) {
+			maxMiles = maxFeet / 5280;
+			miles = this._getRoundNum(maxMiles);
+
+			scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
+			scale.innerHTML = miles + ' mi';
+
+		} else {
+			feet = this._getRoundNum(maxFeet);
+
+			scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
+			scale.innerHTML = feet + ' ft';
+		}
+	},
+
+	_getScaleWidth: function (ratio) {
+		return Math.round(this.options.maxWidth * ratio) - 10;
+	},
+
+	_getRoundNum: function (num) {
+		var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
+		    d = num / pow10;
+
+		d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 2 ? 2 : 1;
+
+		return pow10 * d;
+	}
+});
+
+
+L.Control.Layers = L.Control.extend({
+	options: {
+		collapsed: true,
+		position: 'topright'
+	},
+
+	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._initLayout();
+		this._update();
+
+		return this._container;
+	},
+
+	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 () {
+		var className = 'leaflet-control-layers',
+		    container = this._container = L.DomUtil.create('div', className);
+
+		if (!L.Browser.touch) {
+			L.DomEvent.disableClickPropagation(container);
+		} else {
+			L.DomEvent.addListener(container, 'click', L.DomEvent.stopPropagation);
+		}
+
+		var form = this._form = L.DomUtil.create('form', className + '-list');
+
+		if (this.options.collapsed) {
+			L.DomEvent
+				.addListener(container, 'mouseover', this._expand, this)
+				.addListener(container, 'mouseout', this._collapse, this);
+
+			var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
+			link.href = '#';
+			link.title = 'Layers';
+
+			if (L.Browser.touch) {
+				L.DomEvent
+					.addListener(link, 'click', L.DomEvent.stopPropagation)
+					.addListener(link, 'click', L.DomEvent.preventDefault)
+					.addListener(link, 'click', this._expand, this);
+			}
+			else {
+				L.DomEvent.addListener(link, 'focus', this._expand, this);
+			}
+
+			this._map.on('movestart', this._collapse, this);
+			// TODO keyboard accessibility
+		} else {
+			this._expand();
+		}
+
+		this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
+		this._separator = L.DomUtil.create('div', className + '-separator', form);
+		this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
+
+		container.appendChild(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.layerId = L.Util.stamp(obj.layer);
+		input.defaultChecked = this._map.hasLayer(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', '');
+	}
+});
+
+
+L.Transition = L.Class.extend({
+	includes: L.Mixin.Events,
+
+	statics: {
+		CUSTOM_PROPS_SETTERS: {
+			position: L.DomUtil.setPosition
+			//TODO transform custom attr
+		},
+
+		implemented: function () {
+			return L.Transition.NATIVE || L.Transition.TIMER;
+		}
+	},
+
+	options: {
+		easing: 'ease',
+		duration: 0.5
+	},
+
+	_setProperty: function (prop, value) {
+		var setters = L.Transition.CUSTOM_PROPS_SETTERS;
+		if (prop in setters) {
+			setters[prop](this._el, value);
+		} else {
+			this._el.style[prop] = value;
+		}
+	}
+});
+
+
+/*
+ * L.Transition native implementation that powers Leaflet animation
+ * in browsers that support CSS3 Transitions
+ */
+
+L.Transition = L.Transition.extend({
+	statics: (function () {
+		var transition = L.DomUtil.TRANSITION,
+			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.any3d ? L.DomUtil.TRANSFORM : 'top, left'
+			}
+		};
+	}()),
+
+	options: {
+		fakeStepInterval: 100
+	},
+
+	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) {
+		var prop,
+			propsList = [],
+			customProp = L.Transition.CUSTOM_PROPS_PROPERTIES;
+
+		for (prop in props) {
+			if (props.hasOwnProperty(prop)) {
+				prop = customProp[prop] ? customProp[prop] : prop;
+				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();
+		}
+	},
+
+	_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 (e) {
+		if (this._inProgress) {
+			this._inProgress = false;
+			clearInterval(this._timer);
+
+			this._el.style[L.Transition.TRANSITION] = '';
+
+			this.fire('step');
+
+			if (e && e.type) {
+				this.fire('end');
+			}
+		}
+	}
+});
+
+
+/*
+ * L.Transition fallback implementation that powers Leaflet animation
+ * in browsers that don't support CSS3 Transitions
+ */
+
+L.Transition = L.Transition.NATIVE ? L.Transition : L.Transition.extend({
+	statics: {
+		getTime: Date.now || function () {
+			return +new Date();
+		},
+
+		TIMER: true,
+
+		EASINGS: {
+			'ease': [0.25, 0.1, 0.25, 1.0],
+			'linear': [0.0, 0.0, 1.0, 1.0],
+			'ease-in': [0.42, 0, 1.0, 1.0],
+			'ease-out': [0, 0, 0.58, 1.0],
+			'ease-in-out': [0.42, 0, 0.58, 1.0]
+		},
+
+		CUSTOM_PROPS_GETTERS: {
+			position: L.DomUtil.getPosition
+		},
+
+		//used to get units from strings like "10.5px" (->px)
+		UNIT_RE: /^[\d\.]+(\D*)$/
+	},
+
+	options: {
+		fps: 50
+	},
+
+	initialize: function (el, options) {
+		this._el = el;
+		L.Util.extend(this.options, options);
+
+		var easings = L.Transition.EASINGS[this.options.easing] || L.Transition.EASINGS.ease;
+
+		this._p1 = new L.Point(0, 0);
+		this._p2 = new L.Point(easings[0], easings[1]);
+		this._p3 = new L.Point(easings[2], easings[3]);
+		this._p4 = new L.Point(1, 1);
+
+		this._step = L.Util.bind(this._step, this);
+		this._interval = Math.round(1000 / this.options.fps);
+	},
+
+	run: function (props) {
+		this._props = {};
+
+		var getters = L.Transition.CUSTOM_PROPS_GETTERS,
+			re = L.Transition.UNIT_RE;
+
+		this.fire('start');
+
+		for (var prop in props) {
+			if (props.hasOwnProperty(prop)) {
+				var p = {};
+				if (prop in getters) {
+					p.from = getters[prop](this._el);
+				} else {
+					var matches = this._el.style[prop].match(re);
+					p.from = parseFloat(matches[0]);
+					p.unit = matches[1];
+				}
+				p.to = props[prop];
+				this._props[prop] = p;
+			}
+		}
+
+		clearInterval(this._timer);
+		this._timer = setInterval(this._step, this._interval);
+		this._startTime = L.Transition.getTime();
+	},
+
+	_step: function () {
+		var time = L.Transition.getTime(),
+			elapsed = time - this._startTime,
+			duration = this.options.duration * 1000;
+
+		if (elapsed < duration) {
+			this._runFrame(this._cubicBezier(elapsed / duration));
+		} else {
+			this._runFrame(1);
+			this._complete();
+		}
+	},
+
+	_runFrame: function (percentComplete) {
+		var setters = L.Transition.CUSTOM_PROPS_SETTERS,
+			prop, p, value;
+
+		for (prop in this._props) {
+			if (this._props.hasOwnProperty(prop)) {
+				p = this._props[prop];
+				if (prop in setters) {
+					value = p.to.subtract(p.from).multiplyBy(percentComplete).add(p.from);
+					setters[prop](this._el, value);
+				} else {
+					this._el.style[prop] =
+							((p.to - p.from) * percentComplete + p.from) + p.unit;
+				}
+			}
+		}
+		this.fire('step');
+	},
+
+	_complete: function () {
+		clearInterval(this._timer);
+		this.fire('end');
+	},
+
+	_cubicBezier: function (t) {
+		var a = Math.pow(1 - t, 3),
+			b = 3 * Math.pow(1 - t, 2) * t,
+			c = 3 * (1 - t) * Math.pow(t, 2),
+			d = Math.pow(t, 3),
+			p1 = this._p1.multiplyBy(a),
+			p2 = this._p2.multiplyBy(b),
+			p3 = this._p3.multiplyBy(c),
+			p4 = this._p4.multiplyBy(d);
+
+		return p1.add(p2).add(p3).add(p4).y;
+	}
+});
+
+
+
+L.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, options) {
+		if (!(offset.x || offset.y)) {
+			return this;
+		}
+
+		if (!this._panTransition) {
+			this._panTransition = new L.Transition(this._mapPane);
+
+			this._panTransition.on('step', this._onPanTransitionStep, this);
+			this._panTransition.on('end', this._onPanTransitionEnd, this);
+		}
+
+		L.Util.setOptions(this._panTransition, L.Util.extend({duration: 0.25}, options));
+
+		this.fire('movestart');
+
+		this._mapPane.className += ' leaflet-pan-anim';
+
+		this._panTransition.run({
+			position: L.DomUtil.getPosition(this._mapPane).subtract(offset)
+		});
+
+		return this;
+	},
+
+	_onPanTransitionStep: function () {
+		this.fire('move');
+	},
+
+	_onPanTransitionEnd: function () {
+		this._mapPane.className = this._mapPane.className.replace(/ leaflet-pan-anim/g, '');
+		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);
+	}
+});
+
+
+L.Map.mergeOptions({
+	zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android && !L.Browser.mobileOpera
+});
+
+L.Map.include(!L.DomUtil.TRANSITION ? {} : {
+	_zoomToIfCenterInView: function (center, zoom, centerOffset) {
+
+		if (this._animatingZoom) {
+			return true;
+		}
+		if (!this.options.zoomAnimation) {
+			return false;
+		}
+
+		var scale = Math.pow(2, zoom - this._zoom),
+			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');
+
+		//Hack: Disable this for android due to it not supporting double translate (mentioned in _runAnimation below)
+		//if Foreground layer doesn't have many tiles but bg layer does, keep the existing bg layer
+		if (!L.Browser.android && this._tileBg && this._getLoadedTilesPercentage(this._tileBg) > 0.5 && this._getLoadedTilesPercentage(this._tilePane) < 0.5) {
+			//Leave current bg and just zoom it some more
+
+			this._tilePane.style.visibility = 'hidden';
+			this._tilePane.empty = true;
+			this._stopLoadingImages(this._tilePane);
+		} else {
+			this._prepareTileBg();
+		}
+
+		var centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
+			origin = centerPoint.add(offset);
+
+		this.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
+		
+		this._runAnimation(center, zoom, scale, origin);
+
+		return true;
+	},
+
+
+	_runAnimation: function (center, zoom, scale, origin, backwardsTransform) {
+		this._animatingZoom = true;
+
+		this._animateToCenter = center;
+		this._animateToZoom = zoom;
+
+		var transform = L.DomUtil.TRANSFORM,
+			tileBg = this._tileBg;
+
+		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) {
+			tileBg.style[transform] += ' translate(0,0)';
+		}
+
+		var scaleStr;
+
+		// Android 2.* 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) {
+			tileBg.style[transform + 'Origin'] = origin.x + 'px ' + origin.y + 'px';
+			scaleStr = 'scale(' + scale + ')';
+		} else {
+			scaleStr = L.DomUtil.getScaleString(scale, origin);
+		}
+
+		L.Util.falseFn(tileBg.offsetWidth); //hack to make sure transform is updated before running animation
+
+		var options = {};
+		if (backwardsTransform) {
+			options[transform] = tileBg.style[transform] + ' ' + scaleStr;
+		} else {
+			options[transform] = scaleStr + ' ' + tileBg.style[transform];
+		}
+
+		tileBg.transition.run(options);
+	},
+
+	_prepareTileBg: function () {
+		var tilePane = this._tilePane,
+			tileBg = this._tileBg;
+
+		if (!tileBg) {
+			tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
+			tileBg.style.zIndex = 1;
+		}
+
+		// prepare the background pane to become the main tile pane
+		tileBg.style[L.DomUtil.TRANSFORM] = '';
+		tileBg.style.visibility = 'hidden';
+
+		// tells tile layers to reinitialize their containers
+		tileBg.empty = true; //new FG
+		tilePane.empty = false; //new BG
+
+		//Switch out the current layer to be the new bg layer (And vice-versa)
+		this._tilePane = this._panes.tilePane = tileBg;
+		var newTileBg = this._tileBg = tilePane;
+
+		if (!newTileBg.transition) {
+			// TODO move to Map options
+			newTileBg.transition = new L.Transition(newTileBg, {
+				duration: 0.25,
+				easing: 'cubic-bezier(0.25,0.1,0.25,0.75)'
+			});
+			newTileBg.transition.on('end', this._onZoomTransitionEnd, this);
+		}
+
+		this._stopLoadingImages(newTileBg);
+	},
+
+	_getLoadedTilesPercentage: function (container) {
+		var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
+			i, len, count = 0;
+
+		for (i = 0, len = tiles.length; i < len; i++) {
+			if (tiles[i].complete) {
+				count++;
+			}
+		}
+		return count / len;
+	},
+
+	// stops loading all tiles in the background layer
+	_stopLoadingImages: function (container) {
+		var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
+			i, len, tile;
+
+		for (i = 0, len = tiles.length; i < len; i++) {
+			tile = tiles[i];
+
+			if (!tile.complete) {
+				tile.onload = L.Util.falseFn;
+				tile.onerror = L.Util.falseFn;
+				tile.src = L.Util.emptyImageUrl;
+
+				tile.parentNode.removeChild(tile);
+			}
+		}
+	},
+
+	_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 = '';
+		}
+	}
+});
+
+
+/*
+ * Provides L.Map with convenient shortcuts for W3C geolocation.
+ */
+
+L.Map.include({
+	_defaultLocateOptions: {
+		watch: false,
+		setView: false,
+		maxZoom: Infinity,
+		timeout: 10000,
+		maximumAge: 0,
+		enableHighAccuracy: false
+	},
+
+	locate: function (/*Object*/ options) {
+
+		options = this._locationOptions = L.Util.extend(this._defaultLocateOptions, 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;
+	},
+
+	stopLocate: function () {
+		if (navigator.geolocation) {
+			navigator.geolocation.clearWatch(this._locationWatchId);
+		}
+		return this;
+	},
+
+	_handleGeolocationError: function (error) {
+		var c = error.code,
+			message =
+				(c === 1 ? "permission denied" :
+				(c === 2 ? "position unavailable" : "timeout"));
+
+		if (this._locationOptions.setView && !this._loaded) {
+			this.fitWorld();
+		}
+
+		this.fire('locationerror', {
+			code: c,
+			message: "Geolocation error: " + message + "."
+		});
+	},
+
+	_handleGeolocationResponse: function (pos) {
+		var latAccuracy = 180 * pos.coords.accuracy / 4e7,
+			lngAccuracy = latAccuracy * 2,
+
+			lat = pos.coords.latitude,
+			lng = pos.coords.longitude,
+			latlng = new L.LatLng(lat, lng),
+
+			sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy),
+			ne = new L.LatLng(lat + latAccuracy, lng + lngAccuracy),
+			bounds = new L.LatLngBounds(sw, ne),
+
+			options = this._locationOptions;
+
+		if (options.setView) {
+			var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
+			this.setView(latlng, zoom);
+		}
+
+		this.fire('locationfound', {
+			latlng: latlng,
+			bounds: bounds,
+			accuracy: pos.coords.accuracy
+		});
+	}
+});
+
+
+
+
+}());
\ No newline at end of file
diff --git a/dist/leaflet.css b/dist/leaflet.css
index 119ddb2..de5fd76 100644
--- a/dist/leaflet.css
+++ b/dist/leaflet.css
@@ -17,7 +17,11 @@
 .leaflet-container {
 	overflow: hidden;
 	}
-.leaflet-tile-pane, .leaflet-container {
+.leaflet-tile-pane,
+.leaflet-container,
+.leaflet-corner,
+.leaflet-popup {
+	/* TODO make this configurable */
 	-webkit-transform: translate3d(0,0,0);
 	}
 .leaflet-tile,
@@ -34,9 +38,22 @@
 .leaflet-clickable {
 	cursor: pointer;
 	}
+.leaflet-dragging {
+	cursor: move;
+	}
+.leaflet-dragging .leaflet-clickable {
+	cursor: move;
+	}
 .leaflet-container img {
 	max-width: none !important;
 	}
+.leaflet-div-icon {
+	background: #fff;
+	border: 1px solid #666;
+}
+.leaflet-editing-icon {
+	border-radius: 2px;
+}
 
 .leaflet-tile-pane { z-index: 2; }
 
@@ -52,7 +69,8 @@
 	}
 
 .leaflet-tile {
-	visibility: hidden;
+    filter: inherit;
+    visibility: hidden;
 	}
 .leaflet-tile-loaded {
 	visibility: inherit;
@@ -105,7 +123,7 @@ a.leaflet-active {
 	margin-right: 10px;
 	}
 
-.leaflet-control-zoom, .leaflet-control-layers {
+.leaflet-control-zoom {
 	-moz-border-radius: 7px;
 	-webkit-border-radius: 7px;
 	border-radius: 7px;
@@ -132,7 +150,7 @@ a.leaflet-active {
 .leaflet-control-zoom a:hover {
 	background-color: #fff;
 	}
-.leaflet-big-buttons .leaflet-control-zoom a {
+.leaflet-touch .leaflet-control-zoom a {
 	width: 27px;
 	height: 27px;
 	}
@@ -145,18 +163,18 @@ a.leaflet-active {
 	}
 
 .leaflet-control-layers {
-	-moz-box-shadow: 0 0 7px #999;
-	-webkit-box-shadow: 0 0 7px #999;
-	box-shadow: 0 0 7px #999;
-
+	box-shadow: 0 1px 7px #999;
 	background: #f8f8f9;
+	-moz-border-radius: 8px;
+	-webkit-border-radius: 8px;
+	border-radius: 8px;
 	}
 .leaflet-control-layers a {
 	background-image: url(images/layers.png);
 	width: 36px;
 	height: 36px;
 	}
-.leaflet-big-buttons .leaflet-control-layers a {
+.leaflet-touch .leaflet-control-layers a {
 	width: 44px;
 	height: 44px;
 	}
@@ -189,23 +207,57 @@ a.leaflet-active {
 	}
 
 .leaflet-container .leaflet-control-attribution {
+	background-color: rgba(255, 255, 255, 0.7);
+	box-shadow: 0 0 5px #bbb;
 	margin: 0;
+}
+
+.leaflet-control-attribution,
+.leaflet-control-scale-line {
 	padding: 0 5px;
+	color: #333;
+	}
 
+.leaflet-container .leaflet-control-attribution,
+.leaflet-container .leaflet-control-scale {
 	font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
-	color: #333;
+	}
 
-	background-color: rgba(255, 255, 255, 0.7);
+.leaflet-left .leaflet-control-scale {
+	margin-left: 5px;
+	}
+.leaflet-bottom .leaflet-control-scale {
+	margin-bottom: 5px;
+	}
 
-	-moz-box-shadow: 0 0 7px #ccc;
-	-webkit-box-shadow: 0 0 7px #ccc;
-	box-shadow: 0 0 7px #ccc;
+.leaflet-control-scale-line {
+	border: 2px solid #777;
+	border-top: none;
+	color: black;
+	line-height: 1;
+	font-size: 10px;
+	padding-bottom: 2px;
+	text-shadow: 1px 1px 1px #fff;
+	background-color: rgba(255, 255, 255, 0.5);
+	}
+.leaflet-control-scale-line:nth-child(2) {
+	border-top: 2px solid #777;
+	padding-top: 1px;
+	border-bottom: none;
+	margin-top: -2px;
+	}
+
+.leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers {
+	box-shadow: none;
+	}
+.leaflet-touch .leaflet-control-layers {
+	border: 5px solid #bbb;
 	}
 
 
 /* Fade animations */
 
-.leaflet-fade-anim .leaflet-tile {
+.leaflet-fade-anim .leaflet-tile, .leaflet-fade-anim .leaflet-popup {
 	opacity: 0;
 
 	-webkit-transition: opacity 0.2s linear;
@@ -213,47 +265,48 @@ a.leaflet-active {
 	-o-transition: opacity 0.2s linear;
 	transition: opacity 0.2s linear;
 	}
-.leaflet-fade-anim .leaflet-tile-loaded {
+.leaflet-fade-anim .leaflet-tile-loaded, .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
 	opacity: 1;
 	}
 
-.leaflet-fade-anim .leaflet-popup {
-	opacity: 0;
-
-	-webkit-transition: opacity 0.2s linear;
-	-moz-transition: opacity 0.2s linear;
-	-o-transition: opacity 0.2s linear;
-	transition: opacity 0.2s linear;
+.leaflet-zoom-anim .leaflet-tile, .leaflet-pan-anim .leaflet-tile {
+	-webkit-transition: none;
+	-moz-transition: none;
+	-o-transition: none;
+	transition: none;
 	}
-.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
-	opacity: 1;
+
+
+.leaflet-zoom-anim .leaflet-zoom-animated {
+	-webkit-transition: -webkit-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
+	-moz-transition: -moz-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
+	-o-transition: -o-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
+	transition: transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
 	}
 
-.leaflet-zoom-anim .leaflet-tile {
+.leaflet-touching .leaflet-zoom-animated {
 	-webkit-transition: none;
 	-moz-transition: none;
 	-o-transition: none;
 	transition: none;
 	}
 
-.leaflet-zoom-anim .leaflet-objects-pane {
+.leaflet-zoom-anim .leaflet-zoom-hide {
 	visibility: hidden;
 	}
 
-
 /* Popup layout */
 
 .leaflet-popup {
 	position: absolute;
 	text-align: center;
-	-webkit-transform: translate3d(0,0,0);
 	}
 .leaflet-popup-content-wrapper {
 	padding: 1px;
 	text-align: left;
 	}
 .leaflet-popup-content {
-	margin: 19px;
+	margin: 14px 20px;
 	}
 .leaflet-popup-tip-container {
 	margin: 0 auto;
@@ -277,8 +330,8 @@ a.leaflet-active {
 	}
 .leaflet-popup-close-button {
 	position: absolute;
-	top: 9px;
-	right: 9px;
+	top: 8px;
+	right: 8px;
 
 	width: 10px;
 	height: 10px;
@@ -288,6 +341,11 @@ a.leaflet-active {
 .leaflet-popup-content p {
 	margin: 18px 0;
 	}
+.leaflet-popup-scrolled {
+	overflow: auto;
+	border-bottom: 1px solid #ddd;
+	border-top: 1px solid #ddd;
+	}
 
 
 /* Visual appearance */
@@ -306,9 +364,9 @@ 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;
+	box-shadow: 0 3px 10px #888;
+	-moz-box-shadow: 0 3px 10px #888;
+	 -webkit-box-shadow: 0 3px 14px #999;
 	}
 .leaflet-popup-content-wrapper {
 	-moz-border-radius: 20px;
diff --git a/dist/leaflet.ie.css b/dist/leaflet.ie.css
index a120c0c..9d2a52f 100644
--- a/dist/leaflet.ie.css
+++ b/dist/leaflet.ie.css
@@ -1,7 +1,3 @@
-.leaflet-tile {
-	filter: inherit;
-	}
-
 .leaflet-vml-shape {
 	width: 1px;
 	height: 1px;
diff --git a/dist/leaflet.js b/dist/leaflet.js
new file mode 100644
index 0000000..0f23cb3
--- /dev/null
+++ b/dist/leaflet.js
@@ -0,0 +1,6 @@
+/*
+ Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin
+ Leaflet is a modern open-source JavaScript library for interactive maps.
+ http://leaflet.cloudmade.com
+*/
+(function(){var e,t;typeof exports!="undefined"?e=exports:(e={},t=window.L,e.noConflict=function(){return window.L=t,e},window.L=e),e.version="0.4",e.Util={extend:function(e){var t=Array.prototype.slice.call(arguments,1);for(var n=0,r=t.length,i;n<r;n++){i=t[n]||{};for(var s in i)i.hasOwnProperty(s)&&(e[s]=i[s])}return e},bind:function(e,t){var n=arguments.length>2?Array.prototype.slice.call(arguments,2):null;return function(){return e.apply(t,n||arguments)}},stamp:function(){var e=0,t=" [...]
\ No newline at end of file
diff --git a/spec/jasmine/jasmine-html.js b/spec/jasmine/jasmine-html.js
new file mode 100644
index 0000000..b405821
--- /dev/null
+++ b/spec/jasmine/jasmine-html.js
@@ -0,0 +1,182 @@
+jasmine.TrivialReporter = function(doc) {
+  this.document = doc || document;
+  this.suiteDivs = {};
+  this.logRunningSpecs = false;
+};
+
+jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) { el.appendChild(child); }
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
+  var showPassed, showSkipped;
+
+  this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
+      this.createDom('div', { className: 'banner' },
+        this.createDom('div', { className: 'logo' },
+            "Jasmine",
+            this.createDom('span', { className: 'version' }, runner.env.versionString())),
+        this.createDom('div', { className: 'options' },
+            "Show ",
+            showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
+            showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
+            )
+          ),
+
+      this.runnerDiv = this.createDom('div', { className: 'runner running' },
+          this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
+          this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
+          this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
+      );
+
+  this.document.body.appendChild(this.outerDiv);
+
+  var suites = runner.suites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    var suiteDiv = this.createDom('div', { className: 'suite' },
+        this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
+        this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
+    this.suiteDivs[suite.id] = suiteDiv;
+    var parentDiv = this.outerDiv;
+    if (suite.parentSuite) {
+      parentDiv = this.suiteDivs[suite.parentSuite.id];
+    }
+    parentDiv.appendChild(suiteDiv);
+  }
+
+  this.startedAt = new Date();
+
+  var self = this;
+  showPassed.onchange = function(evt) {
+    if (evt.target.checked) {
+      self.outerDiv.className += ' show-passed';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
+    }
+  };
+
+  showSkipped.onchange = function(evt) {
+    if (evt.target.checked) {
+      self.outerDiv.className += ' show-skipped';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
+    }
+  };
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
+  var results = runner.results();
+  var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
+  this.runnerDiv.setAttribute("class", className);
+  //do it twice for IE
+  this.runnerDiv.setAttribute("className", className);
+  var specs = runner.specs();
+  var specCount = 0;
+  for (var i = 0; i < specs.length; i++) {
+    if (this.specFilter(specs[i])) {
+      specCount++;
+    }
+  }
+  var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
+  message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
+  this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
+
+  this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
+};
+
+jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
+  var results = suite.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.totalCount == 0) { // todo: change this to check results.skipped
+    status = 'skipped';
+  }
+  this.suiteDivs[suite.id].className += " " + status;
+};
+
+jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
+  if (this.logRunningSpecs) {
+    this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+  }
+};
+
+jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
+  var results = spec.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+  var specDiv = this.createDom('div', { className: 'spec '  + status },
+      this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(spec.getFullName()),
+        title: spec.getFullName()
+      }, spec.description));
+
+
+  var resultItems = results.getItems();
+  var messagesDiv = this.createDom('div', { className: 'messages' });
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      if (result.trace.stack) {
+        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    specDiv.appendChild(messagesDiv);
+  }
+
+  this.suiteDivs[spec.suite.id].appendChild(specDiv);
+};
+
+jasmine.TrivialReporter.prototype.log = function() {
+  var console = jasmine.getGlobal().console;
+  if (console && console.log) console.log.apply(console, arguments);
+};
+
+jasmine.TrivialReporter.prototype.getLocation = function() {
+  return this.document.location;
+};
+
+jasmine.TrivialReporter.prototype.specFilter = function(spec) {
+  var paramMap = {};
+  var params = this.getLocation().search.substring(1).split('&');
+  for (var i = 0; i < params.length; i++) {
+    var p = params[i].split('=');
+    paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+  }
+
+  if (!paramMap["spec"]) return true;
+  return spec.getFullName().indexOf(paramMap["spec"]) == 0;
+};
diff --git a/spec/jasmine/jasmine.css b/spec/jasmine/jasmine.css
new file mode 100644
index 0000000..6583fe7
--- /dev/null
+++ b/spec/jasmine/jasmine.css
@@ -0,0 +1,166 @@
+body {
+  font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+}
+
+
+.jasmine_reporter a:visited, .jasmine_reporter a {
+  color: #303; 
+}
+
+.jasmine_reporter a:hover, .jasmine_reporter a:active {
+  color: blue; 
+}
+
+.run_spec {
+  float:right;
+  padding-right: 5px;
+  font-size: .8em;
+  text-decoration: none;
+}
+
+.jasmine_reporter {
+  margin: 0 5px;
+}
+
+.banner {
+  color: #303;
+  background-color: #fef;
+  padding: 5px;
+}
+
+.logo {
+  float: left;
+  font-size: 1.1em;
+  padding-left: 5px;
+}
+
+.logo .version {
+  font-size: .6em;
+  padding-left: 1em;
+}
+
+.runner.running {
+  background-color: yellow;
+}
+
+
+.options {
+  text-align: right;
+  font-size: .8em;
+}
+
+
+
+
+.suite {
+  border: 1px outset gray;
+  margin: 5px 0;
+  padding-left: 1em;
+}
+
+.suite .suite {
+  margin: 5px; 
+}
+
+.suite.passed {
+  background-color: #dfd;
+}
+
+.suite.failed {
+  background-color: #fdd;
+}
+
+.spec {
+  margin: 5px;
+  padding-left: 1em;
+  clear: both;
+}
+
+.spec.failed, .spec.passed, .spec.skipped {
+  padding-bottom: 5px;
+  border: 1px solid gray;
+}
+
+.spec.failed {
+  background-color: #fbb;
+  border-color: red;
+}
+
+.spec.passed {
+  background-color: #bfb;
+  border-color: green;
+}
+
+.spec.skipped {
+  background-color: #bbb;
+}
+
+.messages {
+  border-left: 1px dashed gray;
+  padding-left: 1em;
+  padding-right: 1em;
+}
+
+.passed {
+  background-color: #cfc;
+  display: none;
+}
+
+.failed {
+  background-color: #fbb;
+}
+
+.skipped {
+  color: #777;
+  background-color: #eee;
+  display: none;
+}
+
+
+/*.resultMessage {*/
+  /*white-space: pre;*/
+/*}*/
+
+.resultMessage span.result {
+  display: block;
+  line-height: 2em;
+  color: black;
+}
+
+.resultMessage .mismatch {
+  color: black;
+}
+
+.stackTrace {
+  white-space: pre;
+  font-size: .8em;
+  margin-left: 10px;
+  max-height: 5em;
+  overflow: auto;
+  border: 1px inset red;
+  padding: 1em;
+  background: #eef;
+}
+
+.finished-at {
+  padding-left: 1em;
+  font-size: .6em;
+}
+
+.show-passed .passed,
+.show-skipped .skipped {
+  display: block;
+}
+
+
+#jasmine_content {
+  position:fixed;
+  right: 100%;
+}
+
+.runner {
+  border: 1px solid gray;
+  display: block;
+  margin: 5px 0;
+  padding: 2px 0 2px 10px;
+}
diff --git a/spec/jasmine/jasmine.js b/spec/jasmine/jasmine.js
new file mode 100644
index 0000000..3ace3bc
--- /dev/null
+++ b/spec/jasmine/jasmine.js
@@ -0,0 +1,2421 @@
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+  throw new Error("unimplemented method");
+};
+
+/**
+ * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just
+ * a plain old variable and may be redefined by somebody else.
+ *
+ * @private
+ */
+jasmine.undefined = jasmine.___undefined___;
+
+/**
+ * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.
+ *
+ */
+jasmine.DEFAULT_UPDATE_INTERVAL = 250;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+jasmine.getGlobal = function() {
+  function getGlobal() {
+    return this;
+  }
+
+  return getGlobal();
+};
+
+/**
+ * Allows for bound functions to be compared.  Internal use only.
+ *
+ * @ignore
+ * @private
+ * @param base {Object} bound 'this' for the function
+ * @param name {Function} function to find
+ */
+jasmine.bindOriginal_ = function(base, name) {
+  var original = base[name];
+  if (original.apply) {
+    return function() {
+      return original.apply(base, arguments);
+    };
+  } else {
+    // IE support
+    return jasmine.getGlobal()[name];
+  }
+};
+
+jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');
+jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');
+jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');
+jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');
+
+jasmine.MessageResult = function(values) {
+  this.type = 'log';
+  this.values = values;
+  this.trace = new Error(); // todo: test better
+};
+
+jasmine.MessageResult.prototype.toString = function() {
+  var text = "";
+  for(var i = 0; i < this.values.length; i++) {
+    if (i > 0) text += " ";
+    if (jasmine.isString_(this.values[i])) {
+      text += this.values[i];
+    } else {
+      text += jasmine.pp(this.values[i]);
+    }
+  }
+  return text;
+};
+
+jasmine.ExpectationResult = function(params) {
+  this.type = 'expect';
+  this.matcherName = params.matcherName;
+  this.passed_ = params.passed;
+  this.expected = params.expected;
+  this.actual = params.actual;
+
+  this.message = this.passed_ ? 'Passed.' : params.message;
+  this.trace = this.passed_ ? '' : new Error(this.message);
+};
+
+jasmine.ExpectationResult.prototype.toString = function () {
+  return this.message;
+};
+
+jasmine.ExpectationResult.prototype.passed = function () {
+  return this.passed_;
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function() {
+  return jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isArray_ = function(value) {
+  return jasmine.isA_("Array", value);  
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isString_ = function(value) {
+  return jasmine.isA_("String", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isNumber_ = function(value) {
+  return jasmine.isA_("Number", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param {String} typeName
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isA_ = function(typeName, value) {
+  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+};
+
+/**
+ * Pretty printer for expecations.  Takes any object and turns it into a human-readable string.
+ *
+ * @param value {Object} an object to be outputted
+ * @returns {String}
+ */
+jasmine.pp = function(value) {
+  var stringPrettyPrinter = new jasmine.StringPrettyPrinter();
+  stringPrettyPrinter.format(value);
+  return stringPrettyPrinter.string;
+};
+
+/**
+ * Returns true if the object is a DOM Node.
+ *
+ * @param {Object} obj object to check
+ * @returns {Boolean}
+ */
+jasmine.isDomNode = function(obj) {
+  return obj['nodeType'] > 0;
+};
+
+/**
+ * Returns a matchable 'generic' object of the class type.  For use in expecations of type when values don't matter.
+ *
+ * @example
+ * // don't care about which function is passed in, as long as it's a function
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));
+ *
+ * @param {Class} clazz
+ * @returns matchable object of the type clazz
+ */
+jasmine.any = function(clazz) {
+  return new jasmine.Matchers.Any(clazz);
+};
+
+/**
+ * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.
+ *
+ * Spies should be created in test setup, before expectations.  They can then be checked, using the standard Jasmine
+ * expectation syntax. Spies can be checked if they were called or not and what the calling params were.
+ *
+ * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).
+ *
+ * Spies are torn down at the end of every spec.
+ *
+ * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.
+ *
+ * @example
+ * // a stub
+ * var myStub = jasmine.createSpy('myStub');  // can be used anywhere
+ *
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // actual foo.not will not be called, execution stops
+ * spyOn(foo, 'not');
+
+ // foo.not spied upon, execution will continue to implementation
+ * spyOn(foo, 'not').andCallThrough();
+ *
+ * // fake example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // foo.not(val) will return val
+ * spyOn(foo, 'not').andCallFake(function(value) {return value;});
+ *
+ * // mock example
+ * foo.not(7 == 7);
+ * expect(foo.not).toHaveBeenCalled();
+ * expect(foo.not).toHaveBeenCalledWith(true);
+ *
+ * @constructor
+ * @see spyOn, jasmine.createSpy, jasmine.createSpyObj
+ * @param {String} name
+ */
+jasmine.Spy = function(name) {
+  /**
+   * The name of the spy, if provided.
+   */
+  this.identity = name || 'unknown';
+  /**
+   *  Is this Object a spy?
+   */
+  this.isSpy = true;
+  /**
+   * The actual function this spy stubs.
+   */
+  this.plan = function() {
+  };
+  /**
+   * Tracking of the most recent call to the spy.
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy.mostRecentCall.args = [1, 2];
+   */
+  this.mostRecentCall = {};
+
+  /**
+   * Holds arguments for each call to the spy, indexed by call count
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy(7, 8);
+   * mySpy.mostRecentCall.args = [7, 8];
+   * mySpy.argsForCall[0] = [1, 2];
+   * mySpy.argsForCall[1] = [7, 8];
+   */
+  this.argsForCall = [];
+  this.calls = [];
+};
+
+/**
+ * Tells a spy to call through to the actual implemenatation.
+ *
+ * @example
+ * var foo = {
+ *   bar: function() { // do some stuff }
+ * }
+ *
+ * // defining a spy on an existing property: foo.bar
+ * spyOn(foo, 'bar').andCallThrough();
+ */
+jasmine.Spy.prototype.andCallThrough = function() {
+  this.plan = this.originalValue;
+  return this;
+};
+
+/**
+ * For setting the return value of a spy.
+ *
+ * @example
+ * // defining a spy from scratch: foo() returns 'baz'
+ * var foo = jasmine.createSpy('spy on foo').andReturn('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() returns 'baz'
+ * spyOn(foo, 'bar').andReturn('baz');
+ *
+ * @param {Object} value
+ */
+jasmine.Spy.prototype.andReturn = function(value) {
+  this.plan = function() {
+    return value;
+  };
+  return this;
+};
+
+/**
+ * For throwing an exception when a spy is called.
+ *
+ * @example
+ * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'
+ * var foo = jasmine.createSpy('spy on foo').andThrow('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'
+ * spyOn(foo, 'bar').andThrow('baz');
+ *
+ * @param {String} exceptionMsg
+ */
+jasmine.Spy.prototype.andThrow = function(exceptionMsg) {
+  this.plan = function() {
+    throw exceptionMsg;
+  };
+  return this;
+};
+
+/**
+ * Calls an alternate implementation when a spy is called.
+ *
+ * @example
+ * var baz = function() {
+ *   // do some stuff, return something
+ * }
+ * // defining a spy from scratch: foo() calls the function baz
+ * var foo = jasmine.createSpy('spy on foo').andCall(baz);
+ *
+ * // defining a spy on an existing property: foo.bar() calls an anonymnous function
+ * spyOn(foo, 'bar').andCall(function() { return 'baz';} );
+ *
+ * @param {Function} fakeFunc
+ */
+jasmine.Spy.prototype.andCallFake = function(fakeFunc) {
+  this.plan = fakeFunc;
+  return this;
+};
+
+/**
+ * Resets all of a spy's the tracking variables so that it can be used again.
+ *
+ * @example
+ * spyOn(foo, 'bar');
+ *
+ * foo.bar();
+ *
+ * expect(foo.bar.callCount).toEqual(1);
+ *
+ * foo.bar.reset();
+ *
+ * expect(foo.bar.callCount).toEqual(0);
+ */
+jasmine.Spy.prototype.reset = function() {
+  this.wasCalled = false;
+  this.callCount = 0;
+  this.argsForCall = [];
+  this.calls = [];
+  this.mostRecentCall = {};
+};
+
+jasmine.createSpy = function(name) {
+
+  var spyObj = function() {
+    spyObj.wasCalled = true;
+    spyObj.callCount++;
+    var args = jasmine.util.argsToArray(arguments);
+    spyObj.mostRecentCall.object = this;
+    spyObj.mostRecentCall.args = args;
+    spyObj.argsForCall.push(args);
+    spyObj.calls.push({object: this, args: args});
+    return spyObj.plan.apply(this, arguments);
+  };
+
+  var spy = new jasmine.Spy(name);
+
+  for (var prop in spy) {
+    spyObj[prop] = spy[prop];
+  }
+
+  spyObj.reset();
+
+  return spyObj;
+};
+
+/**
+ * Determines whether an object is a spy.
+ *
+ * @param {jasmine.Spy|Object} putativeSpy
+ * @returns {Boolean}
+ */
+jasmine.isSpy = function(putativeSpy) {
+  return putativeSpy && putativeSpy.isSpy;
+};
+
+/**
+ * Creates a more complicated spy: an Object that has every property a function that is a spy.  Used for stubbing something
+ * large in one call.
+ *
+ * @param {String} baseName name of spy class
+ * @param {Array} methodNames array of names of methods to make spies
+ */
+jasmine.createSpyObj = function(baseName, methodNames) {
+  if (!jasmine.isArray_(methodNames) || methodNames.length == 0) {
+    throw new Error('createSpyObj requires a non-empty array of method names to create spies for');
+  }
+  var obj = {};
+  for (var i = 0; i < methodNames.length; i++) {
+    obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);
+  }
+  return obj;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the current spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.log = function() {
+  var spec = jasmine.getEnv().currentSpec;
+  spec.log.apply(spec, arguments);
+};
+
+/**
+ * Function that installs a spy on an existing object's method name.  Used within a Spec to create a spy.
+ *
+ * @example
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops
+ *
+ * @see jasmine.createSpy
+ * @param obj
+ * @param methodName
+ * @returns a Jasmine spy that can be chained with all spy methods
+ */
+var spyOn = function(obj, methodName) {
+  return jasmine.getEnv().currentSpec.spyOn(obj, methodName);
+};
+
+/**
+ * Creates a Jasmine spec that will be added to the current suite.
+ *
+ * // TODO: pending tests
+ *
+ * @example
+ * it('should be true', function() {
+ *   expect(true).toEqual(true);
+ * });
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var it = function(desc, func) {
+  return jasmine.getEnv().it(desc, func);
+};
+
+/**
+ * Creates a <em>disabled</em> Jasmine spec.
+ *
+ * A convenience method that allows existing specs to be disabled temporarily during development.
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var xit = function(desc, func) {
+  return jasmine.getEnv().xit(desc, func);
+};
+
+/**
+ * Starts a chain for a Jasmine expectation.
+ *
+ * It is passed an Object that is the actual value and should chain to one of the many
+ * jasmine.Matchers functions.
+ *
+ * @param {Object} actual Actual value to test against and expected value
+ */
+var expect = function(actual) {
+  return jasmine.getEnv().currentSpec.expect(actual);
+};
+
+/**
+ * Defines part of a jasmine spec.  Used in cominbination with waits or waitsFor in asynchrnous specs.
+ *
+ * @param {Function} func Function that defines part of a jasmine spec.
+ */
+var runs = function(func) {
+  jasmine.getEnv().currentSpec.runs(func);
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+var waits = function(timeout) {
+  jasmine.getEnv().currentSpec.waits(timeout);
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);
+};
+
+/**
+ * A function that is called before each spec in a suite.
+ *
+ * Used for spec setup, including validating assumptions.
+ *
+ * @param {Function} beforeEachFunction
+ */
+var beforeEach = function(beforeEachFunction) {
+  jasmine.getEnv().beforeEach(beforeEachFunction);
+};
+
+/**
+ * A function that is called after each spec in a suite.
+ *
+ * Used for restoring any state that is hijacked during spec execution.
+ *
+ * @param {Function} afterEachFunction
+ */
+var afterEach = function(afterEachFunction) {
+  jasmine.getEnv().afterEach(afterEachFunction);
+};
+
+/**
+ * Defines a suite of specifications.
+ *
+ * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared
+ * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization
+ * of setup in some tests.
+ *
+ * @example
+ * // TODO: a simple suite
+ *
+ * // TODO: a simple suite with a nested describe block
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var describe = function(description, specDefinitions) {
+  return jasmine.getEnv().describe(description, specDefinitions);
+};
+
+/**
+ * Disables a suite of specifications.  Used to disable some suites in a file, or files, temporarily during development.
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var xdescribe = function(description, specDefinitions) {
+  return jasmine.getEnv().xdescribe(description, specDefinitions);
+};
+
+
+// Provide the XMLHttpRequest class for IE 5.x-6.x:
+jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP.6.0");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP.3.0");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Microsoft.XMLHTTP");
+  } catch(e) {
+  }
+  throw new Error("This browser does not support XMLHttpRequest.");
+} : XMLHttpRequest;
+/**
+ * @namespace
+ */
+jasmine.util = {};
+
+/**
+ * Declare that a child class inherit it's prototype from the parent class.
+ *
+ * @private
+ * @param {Function} childClass
+ * @param {Function} parentClass
+ */
+jasmine.util.inherit = function(childClass, parentClass) {
+  /**
+   * @private
+   */
+  var subclass = function() {
+  };
+  subclass.prototype = parentClass.prototype;
+  childClass.prototype = new subclass;
+};
+
+jasmine.util.formatException = function(e) {
+  var lineNumber;
+  if (e.line) {
+    lineNumber = e.line;
+  }
+  else if (e.lineNumber) {
+    lineNumber = e.lineNumber;
+  }
+
+  var file;
+
+  if (e.sourceURL) {
+    file = e.sourceURL;
+  }
+  else if (e.fileName) {
+    file = e.fileName;
+  }
+
+  var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();
+
+  if (file && lineNumber) {
+    message += ' in ' + file + ' (line ' + lineNumber + ')';
+  }
+
+  return message;
+};
+
+jasmine.util.htmlEscape = function(str) {
+  if (!str) return str;
+  return str.replace(/&/g, '&')
+    .replace(/</g, '<')
+    .replace(/>/g, '>');
+};
+
+jasmine.util.argsToArray = function(args) {
+  var arrayOfArgs = [];
+  for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+  return arrayOfArgs;
+};
+
+jasmine.util.extend = function(destination, source) {
+  for (var property in source) destination[property] = source[property];
+  return destination;
+};
+
+/**
+ * Environment for Jasmine
+ *
+ * @constructor
+ */
+jasmine.Env = function() {
+  this.currentSpec = null;
+  this.currentSuite = null;
+  this.currentRunner_ = new jasmine.Runner(this);
+
+  this.reporter = new jasmine.MultiReporter();
+
+  this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;
+  this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+  this.lastUpdate = 0;
+  this.specFilter = function() {
+    return true;
+  };
+
+  this.nextSpecId_ = 0;
+  this.nextSuiteId_ = 0;
+  this.equalityTesters_ = [];
+
+  // wrap matchers
+  this.matchersClass = function() {
+    jasmine.Matchers.apply(this, arguments);
+  };
+  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
+
+  jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
+};
+
+
+jasmine.Env.prototype.setTimeout = jasmine.setTimeout;
+jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;
+jasmine.Env.prototype.setInterval = jasmine.setInterval;
+jasmine.Env.prototype.clearInterval = jasmine.clearInterval;
+
+/**
+ * @returns an object containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.version = function () {
+  if (jasmine.version_) {
+    return jasmine.version_;
+  } else {
+    throw new Error('Version not set');
+  }
+};
+
+/**
+ * @returns string containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.versionString = function() {
+  if (jasmine.version_) {
+    var version = this.version();
+    return version.major + "." + version.minor + "." + version.build + " revision " + version.revision;
+  } else {
+    return "version unknown";
+  }
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSpecId = function () {
+  return this.nextSpecId_++;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSuiteId = function () {
+  return this.nextSuiteId_++;
+};
+
+/**
+ * Register a reporter to receive status updates from Jasmine.
+ * @param {jasmine.Reporter} reporter An object which will receive status updates.
+ */
+jasmine.Env.prototype.addReporter = function(reporter) {
+  this.reporter.addReporter(reporter);
+};
+
+jasmine.Env.prototype.execute = function() {
+  this.currentRunner_.execute();
+};
+
+jasmine.Env.prototype.describe = function(description, specDefinitions) {
+  var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);
+
+  var parentSuite = this.currentSuite;
+  if (parentSuite) {
+    parentSuite.add(suite);
+  } else {
+    this.currentRunner_.add(suite);
+  }
+
+  this.currentSuite = suite;
+
+  var declarationError = null;
+  try {
+    specDefinitions.call(suite);
+  } catch(e) {
+    declarationError = e;
+  }
+
+  this.currentSuite = parentSuite;
+
+  if (declarationError) {
+    this.it("encountered a declaration exception", function() {
+      throw declarationError;
+    });
+  }
+
+  return suite;
+};
+
+jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.beforeEach(beforeEachFunction);
+  } else {
+    this.currentRunner_.beforeEach(beforeEachFunction);
+  }
+};
+
+jasmine.Env.prototype.currentRunner = function () {
+  return this.currentRunner_;
+};
+
+jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.afterEach(afterEachFunction);
+  } else {
+    this.currentRunner_.afterEach(afterEachFunction);
+  }
+
+};
+
+jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {
+  return {
+    execute: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.it = function(description, func) {
+  var spec = new jasmine.Spec(this, this.currentSuite, description);
+  this.currentSuite.add(spec);
+  this.currentSpec = spec;
+
+  if (func) {
+    spec.runs(func);
+  }
+
+  return spec;
+};
+
+jasmine.Env.prototype.xit = function(desc, func) {
+  return {
+    id: this.nextSpecId(),
+    runs: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
+  if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
+    return true;
+  }
+
+  a.__Jasmine_been_here_before__ = b;
+  b.__Jasmine_been_here_before__ = a;
+
+  var hasKey = function(obj, keyName) {
+    return obj != null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in b) {
+    if (!hasKey(a, property) && hasKey(b, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+  }
+  for (property in a) {
+    if (!hasKey(b, property) && hasKey(a, property)) {
+      mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
+    }
+  }
+  for (property in b) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
+      mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual.");
+    }
+  }
+
+  if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {
+    mismatchValues.push("arrays were not the same length");
+  }
+
+  delete a.__Jasmine_been_here_before__;
+  delete b.__Jasmine_been_here_before__;
+  return (mismatchKeys.length == 0 && mismatchValues.length == 0);
+};
+
+jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  for (var i = 0; i < this.equalityTesters_.length; i++) {
+    var equalityTester = this.equalityTesters_[i];
+    var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
+    if (result !== jasmine.undefined) return result;
+  }
+
+  if (a === b) return true;
+
+  if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {
+    return (a == jasmine.undefined && b == jasmine.undefined);
+  }
+
+  if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+    return a === b;
+  }
+
+  if (a instanceof Date && b instanceof Date) {
+    return a.getTime() == b.getTime();
+  }
+
+  if (a instanceof jasmine.Matchers.Any) {
+    return a.matches(b);
+  }
+
+  if (b instanceof jasmine.Matchers.Any) {
+    return b.matches(a);
+  }
+
+  if (jasmine.isString_(a) && jasmine.isString_(b)) {
+    return (a == b);
+  }
+
+  if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {
+    return (a == b);
+  }
+
+  if (typeof a === "object" && typeof b === "object") {
+    return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
+  }
+
+  //Straight check
+  return (a === b);
+};
+
+jasmine.Env.prototype.contains_ = function(haystack, needle) {
+  if (jasmine.isArray_(haystack)) {
+    for (var i = 0; i < haystack.length; i++) {
+      if (this.equals_(haystack[i], needle)) return true;
+    }
+    return false;
+  }
+  return haystack.indexOf(needle) >= 0;
+};
+
+jasmine.Env.prototype.addEqualityTester = function(equalityTester) {
+  this.equalityTesters_.push(equalityTester);
+};
+/** No-op base class for Jasmine reporters.
+ *
+ * @constructor
+ */
+jasmine.Reporter = function() {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerStarting = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerResults = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecStarting = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecResults = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.log = function(str) {
+};
+
+/**
+ * Blocks are functions with executable code that make up a spec.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {Function} func
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Block = function(env, func, spec) {
+  this.env = env;
+  this.func = func;
+  this.spec = spec;
+};
+
+jasmine.Block.prototype.execute = function(onComplete) {  
+  try {
+    this.func.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+  }
+  onComplete();
+};
+/** JavaScript API reporter.
+ *
+ * @constructor
+ */
+jasmine.JsApiReporter = function() {
+  this.started = false;
+  this.finished = false;
+  this.suites_ = [];
+  this.results_ = {};
+};
+
+jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {
+  this.started = true;
+  var suites = runner.topLevelSuites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    this.suites_.push(this.summarize_(suite));
+  }
+};
+
+jasmine.JsApiReporter.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {
+  var isSuite = suiteOrSpec instanceof jasmine.Suite;
+  var summary = {
+    id: suiteOrSpec.id,
+    name: suiteOrSpec.description,
+    type: isSuite ? 'suite' : 'spec',
+    children: []
+  };
+  
+  if (isSuite) {
+    var children = suiteOrSpec.children();
+    for (var i = 0; i < children.length; i++) {
+      summary.children.push(this.summarize_(children[i]));
+    }
+  }
+  return summary;
+};
+
+jasmine.JsApiReporter.prototype.results = function() {
+  return this.results_;
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {
+  return this.results_[specId];
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {
+  this.finished = true;
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {
+  this.results_[spec.id] = {
+    messages: spec.results().getItems(),
+    result: spec.results().failedCount > 0 ? "failed" : "passed"
+  };
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.log = function(str) {
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){
+  var results = {};
+  for (var i = 0; i < specIds.length; i++) {
+    var specId = specIds[i];
+    results[specId] = this.summarizeResult_(this.results_[specId]);
+  }
+  return results;
+};
+
+jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){
+  var summaryMessages = [];
+  var messagesLength = result.messages.length;
+  for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {
+    var resultMessage = result.messages[messageIndex];
+    summaryMessages.push({
+      text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,
+      passed: resultMessage.passed ? resultMessage.passed() : true,
+      type: resultMessage.type,
+      message: resultMessage.message,
+      trace: {
+        stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined
+      }
+    });
+  }
+
+  return {
+    result : result.result,
+    messages : summaryMessages
+  };
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+  this.env = env;
+  this.actual = actual;
+  this.spec = spec;
+  this.isNot = opt_isNot || false;
+  this.reportWasCalled_ = false;
+};
+
+// todo: @deprecated as of Jasmine 0.11, remove soon [xw]
+jasmine.Matchers.pp = function(str) {
+  throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!");
+};
+
+// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]
+jasmine.Matchers.prototype.report = function(result, failing_message, details) {
+  throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs");
+};
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+  for (var methodName in prototype) {
+    if (methodName == 'report') continue;
+    var orig = prototype[methodName];
+    matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);
+  }
+};
+
+jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+  return function() {
+    var matcherArgs = jasmine.util.argsToArray(arguments);
+    var result = matcherFunction.apply(this, arguments);
+
+    if (this.isNot) {
+      result = !result;
+    }
+
+    if (this.reportWasCalled_) return result;
+
+    var message;
+    if (!result) {
+      if (this.message) {
+        message = this.message.apply(this, arguments);
+        if (jasmine.isArray_(message)) {
+          message = message[this.isNot ? 1 : 0];
+        }
+      } else {
+        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+        message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
+        if (matcherArgs.length > 0) {
+          for (var i = 0; i < matcherArgs.length; i++) {
+            if (i > 0) message += ",";
+            message += " " + jasmine.pp(matcherArgs[i]);
+          }
+        }
+        message += ".";
+      }
+    }
+    var expectationResult = new jasmine.ExpectationResult({
+      matcherName: matcherName,
+      passed: result,
+      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+      actual: this.actual,
+      message: message
+    });
+    this.spec.addMatcherResult(expectationResult);
+    return jasmine.undefined;
+  };
+};
+
+
+
+
+/**
+ * toBe: compares the actual to the expected using ===
+ * @param expected
+ */
+jasmine.Matchers.prototype.toBe = function(expected) {
+  return this.actual === expected;
+};
+
+/**
+ * toNotBe: compares the actual to the expected using !==
+ * @param expected
+ * @deprecated as of 1.0. Use not.toBe() instead.
+ */
+jasmine.Matchers.prototype.toNotBe = function(expected) {
+  return this.actual !== expected;
+};
+
+/**
+ * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toEqual = function(expected) {
+  return this.env.equals_(this.actual, expected);
+};
+
+/**
+ * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual
+ * @param expected
+ * @deprecated as of 1.0. Use not.toNotEqual() instead.
+ */
+jasmine.Matchers.prototype.toNotEqual = function(expected) {
+  return !this.env.equals_(this.actual, expected);
+};
+
+/**
+ * Matcher that compares the actual to the expected using a regular expression.  Constructs a RegExp, so takes
+ * a pattern or a String.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toMatch = function(expected) {
+  return new RegExp(expected).test(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch
+ * @param expected
+ * @deprecated as of 1.0. Use not.toMatch() instead.
+ */
+jasmine.Matchers.prototype.toNotMatch = function(expected) {
+  return !(new RegExp(expected).test(this.actual));
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeDefined = function() {
+  return (this.actual !== jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+  return (this.actual === jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+  return (this.actual === null);
+};
+
+/**
+ * Matcher that boolean not-nots the actual.
+ */
+jasmine.Matchers.prototype.toBeTruthy = function() {
+  return !!this.actual;
+};
+
+
+/**
+ * Matcher that boolean nots the actual.
+ */
+jasmine.Matchers.prototype.toBeFalsy = function() {
+  return !this.actual;
+};
+
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called.
+ */
+jasmine.Matchers.prototype.toHaveBeenCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to have been called.",
+      "Expected spy " + this.actual.identity + " not to have been called."
+    ];
+  };
+
+  return this.actual.wasCalled;
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */
+jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was not called.
+ *
+ * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead
+ */
+jasmine.Matchers.prototype.wasNotCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('wasNotCalled does not take arguments');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to not have been called.",
+      "Expected spy " + this.actual.identity + " to have been called."
+    ];
+  };
+
+  return !this.actual.wasCalled;
+};
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.
+ *
+ * @example
+ *
+ */
+jasmine.Matchers.prototype.toHaveBeenCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+  this.message = function() {
+    if (this.actual.callCount == 0) {
+      // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw]
+      return [
+        "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.",
+        "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was."
+      ];
+    } else {
+      return [
+        "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall),
+        "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall)
+      ];
+    }
+  };
+
+  return this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;
+
+/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasNotCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was",
+      "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was"
+    ]
+  };
+
+  return !this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/**
+ * Matcher that checks that the expected item is an element in the actual Array.
+ *
+ * @param {Object} expected
+ */
+jasmine.Matchers.prototype.toContain = function(expected) {
+  return this.env.contains_(this.actual, expected);
+};
+
+/**
+ * Matcher that checks that the expected item is NOT an element in the actual Array.
+ *
+ * @param {Object} expected
+ * @deprecated as of 1.0. Use not.toNotContain() instead.
+ */
+jasmine.Matchers.prototype.toNotContain = function(expected) {
+  return !this.env.contains_(this.actual, expected);
+};
+
+jasmine.Matchers.prototype.toBeLessThan = function(expected) {
+  return this.actual < expected;
+};
+
+jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
+  return this.actual > expected;
+};
+
+/**
+ * Matcher that checks that the expected exception was thrown by the actual.
+ *
+ * @param {String} expected
+ */
+jasmine.Matchers.prototype.toThrow = function(expected) {
+  var result = false;
+  var exception;
+  if (typeof this.actual != 'function') {
+    throw new Error('Actual is not a function');
+  }
+  try {
+    this.actual();
+  } catch (e) {
+    exception = e;
+  }
+  if (exception) {
+    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
+  }
+
+  var not = this.isNot ? "not " : "";
+
+  this.message = function() {
+    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
+      return ["Expected function " + not + "to throw", expected ? expected.message || expected : " an exception", ", but it threw", exception.message || exception].join(' ');
+    } else {
+      return "Expected function to throw an exception.";
+    }
+  };
+
+  return result;
+};
+
+jasmine.Matchers.Any = function(expectedClass) {
+  this.expectedClass = expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.matches = function(other) {
+  if (this.expectedClass == String) {
+    return typeof other == 'string' || other instanceof String;
+  }
+
+  if (this.expectedClass == Number) {
+    return typeof other == 'number' || other instanceof Number;
+  }
+
+  if (this.expectedClass == Function) {
+    return typeof other == 'function' || other instanceof Function;
+  }
+
+  if (this.expectedClass == Object) {
+    return typeof other == 'object';
+  }
+
+  return other instanceof this.expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.toString = function() {
+  return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+/**
+ * @constructor
+ */
+jasmine.MultiReporter = function() {
+  this.subReporters_ = [];
+};
+jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);
+
+jasmine.MultiReporter.prototype.addReporter = function(reporter) {
+  this.subReporters_.push(reporter);
+};
+
+(function() {
+  var functionNames = [
+    "reportRunnerStarting",
+    "reportRunnerResults",
+    "reportSuiteResults",
+    "reportSpecStarting",
+    "reportSpecResults",
+    "log"
+  ];
+  for (var i = 0; i < functionNames.length; i++) {
+    var functionName = functionNames[i];
+    jasmine.MultiReporter.prototype[functionName] = (function(functionName) {
+      return function() {
+        for (var j = 0; j < this.subReporters_.length; j++) {
+          var subReporter = this.subReporters_[j];
+          if (subReporter[functionName]) {
+            subReporter[functionName].apply(subReporter, arguments);
+          }
+        }
+      };
+    })(functionName);
+  }
+})();
+/**
+ * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults
+ *
+ * @constructor
+ */
+jasmine.NestedResults = function() {
+  /**
+   * The total count of results
+   */
+  this.totalCount = 0;
+  /**
+   * Number of passed results
+   */
+  this.passedCount = 0;
+  /**
+   * Number of failed results
+   */
+  this.failedCount = 0;
+  /**
+   * Was this suite/spec skipped?
+   */
+  this.skipped = false;
+  /**
+   * @ignore
+   */
+  this.items_ = [];
+};
+
+/**
+ * Roll up the result counts.
+ *
+ * @param result
+ */
+jasmine.NestedResults.prototype.rollupCounts = function(result) {
+  this.totalCount += result.totalCount;
+  this.passedCount += result.passedCount;
+  this.failedCount += result.failedCount;
+};
+
+/**
+ * Adds a log message.
+ * @param values Array of message parts which will be concatenated later.
+ */
+jasmine.NestedResults.prototype.log = function(values) {
+  this.items_.push(new jasmine.MessageResult(values));
+};
+
+/**
+ * Getter for the results: message & results.
+ */
+jasmine.NestedResults.prototype.getItems = function() {
+  return this.items_;
+};
+
+/**
+ * Adds a result, tracking counts (total, passed, & failed)
+ * @param {jasmine.ExpectationResult|jasmine.NestedResults} result
+ */
+jasmine.NestedResults.prototype.addResult = function(result) {
+  if (result.type != 'log') {
+    if (result.items_) {
+      this.rollupCounts(result);
+    } else {
+      this.totalCount++;
+      if (result.passed()) {
+        this.passedCount++;
+      } else {
+        this.failedCount++;
+      }
+    }
+  }
+  this.items_.push(result);
+};
+
+/**
+ * @returns {Boolean} True if <b>everything</b> below passed
+ */
+jasmine.NestedResults.prototype.passed = function() {
+  return this.passedCount === this.totalCount;
+};
+/**
+ * Base class for pretty printing for expectation results.
+ */
+jasmine.PrettyPrinter = function() {
+  this.ppNestLevel_ = 0;
+};
+
+/**
+ * Formats a value in a nice, human-readable string.
+ *
+ * @param value
+ */
+jasmine.PrettyPrinter.prototype.format = function(value) {
+  if (this.ppNestLevel_ > 40) {
+    throw new Error('jasmine.PrettyPrinter: format() nested too deeply!');
+  }
+
+  this.ppNestLevel_++;
+  try {
+    if (value === jasmine.undefined) {
+      this.emitScalar('undefined');
+    } else if (value === null) {
+      this.emitScalar('null');
+    } else if (value === jasmine.getGlobal()) {
+      this.emitScalar('<global>');
+    } else if (value instanceof jasmine.Matchers.Any) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'string') {
+      this.emitString(value);
+    } else if (jasmine.isSpy(value)) {
+      this.emitScalar("spy on " + value.identity);
+    } else if (value instanceof RegExp) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'function') {
+      this.emitScalar('Function');
+    } else if (typeof value.nodeType === 'number') {
+      this.emitScalar('HTMLNode');
+    } else if (value instanceof Date) {
+      this.emitScalar('Date(' + value + ')');
+    } else if (value.__Jasmine_been_here_before__) {
+      this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+    } else if (jasmine.isArray_(value) || typeof value == 'object') {
+      value.__Jasmine_been_here_before__ = true;
+      if (jasmine.isArray_(value)) {
+        this.emitArray(value);
+      } else {
+        this.emitObject(value);
+      }
+      delete value.__Jasmine_been_here_before__;
+    } else {
+      this.emitScalar(value.toString());
+    }
+  } finally {
+    this.ppNestLevel_--;
+  }
+};
+
+jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+  for (var property in obj) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) != null) : false);
+  }
+};
+
+jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;
+
+jasmine.StringPrettyPrinter = function() {
+  jasmine.PrettyPrinter.call(this);
+
+  this.string = '';
+};
+jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);
+
+jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {
+  this.append(value);
+};
+
+jasmine.StringPrettyPrinter.prototype.emitString = function(value) {
+  this.append("'" + value + "'");
+};
+
+jasmine.StringPrettyPrinter.prototype.emitArray = function(array) {
+  this.append('[ ');
+  for (var i = 0; i < array.length; i++) {
+    if (i > 0) {
+      this.append(', ');
+    }
+    this.format(array[i]);
+  }
+  this.append(' ]');
+};
+
+jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
+  var self = this;
+  this.append('{ ');
+  var first = true;
+
+  this.iterateObject(obj, function(property, isGetter) {
+    if (first) {
+      first = false;
+    } else {
+      self.append(', ');
+    }
+
+    self.append(property);
+    self.append(' : ');
+    if (isGetter) {
+      self.append('<getter>');
+    } else {
+      self.format(obj[property]);
+    }
+  });
+
+  this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+  this.string += value;
+};
+jasmine.Queue = function(env) {
+  this.env = env;
+  this.blocks = [];
+  this.running = false;
+  this.index = 0;
+  this.offset = 0;
+  this.abort = false;
+};
+
+jasmine.Queue.prototype.addBefore = function(block) {
+  this.blocks.unshift(block);
+};
+
+jasmine.Queue.prototype.add = function(block) {
+  this.blocks.push(block);
+};
+
+jasmine.Queue.prototype.insertNext = function(block) {
+  this.blocks.splice((this.index + this.offset + 1), 0, block);
+  this.offset++;
+};
+
+jasmine.Queue.prototype.start = function(onComplete) {
+  this.running = true;
+  this.onComplete = onComplete;
+  this.next_();
+};
+
+jasmine.Queue.prototype.isRunning = function() {
+  return this.running;
+};
+
+jasmine.Queue.LOOP_DONT_RECURSE = true;
+
+jasmine.Queue.prototype.next_ = function() {
+  var self = this;
+  var goAgain = true;
+
+  while (goAgain) {
+    goAgain = false;
+    
+    if (self.index < self.blocks.length && !this.abort) {
+      var calledSynchronously = true;
+      var completedSynchronously = false;
+
+      var onComplete = function () {
+        if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {
+          completedSynchronously = true;
+          return;
+        }
+
+        if (self.blocks[self.index].abort) {
+          self.abort = true;
+        }
+
+        self.offset = 0;
+        self.index++;
+
+        var now = new Date().getTime();
+        if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {
+          self.env.lastUpdate = now;
+          self.env.setTimeout(function() {
+            self.next_();
+          }, 0);
+        } else {
+          if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {
+            goAgain = true;
+          } else {
+            self.next_();
+          }
+        }
+      };
+      self.blocks[self.index].execute(onComplete);
+
+      calledSynchronously = false;
+      if (completedSynchronously) {
+        onComplete();
+      }
+      
+    } else {
+      self.running = false;
+      if (self.onComplete) {
+        self.onComplete();
+      }
+    }
+  }
+};
+
+jasmine.Queue.prototype.results = function() {
+  var results = new jasmine.NestedResults();
+  for (var i = 0; i < this.blocks.length; i++) {
+    if (this.blocks[i].results) {
+      results.addResult(this.blocks[i].results());
+    }
+  }
+  return results;
+};
+
+
+/**
+ * Runner
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ */
+jasmine.Runner = function(env) {
+  var self = this;
+  self.env = env;
+  self.queue = new jasmine.Queue(env);
+  self.before_ = [];
+  self.after_ = [];
+  self.suites_ = [];
+};
+
+jasmine.Runner.prototype.execute = function() {
+  var self = this;
+  if (self.env.reporter.reportRunnerStarting) {
+    self.env.reporter.reportRunnerStarting(this);
+  }
+  self.queue.start(function () {
+    self.finishCallback();
+  });
+};
+
+jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.splice(0,0,beforeEachFunction);
+};
+
+jasmine.Runner.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.splice(0,0,afterEachFunction);
+};
+
+
+jasmine.Runner.prototype.finishCallback = function() {
+  this.env.reporter.reportRunnerResults(this);
+};
+
+jasmine.Runner.prototype.addSuite = function(suite) {
+  this.suites_.push(suite);
+};
+
+jasmine.Runner.prototype.add = function(block) {
+  if (block instanceof jasmine.Suite) {
+    this.addSuite(block);
+  }
+  this.queue.add(block);
+};
+
+jasmine.Runner.prototype.specs = function () {
+  var suites = this.suites();
+  var specs = [];
+  for (var i = 0; i < suites.length; i++) {
+    specs = specs.concat(suites[i].specs());
+  }
+  return specs;
+};
+
+jasmine.Runner.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Runner.prototype.topLevelSuites = function() {
+  var topLevelSuites = [];
+  for (var i = 0; i < this.suites_.length; i++) {
+    if (!this.suites_[i].parentSuite) {
+      topLevelSuites.push(this.suites_[i]);
+    }
+  }
+  return topLevelSuites;
+};
+
+jasmine.Runner.prototype.results = function() {
+  return this.queue.results();
+};
+/**
+ * Internal representation of a Jasmine specification, or test.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {jasmine.Suite} suite
+ * @param {String} description
+ */
+jasmine.Spec = function(env, suite, description) {
+  if (!env) {
+    throw new Error('jasmine.Env() required');
+  }
+  if (!suite) {
+    throw new Error('jasmine.Suite() required');
+  }
+  var spec = this;
+  spec.id = env.nextSpecId ? env.nextSpecId() : null;
+  spec.env = env;
+  spec.suite = suite;
+  spec.description = description;
+  spec.queue = new jasmine.Queue(env);
+
+  spec.afterCallbacks = [];
+  spec.spies_ = [];
+
+  spec.results_ = new jasmine.NestedResults();
+  spec.results_.description = description;
+  spec.matchersClass = null;
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+  return this.suite.getFullName() + ' ' + this.description + '.';
+};
+
+
+jasmine.Spec.prototype.results = function() {
+  return this.results_;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.Spec.prototype.log = function() {
+  return this.results_.log(arguments);
+};
+
+jasmine.Spec.prototype.runs = function (func) {
+  var block = new jasmine.Block(this.env, func, this);
+  this.addToQueue(block);
+  return this;
+};
+
+jasmine.Spec.prototype.addToQueue = function (block) {
+  if (this.queue.isRunning()) {
+    this.queue.insertNext(block);
+  } else {
+    this.queue.add(block);
+  }
+};
+
+/**
+ * @param {jasmine.ExpectationResult} result
+ */
+jasmine.Spec.prototype.addMatcherResult = function(result) {
+  this.results_.addResult(result);
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+  var positive = new (this.getMatchersClass_())(this.env, actual, this);
+  positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
+  return positive;
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+jasmine.Spec.prototype.waits = function(timeout) {
+  var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);
+  this.addToQueue(waitsFunc);
+  return this;
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  var latchFunction_ = null;
+  var optional_timeoutMessage_ = null;
+  var optional_timeout_ = null;
+
+  for (var i = 0; i < arguments.length; i++) {
+    var arg = arguments[i];
+    switch (typeof arg) {
+      case 'function':
+        latchFunction_ = arg;
+        break;
+      case 'string':
+        optional_timeoutMessage_ = arg;
+        break;
+      case 'number':
+        optional_timeout_ = arg;
+        break;
+    }
+  }
+
+  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
+  this.addToQueue(waitsForFunc);
+  return this;
+};
+
+jasmine.Spec.prototype.fail = function (e) {
+  var expectationResult = new jasmine.ExpectationResult({
+    passed: false,
+    message: e ? jasmine.util.formatException(e) : 'Exception'
+  });
+  this.results_.addResult(expectationResult);
+};
+
+jasmine.Spec.prototype.getMatchersClass_ = function() {
+  return this.matchersClass || this.env.matchersClass;
+};
+
+jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
+  var parent = this.getMatchersClass_();
+  var newMatchersClass = function() {
+    parent.apply(this, arguments);
+  };
+  jasmine.util.inherit(newMatchersClass, parent);
+  jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+  this.matchersClass = newMatchersClass;
+};
+
+jasmine.Spec.prototype.finishCallback = function() {
+  this.env.reporter.reportSpecResults(this);
+};
+
+jasmine.Spec.prototype.finish = function(onComplete) {
+  this.removeAllSpies();
+  this.finishCallback();
+  if (onComplete) {
+    onComplete();
+  }
+};
+
+jasmine.Spec.prototype.after = function(doAfter) {
+  if (this.queue.isRunning()) {
+    this.queue.add(new jasmine.Block(this.env, doAfter, this));
+  } else {
+    this.afterCallbacks.unshift(doAfter);
+  }
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+  var spec = this;
+  if (!spec.env.specFilter(spec)) {
+    spec.results_.skipped = true;
+    spec.finish(onComplete);
+    return;
+  }
+
+  this.env.reporter.reportSpecStarting(this);
+
+  spec.env.currentSpec = spec;
+
+  spec.addBeforesAndAftersToQueue();
+
+  spec.queue.start(function () {
+    spec.finish(onComplete);
+  });
+};
+
+jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {
+  var runner = this.env.currentRunner();
+  var i;
+
+  for (var suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.before_.length; i++) {
+      this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));
+    }
+  }
+  for (i = 0; i < runner.before_.length; i++) {
+    this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));
+  }
+  for (i = 0; i < this.afterCallbacks.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this));
+  }
+  for (suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.after_.length; i++) {
+      this.queue.add(new jasmine.Block(this.env, suite.after_[i], this));
+    }
+  }
+  for (i = 0; i < runner.after_.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, runner.after_[i], this));
+  }
+};
+
+jasmine.Spec.prototype.explodes = function() {
+  throw 'explodes function should not have been called';
+};
+
+jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {
+  if (obj == jasmine.undefined) {
+    throw "spyOn could not find an object to spy upon for " + methodName + "()";
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {
+    throw methodName + '() method does not exist';
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {
+    throw new Error(methodName + ' has already been spied upon');
+  }
+
+  var spyObj = jasmine.createSpy(methodName);
+
+  this.spies_.push(spyObj);
+  spyObj.baseObj = obj;
+  spyObj.methodName = methodName;
+  spyObj.originalValue = obj[methodName];
+
+  obj[methodName] = spyObj;
+
+  return spyObj;
+};
+
+jasmine.Spec.prototype.removeAllSpies = function() {
+  for (var i = 0; i < this.spies_.length; i++) {
+    var spy = this.spies_[i];
+    spy.baseObj[spy.methodName] = spy.originalValue;
+  }
+  this.spies_ = [];
+};
+
+/**
+ * Internal representation of a Jasmine suite.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {String} description
+ * @param {Function} specDefinitions
+ * @param {jasmine.Suite} parentSuite
+ */
+jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
+  var self = this;
+  self.id = env.nextSuiteId ? env.nextSuiteId() : null;
+  self.description = description;
+  self.queue = new jasmine.Queue(env);
+  self.parentSuite = parentSuite;
+  self.env = env;
+  self.before_ = [];
+  self.after_ = [];
+  self.children_ = [];
+  self.suites_ = [];
+  self.specs_ = [];
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+  var fullName = this.description;
+  for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+    fullName = parentSuite.description + ' ' + fullName;
+  }
+  return fullName;
+};
+
+jasmine.Suite.prototype.finish = function(onComplete) {
+  this.env.reporter.reportSuiteResults(this);
+  this.finished = true;
+  if (typeof(onComplete) == 'function') {
+    onComplete();
+  }
+};
+
+jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.unshift(beforeEachFunction);
+};
+
+jasmine.Suite.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.unshift(afterEachFunction);
+};
+
+jasmine.Suite.prototype.results = function() {
+  return this.queue.results();
+};
+
+jasmine.Suite.prototype.add = function(suiteOrSpec) {
+  this.children_.push(suiteOrSpec);
+  if (suiteOrSpec instanceof jasmine.Suite) {
+    this.suites_.push(suiteOrSpec);
+    this.env.currentRunner().addSuite(suiteOrSpec);
+  } else {
+    this.specs_.push(suiteOrSpec);
+  }
+  this.queue.add(suiteOrSpec);
+};
+
+jasmine.Suite.prototype.specs = function() {
+  return this.specs_;
+};
+
+jasmine.Suite.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Suite.prototype.children = function() {
+  return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+  var self = this;
+  this.queue.start(function () {
+    self.finish(onComplete);
+  });
+};
+jasmine.WaitsBlock = function(env, timeout, spec) {
+  this.timeout = timeout;
+  jasmine.Block.call(this, env, null, spec);
+};
+
+jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);
+
+jasmine.WaitsBlock.prototype.execute = function (onComplete) {
+  this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');
+  this.env.setTimeout(function () {
+    onComplete();
+  }, this.timeout);
+};
+/**
+ * A block which waits for some condition to become true, with timeout.
+ *
+ * @constructor
+ * @extends jasmine.Block
+ * @param {jasmine.Env} env The Jasmine environment.
+ * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.
+ * @param {Function} latchFunction A function which returns true when the desired condition has been met.
+ * @param {String} message The message to display if the desired condition hasn't been met within the given time period.
+ * @param {jasmine.Spec} spec The Jasmine spec.
+ */
+jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {
+  this.timeout = timeout || env.defaultTimeoutInterval;
+  this.latchFunction = latchFunction;
+  this.message = message;
+  this.totalTimeSpentWaitingForLatch = 0;
+  jasmine.Block.call(this, env, null, spec);
+};
+jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);
+
+jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;
+
+jasmine.WaitsForBlock.prototype.execute = function(onComplete) {
+  this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));
+  var latchFunctionResult;
+  try {
+    latchFunctionResult = this.latchFunction.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+    onComplete();
+    return;
+  }
+
+  if (latchFunctionResult) {
+    onComplete();
+  } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {
+    var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');
+    this.spec.fail({
+      name: 'timeout',
+      message: message
+    });
+
+    this.abort = true;
+    onComplete();
+  } else {
+    this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;
+    var self = this;
+    this.env.setTimeout(function() {
+      self.execute(onComplete);
+    }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);
+  }
+};
+// Mock setTimeout, clearTimeout
+// Contributed by Pivotal Computer Systems, www.pivotalsf.com
+
+jasmine.FakeTimer = function() {
+  this.reset();
+
+  var self = this;
+  self.setTimeout = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);
+    return self.timeoutsMade;
+  };
+
+  self.setInterval = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);
+    return self.timeoutsMade;
+  };
+
+  self.clearTimeout = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+  self.clearInterval = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+};
+
+jasmine.FakeTimer.prototype.reset = function() {
+  this.timeoutsMade = 0;
+  this.scheduledFunctions = {};
+  this.nowMillis = 0;
+};
+
+jasmine.FakeTimer.prototype.tick = function(millis) {
+  var oldMillis = this.nowMillis;
+  var newMillis = oldMillis + millis;
+  this.runFunctionsWithinRange(oldMillis, newMillis);
+  this.nowMillis = newMillis;
+};
+
+jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {
+  var scheduledFunc;
+  var funcsToRun = [];
+  for (var timeoutKey in this.scheduledFunctions) {
+    scheduledFunc = this.scheduledFunctions[timeoutKey];
+    if (scheduledFunc != jasmine.undefined &&
+        scheduledFunc.runAtMillis >= oldMillis &&
+        scheduledFunc.runAtMillis <= nowMillis) {
+      funcsToRun.push(scheduledFunc);
+      this.scheduledFunctions[timeoutKey] = jasmine.undefined;
+    }
+  }
+
+  if (funcsToRun.length > 0) {
+    funcsToRun.sort(function(a, b) {
+      return a.runAtMillis - b.runAtMillis;
+    });
+    for (var i = 0; i < funcsToRun.length; ++i) {
+      try {
+        var funcToRun = funcsToRun[i];
+        this.nowMillis = funcToRun.runAtMillis;
+        funcToRun.funcToCall();
+        if (funcToRun.recurring) {
+          this.scheduleFunction(funcToRun.timeoutKey,
+              funcToRun.funcToCall,
+              funcToRun.millis,
+              true);
+        }
+      } catch(e) {
+      }
+    }
+    this.runFunctionsWithinRange(oldMillis, nowMillis);
+  }
+};
+
+jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {
+  this.scheduledFunctions[timeoutKey] = {
+    runAtMillis: this.nowMillis + millis,
+    funcToCall: funcToCall,
+    recurring: recurring,
+    timeoutKey: timeoutKey,
+    millis: millis
+  };
+};
+
+/**
+ * @namespace
+ */
+jasmine.Clock = {
+  defaultFakeTimer: new jasmine.FakeTimer(),
+
+  reset: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.reset();
+  },
+
+  tick: function(millis) {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.tick(millis);
+  },
+
+  runFunctionsWithinRange: function(oldMillis, nowMillis) {
+    jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);
+  },
+
+  scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
+    jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);
+  },
+
+  useMock: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      var spec = jasmine.getEnv().currentSpec;
+      spec.after(jasmine.Clock.uninstallMock);
+
+      jasmine.Clock.installMock();
+    }
+  },
+
+  installMock: function() {
+    jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;
+  },
+
+  uninstallMock: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.installed = jasmine.Clock.real;
+  },
+
+  real: {
+    setTimeout: jasmine.getGlobal().setTimeout,
+    clearTimeout: jasmine.getGlobal().clearTimeout,
+    setInterval: jasmine.getGlobal().setInterval,
+    clearInterval: jasmine.getGlobal().clearInterval
+  },
+
+  assertInstalled: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+    }
+  },
+
+  isInstalled: function() {
+    return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;
+  },
+
+  installed: null
+};
+jasmine.Clock.installed = jasmine.Clock.real;
+
+//else for IE support
+jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setTimeout.apply) {
+    return jasmine.Clock.installed.setTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setTimeout(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().setInterval = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setInterval.apply) {
+    return jasmine.Clock.installed.setInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setInterval(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().clearTimeout = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearTimeout(timeoutKey);
+  }
+};
+
+jasmine.getGlobal().clearInterval = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearInterval(timeoutKey);
+  }
+};
+
+
+jasmine.version_= {
+  "major": 1,
+  "minor": 0,
+  "build": "0.rc1",
+  "revision": 1282853377
+};
diff --git a/spec/runner.html b/spec/runner.html
index 3013985..27870b7 100644
--- a/spec/runner.html
+++ b/spec/runner.html
@@ -2,24 +2,24 @@
 <html>
 <head>
 	<title>Jasmine Test Runner</title>
-	<link rel="stylesheet" type="text/css" href="../lib/jasmine/jasmine.css">
-	<script type="text/javascript" src="../lib/jasmine/jasmine.js"></script>
-	<script type="text/javascript" src="../lib/jasmine/jasmine-html.js"></script>
-	
+	<link rel="stylesheet" type="text/css" href="jasmine/jasmine.css">
+	<script type="text/javascript" src="jasmine/jasmine.js"></script>
+	<script type="text/javascript" src="jasmine/jasmine-html.js"></script>
+
 	<!-- source files -->
-		
+
 		<script type="text/javascript">
 			L = 'test'; //to test L#noConflict later
 		</script>
-		
+
 		<script type="text/javascript" src="../debug/leaflet-include.js"></script>
-		
+
 
 	<!-- spec files -->
-		
+
 		<script type="text/javascript" src="suites/SpecHelper.js"></script>
 		<script type="text/javascript" src="suites/LeafletSpec.js"></script>
-	
+
 		<!-- /core -->
 		<script type="text/javascript" src="suites/core/UtilSpec.js"></script>
 		<script type="text/javascript" src="suites/core/ClassSpec.js"></script>
@@ -42,7 +42,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>
 </head>
diff --git a/spec/suites/core/ClassSpec.js b/spec/suites/core/ClassSpec.js
index 48e5d5f..d6eef3d 100644
--- a/spec/suites/core/ClassSpec.js
+++ b/spec/suites/core/ClassSpec.js
@@ -46,6 +46,7 @@ describe("Class", function() {
 			expect(method).toHaveBeenCalled();
 		});
 		
+		/* superclass deprecated
 		it("should grant the ability to call parent methods, including constructor", function() {
 			var Klass2 = Klass.extend({
 				initialize: function() {},
@@ -60,7 +61,7 @@ describe("Class", function() {
 
 			b.constructor.superclass.bar.call(this);
 			expect(method).toHaveBeenCalled();
-		});
+		}); */
 		
 		it("should support static properties", function() {
 			expect(Klass.bla).toEqual(1);
@@ -123,6 +124,7 @@ describe("Class", function() {
 			});
 		});
 		
+		/* superclass deprecated
 		it("should have working superclass access with inheritance level > 2", function() {
 			var constructor2 = jasmine.createSpy("Klass2 constructor"),
 				constructor3 = jasmine.createSpy("Klass3 constructor");
@@ -149,5 +151,6 @@ describe("Class", function() {
 			expect(constructor2).toHaveBeenCalled();
 			expect(constructor).toHaveBeenCalled();
 		});
+		*/
 	});
 });
\ No newline at end of file
diff --git a/src/Leaflet.js b/src/Leaflet.js
index 5532614..8775784 100644
--- a/src/Leaflet.js
+++ b/src/Leaflet.js
@@ -1,33 +1,18 @@
+var L, originalL;
 
-(function (root) {
-	root.L = {
-		VERSION: '0.3',
+if (typeof exports !== 'undefined') {
+	L = exports;
+} else {
+	L = {};
+	
+	originalL = window.L;
 
-		ROOT_URL: root.L_ROOT_URL || (function () {
-			var scripts = document.getElementsByTagName('script'),
-				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.split(leafletRe)[0] + '/';
-				}
-			}
-			return '';
-		}()),
+	L.noConflict = function () {
+		window.L = originalL;
+		return L;
+	};
 
-		noConflict: function () {
-			root.L = this._originalL;
-			return this;
-		},
+	window.L = L;
+}
 
-		_originalL: root.L
-	};
-}(this));
+L.version = '0.4';
diff --git a/src/control/Control.Attribution.js b/src/control/Control.Attribution.js
index c56e5ed..fb3a1b8 100644
--- a/src/control/Control.Attribution.js
+++ b/src/control/Control.Attribution.js
@@ -1,69 +1,100 @@
-L.Control.Attribution = L.Class.extend({
-	initialize: function (prefix) {
-		this._prefix = prefix || 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>';
+L.Control.Attribution = L.Control.extend({
+	options: {
+		position: 'bottomright',
+		prefix: 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>'
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+
 		this._attributions = {};
 	},
 
 	onAdd: function (map) {
 		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
 		L.DomEvent.disableClickPropagation(this._container);
-		this._map = map;
+
+		map
+			.on('layeradd', this._onLayerAdd, this)
+			.on('layerremove', this._onLayerRemove, this);
+
 		this._update();
-	},
 
-	getPosition: function () {
-		return L.Control.Position.BOTTOM_RIGHT;
+		return this._container;
 	},
 
-	getContainer: function () {
-		return this._container;
+	onRemove: function (map) {
+		map
+			.off('layeradd', this._onLayerAdd)
+			.off('layerremove', this._onLayerRemove);
+
 	},
 
 	setPrefix: function (prefix) {
-		this._prefix = prefix;
+		this.options.prefix = prefix;
 		this._update();
 	},
 
 	addAttribution: function (text) {
-		if (!text) {
-			return;
-		}
+		if (!text) { return; }
+
 		if (!this._attributions[text]) {
 			this._attributions[text] = 0;
 		}
 		this._attributions[text]++;
+
 		this._update();
 	},
 
 	removeAttribution: function (text) {
-		if (!text) {
-			return;
-		}
+		if (!text) { return; }
+
 		this._attributions[text]--;
 		this._update();
 	},
 
 	_update: function () {
-		if (!this._map) {
-			return;
-		}
+		if (!this._map) { return; }
 
 		var attribs = [];
 
 		for (var i in this._attributions) {
-			if (this._attributions.hasOwnProperty(i)) {
+			if (this._attributions.hasOwnProperty(i) && this._attributions[i]) {
 				attribs.push(i);
 			}
 		}
 
 		var prefixAndAttribs = [];
-		if (this._prefix) {
-			prefixAndAttribs.push(this._prefix);
+
+		if (this.options.prefix) {
+			prefixAndAttribs.push(this.options.prefix);
 		}
 		if (attribs.length) {
 			prefixAndAttribs.push(attribs.join(', '));
 		}
 
 		this._container.innerHTML = prefixAndAttribs.join(' — ');
+	},
+
+	_onLayerAdd: function (e) {
+		if (e.layer.getAttribution) {
+			this.addAttribution(e.layer.getAttribution());
+		}
+	},
+
+	_onLayerRemove: function (e) {
+		if (e.layer.getAttribution) {
+			this.removeAttribution(e.layer.getAttribution());
+		}
 	}
 });
+
+L.Map.mergeOptions({
+	attributionControl: true
+});
+
+L.Map.addInitHook(function () {
+	if (this.options.attributionControl) {
+		this.attributionControl = (new L.Control.Attribution()).addTo(this);
+	}
+});
\ No newline at end of file
diff --git a/src/control/Control.Layers.js b/src/control/Control.Layers.js
index a8667c1..61c7564 100644
--- a/src/control/Control.Layers.js
+++ b/src/control/Control.Layers.js
@@ -1,7 +1,8 @@
 
-L.Control.Layers = L.Class.extend({
+L.Control.Layers = L.Control.extend({
 	options: {
-		collapsed: true
+		collapsed: true,
+		position: 'topright'
 	},
 
 	initialize: function (baseLayers, overlays, options) {
@@ -23,20 +24,12 @@ L.Control.Layers = L.Class.extend({
 	},
 
 	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();
@@ -57,40 +50,47 @@ L.Control.Layers = L.Class.extend({
 	},
 
 	_initLayout: function () {
-		this._container = L.DomUtil.create('div', 'leaflet-control-layers');
+		var className = 'leaflet-control-layers',
+		    container = this._container = L.DomUtil.create('div', className);
+
 		if (!L.Browser.touch) {
-			L.DomEvent.disableClickPropagation(this._container);
+			L.DomEvent.disableClickPropagation(container);
+		} else {
+			L.DomEvent.addListener(container, 'click', L.DomEvent.stopPropagation);
 		}
 
-		this._form = L.DomUtil.create('form', 'leaflet-control-layers-list');
+		var form = this._form = L.DomUtil.create('form', className + '-list');
 
 		if (this.options.collapsed) {
-			L.DomEvent.addListener(this._container, 'mouseover', this._expand, this);
-			L.DomEvent.addListener(this._container, 'mouseout', this._collapse, this);
+			L.DomEvent
+				.addListener(container, 'mouseover', this._expand, this)
+				.addListener(container, 'mouseout', this._collapse, this);
 
-			var link = this._layersLink = L.DomUtil.create('a', 'leaflet-control-layers-toggle');
+			var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
 			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, 'click', L.DomEvent.stopPropagation)
+					.addListener(link, 'click', L.DomEvent.preventDefault)
+					.addListener(link, 'click', this._expand, this);
+			}
+			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._baseLayersList = L.DomUtil.create('div', className + '-base', form);
+		this._separator = L.DomUtil.create('div', className + '-separator', form);
+		this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
 
-		this._container.appendChild(this._form);
+		container.appendChild(form);
 	},
 
 	_addLayer: function (layer, name, overlay) {
@@ -133,8 +133,8 @@ L.Control.Layers = L.Class.extend({
 			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);
+		input.defaultChecked = this._map.hasLayer(obj.layer);
 
 		L.DomEvent.addListener(input, 'click', this._onInputClick, this);
 
diff --git a/src/control/Control.Scale.js b/src/control/Control.Scale.js
new file mode 100644
index 0000000..3b90528
--- /dev/null
+++ b/src/control/Control.Scale.js
@@ -0,0 +1,94 @@
+L.Control.Scale = L.Control.extend({
+	options: {
+		position: 'bottomleft',
+		maxWidth: 100,
+		metric: true,
+		imperial: true,
+		updateWhenIdle: false
+	},
+
+	onAdd: function (map) {
+		this._map = map;
+
+		var className = 'leaflet-control-scale',
+		    container = L.DomUtil.create('div', className),
+		    options = this.options;
+
+		if (options.metric) {
+			this._mScale = L.DomUtil.create('div', className + '-line', container);
+		}
+		if (options.imperial) {
+			this._iScale = L.DomUtil.create('div', className + '-line', container);
+		}
+
+		map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
+		this._update();
+
+		return container;
+	},
+
+	onRemove: function (map) {
+		map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
+	},
+
+	_update: function () {
+		var bounds = this._map.getBounds(),
+		    centerLat = bounds.getCenter().lat,
+
+		    left = new L.LatLng(centerLat, bounds.getSouthWest().lng),
+		    right = new L.LatLng(centerLat, bounds.getNorthEast().lng),
+
+		    size = this._map.getSize(),
+		    options = this.options,
+
+		    maxMeters = left.distanceTo(right) * (options.maxWidth / size.x);
+
+		if (options.metric) {
+			this._updateMetric(maxMeters);
+		}
+
+		if (options.imperial) {
+			this._updateImperial(maxMeters);
+		}
+	},
+
+	_updateMetric: function (maxMeters) {
+		var meters = this._getRoundNum(maxMeters);
+
+		this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px';
+		this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
+	},
+
+	_updateImperial: function (maxMeters) {
+		var maxFeet = maxMeters * 3.2808399,
+			scale = this._iScale,
+			maxMiles, miles, feet;
+
+		if (maxFeet > 5280) {
+			maxMiles = maxFeet / 5280;
+			miles = this._getRoundNum(maxMiles);
+
+			scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
+			scale.innerHTML = miles + ' mi';
+
+		} else {
+			feet = this._getRoundNum(maxFeet);
+
+			scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
+			scale.innerHTML = feet + ' ft';
+		}
+	},
+
+	_getScaleWidth: function (ratio) {
+		return Math.round(this.options.maxWidth * ratio) - 10;
+	},
+
+	_getRoundNum: function (num) {
+		var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
+		    d = num / pow10;
+
+		d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 2 ? 2 : 1;
+
+		return pow10 * d;
+	}
+});
\ No newline at end of file
diff --git a/src/control/Control.Zoom.js b/src/control/Control.Zoom.js
index 38cfd8e..06e6388 100644
--- a/src/control/Control.Zoom.js
+++ b/src/control/Control.Zoom.js
@@ -1,38 +1,39 @@
+L.Control.Zoom = L.Control.extend({
+	options: {
+		position: 'topleft'
+	},
 
-L.Control.Zoom = L.Class.extend({
 	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(
-				'Zoom out', 'leaflet-control-zoom-out', this._map.zoomOut, this._map);
+		var className = 'leaflet-control-zoom',
+		    container = L.DomUtil.create('div', className);
 
-		this._container.appendChild(this._zoomInButton);
-		this._container.appendChild(this._zoomOutButton);
-	},
-
-	getContainer: function () {
-		return this._container;
-	},
+		this._createButton('Zoom in', className + '-in', container, map.zoomIn, map);
+		this._createButton('Zoom out', className + '-out', container, map.zoomOut, map);
 
-	getPosition: function () {
-		return L.Control.Position.TOP_LEFT;
+		return container;
 	},
 
-	_createButton: function (title, className, fn, context) {
-		var link = document.createElement('a');
+	_createButton: function (title, className, container, fn, context) {
+		var link = L.DomUtil.create('a', className, container);
 		link.href = '#';
 		link.title = title;
-		link.className = className;
 
-		if (!L.Browser.touch) {
-			L.DomEvent.disableClickPropagation(link);
-		}
-		L.DomEvent.addListener(link, 'click', L.DomEvent.preventDefault);
-		L.DomEvent.addListener(link, 'click', fn, context);
+		L.DomEvent
+			.addListener(link, 'click', L.DomEvent.stopPropagation)
+			.addListener(link, 'click', L.DomEvent.preventDefault)
+			.addListener(link, 'click', fn, context);
 
 		return link;
 	}
 });
+
+L.Map.mergeOptions({
+	zoomControl: true
+});
+
+L.Map.addInitHook(function () {
+	if (this.options.zoomControl) {
+		this.zoomControl = new L.Control.Zoom();
+		this.addControl(this.zoomControl);
+	}
+});
\ No newline at end of file
diff --git a/src/control/Control.js b/src/control/Control.js
index 2d60e95..1baee7c 100644
--- a/src/control/Control.js
+++ b/src/control/Control.js
@@ -1,9 +1,61 @@
 
-L.Control = {};
-
-L.Control.Position = {
-	TOP_LEFT: 'topLeft',
-	TOP_RIGHT: 'topRight',
-	BOTTOM_LEFT: 'bottomLeft',
-	BOTTOM_RIGHT: 'bottomRight'
-};
+L.Control = L.Class.extend({
+	options: {
+		position: 'topright'
+	},
+
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
+	},
+
+	getPosition: function () {
+		return this.options.position;
+	},
+
+	setPosition: function (position) {
+		var map = this._map;
+
+		if (map) {
+			map.removeControl(this);
+		}
+
+		this.options.position = position;
+
+		if (map) {
+			map.addControl(this);
+		}
+	},
+
+	addTo: function (map) {
+		this._map = map;
+
+		var container = this._container = this.onAdd(map),
+		    pos = this.getPosition(),
+			corner = map._controlCorners[pos];
+
+		L.DomUtil.addClass(container, 'leaflet-control');
+
+		if (pos.indexOf('bottom') !== -1) {
+			corner.insertBefore(container, corner.firstChild);
+		} else {
+			corner.appendChild(container);
+		}
+
+		return this;
+	},
+
+	removeFrom: function (map) {
+		var pos = this.getPosition(),
+			corner = map._controlCorners[pos];
+
+		corner.removeChild(this._container);
+		this._map = null;
+
+		if (this.onRemove) {
+			this.onRemove(map);
+		}
+
+		return this;
+	}
+});
+
diff --git a/src/core/Browser.js b/src/core/Browser.js
index ec54817..09cb16c 100644
--- a/src/core/Browser.js
+++ b/src/core/Browser.js
@@ -2,6 +2,7 @@
 	var ua = navigator.userAgent.toLowerCase(),
 		ie = !!window.ActiveXObject,
 		webkit = ua.indexOf("webkit") !== -1,
+		gecko = ua.indexOf("gecko") !== -1,
 		mobile = typeof orientation !== 'undefined' ? true : false,
 		android = ua.indexOf("android") !== -1,
 		opera = window.opera;
@@ -13,9 +14,11 @@
 		webkit: webkit,
 		webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()),
 
-		gecko: ua.indexOf("gecko") !== -1,
+		gecko: gecko,
+		gecko3d: gecko && ('MozPerspective' in document.createElement('div').style),
 
 		opera: opera,
+		opera3d: opera && ('OTransition' in document.createElement('div').style),
 
 		android: android,
 		mobileWebkit: mobile && webkit,
@@ -50,4 +53,6 @@
 			return touchSupported;
 		}())
 	};
+	L.Browser.any3d = !!L.Browser.webkit3d || !!L.Browser.gecko3d || !!L.Browser.opera3d;
+		
 }());
diff --git a/src/core/Class.js b/src/core/Class.js
index e1bfc0d..70eae2b 100644
--- a/src/core/Class.js
+++ b/src/core/Class.js
@@ -16,20 +16,15 @@ L.Class.extend = function (/*Object*/ props) /*-> Class*/ {
 	// instantiate class without calling constructor
 	var F = function () {};
 	F.prototype = this.prototype;
-	var proto = new F();
 
+	var proto = new F();
 	proto.constructor = NewClass;
-	NewClass.prototype = proto;
 
-	// add superclass access
-	NewClass.superclass = this.prototype;
-
-	// add class name
-	//proto.className = props;
+	NewClass.prototype = proto;
 
 	//inherit parent's statics
 	for (var i in this) {
-		if (this.hasOwnProperty(i) && i !== 'prototype' && i !== 'superclass') {
+		if (this.hasOwnProperty(i) && i !== 'prototype') {
 			NewClass[i] = this[i];
 		}
 	}
@@ -54,13 +49,15 @@ L.Class.extend = function (/*Object*/ props) /*-> Class*/ {
 	// mix given properties into the prototype
 	L.Util.extend(proto, props);
 
-	// allow inheriting further
-	NewClass.extend = L.Class.extend;
+	return NewClass;
+};
 
-	// method for adding properties to prototype
-	NewClass.include = function (props) {
-		L.Util.extend(this.prototype, props);
-	};
 
-	return NewClass;
+// method for adding properties to prototype
+L.Class.include = function (props) {
+	L.Util.extend(this.prototype, props);
 };
+
+L.Class.mergeOptions = function (options) {
+	L.Util.extend(this.prototype.options, options);
+};
\ No newline at end of file
diff --git a/src/core/Util.js b/src/core/Util.js
index 1bd85e2..bae1436 100644
--- a/src/core/Util.js
+++ b/src/core/Util.js
@@ -16,9 +16,10 @@ L.Util = {
 		return dest;
 	},
 
-	bind: function (/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
+	bind: function (fn, obj) { // (Function, Object) -> Function
+		var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null;
 		return function () {
-			return fn.apply(obj, arguments);
+			return fn.apply(obj, args || arguments);
 		};
 	},
 
@@ -30,6 +31,9 @@ L.Util = {
 		};
 	}()),
 
+
+	// TODO refactor: remove repetition
+
 	requestAnimFrame: (function () {
 		function timeoutDefer(callback) {
 			window.setTimeout(callback, 1000 / 60);
@@ -47,29 +51,48 @@ L.Util = {
 			if (immediate && requestFn === timeoutDefer) {
 				callback();
 			} else {
-				requestFn(callback, contextEl);
+				return requestFn.call(window, callback, contextEl);
 			}
 		};
 	}()),
 
+	cancelAnimFrame: (function () {
+		var requestFn = window.cancelAnimationFrame ||
+			window.webkitCancelRequestAnimationFrame ||
+			window.mozCancelRequestAnimationFrame ||
+			window.oCancelRequestAnimationFrame ||
+			window.msCancelRequestAnimationFrame ||
+			clearTimeout;
+
+		return function (handle) {
+			if (!handle) { return; }
+			return requestFn.call(window, handle);
+		};
+	}()),
+
 	limitExecByInterval: function (fn, time, context) {
-		var lock, execOnUnlock, args;
-		function exec() {
-			lock = false;
-			if (execOnUnlock) {
-				args.callee.apply(context, args);
-				execOnUnlock = false;
-			}
-		}
-		return function () {
-			args = arguments;
-			if (!lock) {
-				lock = true;
-				setTimeout(exec, time);
-				fn.apply(context, args);
-			} else {
+		var lock, execOnUnlock;
+		
+		return function wrapperFn() {
+			var args = arguments;
+
+			if (lock) {
 				execOnUnlock = true;
+				return;
 			}
+
+			lock = true;
+			
+			setTimeout(function () {
+				lock = false;
+				
+				if (execOnUnlock) {
+					wrapperFn.apply(context, args);
+					execOnUnlock = false;
+				}
+			}, time);
+
+			fn.apply(context, args);
 		};
 	},
 
@@ -84,6 +107,7 @@ L.Util = {
 
 	setOptions: function (obj, options) {
 		obj.options = L.Util.extend({}, obj.options, options);
+		return obj.options;
 	},
 
 	getParamString: function (obj) {
@@ -104,5 +128,7 @@ L.Util = {
 			}
 			return value;
 		});
-	}
+	},
+
+	emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
 };
diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js
index 5ac4f07..3240c54 100644
--- a/src/dom/DomEvent.js
+++ b/src/dom/DomEvent.js
@@ -9,7 +9,7 @@ L.DomEvent = {
 			key = '_leaflet_' + type + id;
 
 		if (obj[key]) {
-			return;
+			return this;
 		}
 
 		var handler = function (e) {
@@ -40,6 +40,8 @@ L.DomEvent = {
 		}
 
 		obj[key] = handler;
+
+		return this;
 	},
 
 	removeListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn) {
@@ -66,6 +68,8 @@ L.DomEvent = {
 			obj.detachEvent("on" + type, handler);
 		}
 		obj[key] = null;
+
+		return this;
 	},
 
 	_checkMouse: function (el, e) {
@@ -109,12 +113,14 @@ L.DomEvent = {
 		} else {
 			e.cancelBubble = true;
 		}
+		return this;
 	},
 
 	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);
+		return L.DomEvent
+			.addListener(el, L.Draggable.START, L.DomEvent.stopPropagation)
+			.addListener(el, 'click', L.DomEvent.stopPropagation)
+			.addListener(el, 'dblclick', L.DomEvent.stopPropagation);
 	},
 
 	preventDefault: function (/*Event*/ e) {
@@ -123,11 +129,13 @@ L.DomEvent = {
 		} else {
 			e.returnValue = false;
 		}
+		return this;
 	},
 
 	stop: function (e) {
-		L.DomEvent.preventDefault(e);
-		L.DomEvent.stopPropagation(e);
+		return L.DomEvent
+			.preventDefault(e)
+			.stopPropagation(e);
 	},
 
 	getMousePosition: function (e, container) {
@@ -136,6 +144,7 @@ L.DomEvent = {
 			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.getViewportOffset(container)) : pos);
 	},
diff --git a/src/dom/DomUtil.js b/src/dom/DomUtil.js
index db10201..a6cec06 100644
--- a/src/dom/DomUtil.js
+++ b/src/dom/DomUtil.js
@@ -33,6 +33,12 @@ L.DomUtil = {
 					L.DomUtil.getStyle(el, 'position') === 'absolute') {
 				break;
 			}
+			if (L.DomUtil.getStyle(el, 'position') === 'fixed') {
+				top += docBody.scrollTop || 0;
+				left += docBody.scrollLeft || 0;
+				break;
+			}
+
 			el = el.offsetParent;
 		} while (el);
 
@@ -98,7 +104,7 @@ L.DomUtil = {
 
 	setOpacity: function (el, value) {
 		if (L.Browser.ie) {
-			el.style.filter = 'alpha(opacity=' + Math.round(value * 100) + ')';
+		    el.style.filter += value !== 1 ? 'alpha(opacity=' + Math.round(value * 100) + ')' : '';
 		} else {
 			el.style.opacity = value;
 		}
@@ -133,13 +139,9 @@ L.DomUtil = {
 
 	setPosition: function (el, point) {
 		el._leaflet_pos = point;
-		if (L.Browser.webkit3d) {
+		if (L.Browser.any3d) {
 			el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
-
-			if (L.Browser.android) {
-				el.style['-webkit-perspective'] = '1000';
-				el.style['-webkit-backface-visibility'] = 'hidden';
-			}
+			el.style[L.DomUtil.BACKFACEVISIBILITY] = 'hidden';
 		} else {
 			el.style.left = point.x + 'px';
 			el.style.top = point.y + 'px';
@@ -154,6 +156,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']),
+	BACKFACEVISIBILITY: L.DomUtil.testProp(['backfaceVisibility', 'WebkitBackfaceVisibility', 'OBackfaceVisibility', 'MozBackfaceVisibility', 'msBackfaceVisibility']),
 
 	TRANSLATE_OPEN: 'translate' + (L.Browser.webkit3d ? '3d(' : '('),
 	TRANSLATE_CLOSE: L.Browser.webkit3d ? ',0)' : ')'
diff --git a/src/dom/Draggable.js b/src/dom/Draggable.js
index 53a0091..927eed1 100644
--- a/src/dom/Draggable.js
+++ b/src/dom/Draggable.js
@@ -31,6 +31,7 @@ L.Draggable = L.Class.extend({
 		}
 		L.DomEvent.removeListener(this._dragStartTarget, L.Draggable.START, this._onDown);
 		this._enabled = false;
+		this._moved = false;
 	},
 
 	_onDown: function (e) {
@@ -38,7 +39,10 @@ L.Draggable = L.Class.extend({
 			return;
 		}
 
+		this._simulateClick = true;
+
 		if (e.touches && e.touches.length > 1) {
+			this._simulateClick = false;
 			return;
 		}
 
@@ -86,7 +90,8 @@ L.Draggable = L.Class.extend({
 		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._dragStartTarget);
+		L.Util.cancelAnimFrame(this._animRequest);
+		this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
 	},
 
 	_updatePosition: function () {
@@ -96,7 +101,7 @@ L.Draggable = L.Class.extend({
 	},
 
 	_onUp: function (e) {
-		if (e.changedTouches) {
+		if (this._simulateClick && e.changedTouches) {
 			var first = e.changedTouches[0],
 				el = first.target,
 				dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0;
@@ -125,12 +130,11 @@ L.Draggable = L.Class.extend({
 	},
 
 	_setMovingCursor: function () {
-		this._bodyCursor = document.body.style.cursor;
-		document.body.style.cursor = 'move';
+		document.body.className += ' leaflet-dragging';
 	},
 
 	_restoreCursor: function () {
-		document.body.style.cursor = this._bodyCursor;
+		document.body.className = document.body.className.replace(/ leaflet-dragging/g, '');
 	},
 
 	_simulateEvent: function (type, e) {
diff --git a/src/dom/transition/Transition.Native.js b/src/dom/transition/Transition.Native.js
index 05cc381..be9c557 100644
--- a/src/dom/transition/Transition.Native.js
+++ b/src/dom/transition/Transition.Native.js
@@ -20,7 +20,7 @@ L.Transition = L.Transition.extend({
 
 			// transition-property value to use with each particular custom property
 			CUSTOM_PROPS_PROPERTIES: {
-				position: L.Browser.webkit ? L.DomUtil.TRANSFORM : 'top, left'
+				position: L.Browser.any3d ? L.DomUtil.TRANSFORM : 'top, left'
 			}
 		};
 	}()),
@@ -88,15 +88,18 @@ L.Transition = L.Transition.extend({
 		this.fire('step');
 	},
 
-	_onTransitionEnd: function () {
+	_onTransitionEnd: function (e) {
 		if (this._inProgress) {
 			this._inProgress = false;
 			clearInterval(this._timer);
 
-			this._el.style[L.Transition.PROPERTY] = 'none';
+			this._el.style[L.Transition.TRANSITION] = '';
 
 			this.fire('step');
-			this.fire('end');
+
+			if (e && e.type) {
+				this.fire('end');
+			}
 		}
 	}
 });
diff --git a/src/geo/LatLngBounds.js b/src/geo/LatLngBounds.js
index 7a87ea4..504258f 100644
--- a/src/geo/LatLngBounds.js
+++ b/src/geo/LatLngBounds.js
@@ -13,17 +13,35 @@ L.LatLngBounds = L.Class.extend({
 		}
 	},
 
-	// extend the bounds to contain the given point
-	extend: function (/*LatLng*/ latlng) {
-		if (!this._southWest && !this._northEast) {
-			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);
-			this._northEast.lat = Math.max(latlng.lat, this._northEast.lat);
-			this._northEast.lng = Math.max(latlng.lng, this._northEast.lng);
+	// extend the bounds to contain the given point or bounds
+	extend: function (/*LatLng or LatLngBounds*/ obj) {
+		if (obj instanceof L.LatLng) {
+			if (!this._southWest && !this._northEast) {
+				this._southWest = new L.LatLng(obj.lat, obj.lng, true);
+				this._northEast = new L.LatLng(obj.lat, obj.lng, true);
+			} else {
+				this._southWest.lat = Math.min(obj.lat, this._southWest.lat);
+				this._southWest.lng = Math.min(obj.lng, this._southWest.lng);
+				this._northEast.lat = Math.max(obj.lat, this._northEast.lat);
+				this._northEast.lng = Math.max(obj.lng, this._northEast.lng);
+			}
+		} else if (obj instanceof L.LatLngBounds) {
+			this.extend(obj._southWest);
+            this.extend(obj._northEast);
 		}
+		return this;
+	},
+
+	// extend the bounds by a percentage
+	pad: function (bufferRatio) { // (Number) -> LatLngBounds
+		var sw = this._southWest,
+			ne = this._northEast,
+			heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
+			widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
+
+		return new L.LatLngBounds(
+			new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
+			new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
 	},
 
 	getCenter: function () /*-> LatLng*/ {
@@ -80,6 +98,11 @@ L.LatLngBounds = L.Class.extend({
 		var sw = this._southWest,
 			ne = this._northEast;
 		return [sw.lng, sw.lat, ne.lng, ne.lat].join(',');
+	},
+
+	equals: function (/*LatLngBounds*/ bounds) {
+		return bounds ? this._southWest.equals(bounds.getSouthWest()) &&
+		                this._northEast.equals(bounds.getNorthEast()) : false;
 	}
 });
 
diff --git a/src/geo/crs/CRS.EPSG3395.js b/src/geo/crs/CRS.EPSG3395.js
index a0d40a9..84d627e 100644
--- a/src/geo/crs/CRS.EPSG3395.js
+++ b/src/geo/crs/CRS.EPSG3395.js
@@ -3,6 +3,7 @@ L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, {
 	code: 'EPSG:3395',
 
 	projection: L.Projection.Mercator,
+
 	transformation: (function () {
 		var m = L.Projection.Mercator,
 			r = m.R_MAJOR,
diff --git a/src/geo/crs/CRS.EPSG3857.js b/src/geo/crs/CRS.EPSG3857.js
index d76722a..158a00f 100644
--- a/src/geo/crs/CRS.EPSG3857.js
+++ b/src/geo/crs/CRS.EPSG3857.js
@@ -5,7 +5,7 @@ L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, {
 	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*/ {
+	project: function (latlng) { // (LatLng) -> Point
 		var projectedPoint = this.projection.project(latlng),
 			earthRadius = 6378137;
 		return projectedPoint.multiplyBy(earthRadius);
diff --git a/src/geo/crs/CRS.Simple.js b/src/geo/crs/CRS.Simple.js
new file mode 100644
index 0000000..52def07
--- /dev/null
+++ b/src/geo/crs/CRS.Simple.js
@@ -0,0 +1,5 @@
+
+L.CRS.Simple = L.Util.extend({}, L.CRS, {
+	projection: L.Projection.LonLat,
+	transformation: new L.Transformation(1, 0, 1, 0)
+});
diff --git a/src/geo/crs/CRS.js b/src/geo/crs/CRS.js
index eeb633a..107f078 100644
--- a/src/geo/crs/CRS.js
+++ b/src/geo/crs/CRS.js
@@ -1,17 +1,25 @@
 
 L.CRS = {
-	latLngToPoint: function (/*LatLng*/ latlng, /*Number*/ scale)/*-> Point*/ {
-		var projectedPoint = this.projection.project(latlng);
+	latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point
+		var projectedPoint = this.projection.project(latlng),
+		    scale = this.scale(zoom);
+
 		return this.transformation._transform(projectedPoint, scale);
 	},
 
-	pointToLatLng: function (/*Point*/ point, /*Number*/ scale, /*(optional) Boolean*/ unbounded)/*-> LatLng*/ {
-		var untransformedPoint = this.transformation.untransform(point, scale);
+	pointToLatLng: function (point, zoom, unbounded) { // (Point, Number[, Boolean]) -> LatLng
+		var scale = this.scale(zoom),
+		    untransformedPoint = this.transformation.untransform(point, scale);
+
 		return this.projection.unproject(untransformedPoint, unbounded);
 		//TODO get rid of 'unbounded' everywhere
 	},
 
 	project: function (latlng) {
 		return this.projection.project(latlng);
+	},
+
+	scale: function (zoom) {
+		return 256 * Math.pow(2, zoom);
 	}
 };
diff --git a/src/geo/projection/Projection.Mercator.js b/src/geo/projection/Projection.Mercator.js
index e89776a..860bd5c 100644
--- a/src/geo/projection/Projection.Mercator.js
+++ b/src/geo/projection/Projection.Mercator.js
@@ -5,7 +5,7 @@ L.Projection.Mercator = {
 	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),
@@ -25,7 +25,7 @@ L.Projection.Mercator = {
 		return new L.Point(x, y);
 	},
 
-	unproject: function (/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {
+	unproject: function (point, unbounded) { // (Point, Boolean) -> LatLng
 		var d = L.LatLng.RAD_TO_DEG,
 			r = this.R_MAJOR,
 			r2 = this.R_MINOR,
diff --git a/src/geo/projection/Projection.SphericalMercator.js b/src/geo/projection/Projection.SphericalMercator.js
index 275e713..d6fb5ca 100644
--- a/src/geo/projection/Projection.SphericalMercator.js
+++ b/src/geo/projection/Projection.SphericalMercator.js
@@ -2,7 +2,7 @@
 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),
@@ -13,7 +13,7 @@ L.Projection.SphericalMercator = {
 		return new L.Point(x, y);
 	},
 
-	unproject: function (/*Point*/ point, /*Boolean*/ unbounded) /*-> LatLng*/ {
+	unproject: function (point, unbounded) { // (Point, Boolean) -> LatLng
 		var d = L.LatLng.RAD_TO_DEG,
 			lng = point.x * d,
 			lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;
diff --git a/src/layer/FeatureGroup.js b/src/layer/FeatureGroup.js
index eee0b11..984c3b8 100644
--- a/src/layer/FeatureGroup.js
+++ b/src/layer/FeatureGroup.js
@@ -7,6 +7,7 @@ L.FeatureGroup = L.LayerGroup.extend({
 
 	addLayer: function (layer) {
 		this._initEvents(layer);
+
 		L.LayerGroup.prototype.addLayer.call(this, layer);
 
 		if (this._popupContent && layer.bindPopup) {
@@ -16,7 +17,6 @@ L.FeatureGroup = L.LayerGroup.extend({
 
 	bindPopup: function (content) {
 		this._popupContent = content;
-
 		return this.invoke('bindPopup', content);
 	},
 
@@ -24,17 +24,27 @@ L.FeatureGroup = L.LayerGroup.extend({
 		return this.invoke('setStyle', style);
 	},
 
-	_events: ['click', 'dblclick', 'mouseover', 'mouseout'],
+	getBounds: function () {
+		var bounds = new L.LatLngBounds();
+		this._iterateLayers(function (layer) {
+			bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds());
+		}, this);
+		return bounds;
+	},
 
 	_initEvents: function (layer) {
-		for (var i = 0, len = this._events.length; i < len; i++) {
-			layer.on(this._events[i], this._propagateEvent, this);
+		var events = ['click', 'dblclick', 'mouseover', 'mouseout'],
+			i, len;
+
+		for (i = 0, len = events.length; i < len; i++) {
+			layer.on(events[i], this._propagateEvent, this);
 		}
 	},
 
 	_propagateEvent: function (e) {
-		e.layer = e.target;
+		e.layer  = e.target;
 		e.target = this;
+
 		this.fire(e.type, e);
 	}
 });
diff --git a/src/layer/GeoJSON.js b/src/layer/GeoJSON.js
index f0c2f41..fb4f4d5 100644
--- a/src/layer/GeoJSON.js
+++ b/src/layer/GeoJSON.js
@@ -1,7 +1,7 @@
-
 L.GeoJSON = L.FeatureGroup.extend({
 	initialize: function (geojson, options) {
 		L.Util.setOptions(this, options);
+
 		this._geojson = geojson;
 		this._layers = {};
 
@@ -11,23 +11,27 @@ L.GeoJSON = L.FeatureGroup.extend({
 	},
 
 	addGeoJSON: function (geojson) {
-		if (geojson.features) {
-			for (var i = 0, len = geojson.features.length; i < len; i++) {
-				this.addGeoJSON(geojson.features[i]);
+		var features = geojson.features,
+		    i, len;
+
+		if (features) {
+			for (i = 0, len = features.length; i < len; i++) {
+				this.addGeoJSON(features[i]);
 			}
 			return;
 		}
 
 		var isFeature = (geojson.type === 'Feature'),
-			geometry = (isFeature ? geojson.geometry : geojson),
-			layer = L.GeoJSON.geometryToLayer(geometry, this.options.pointToLayer);
+		    geometry = isFeature ? geojson.geometry : geojson,
+		    layer = L.GeoJSON.geometryToLayer(geometry, this.options.pointToLayer);
 
 		this.fire('featureparse', {
 			layer: layer,
 			properties: geojson.properties,
 			geometryType: geometry.type,
 			bbox: geojson.bbox,
-			id: geojson.id
+			id: geojson.id,
+			geometry: geojson.geometry
 		});
 
 		this.addLayer(layer);
@@ -37,10 +41,8 @@ L.GeoJSON = L.FeatureGroup.extend({
 L.Util.extend(L.GeoJSON, {
 	geometryToLayer: function (geometry, pointToLayer) {
 		var coords = geometry.coordinates,
-			latlng, latlngs,
-			i, len,
-			layer,
-			layers = [];
+		    layers = [],
+		    latlng, latlngs, i, len, layer;
 
 		switch (geometry.type) {
 		case 'Point':
@@ -83,22 +85,25 @@ L.Util.extend(L.GeoJSON, {
 		}
 	},
 
-	coordsToLatLng: function (/*Array*/ coords, /*Boolean*/ reverse)/*: LatLng*/ {
+	coordsToLatLng: function (coords, reverse) { // (Array, Boolean) -> LatLng
 		var lat = parseFloat(coords[reverse ? 0 : 1]),
-			lng = parseFloat(coords[reverse ? 1 : 0]);
+		    lng = parseFloat(coords[reverse ? 1 : 0]);
+
 		return new L.LatLng(lat, lng, true);
 	},
 
-	coordsToLatLngs: function (/*Array*/ coords, /*Number*/ levelsDeep, /*Boolean*/ reverse)/*: Array*/ {
-		var latlng, latlngs = [],
-			i, len = coords.length;
+	coordsToLatLngs: function (coords, levelsDeep, reverse) { // (Array, Number, Boolean) -> Array
+		var latlng,
+		    latlngs = [],
+		    i, len;
 
-		for (i = 0; i < len; i++) {
+		for (i = 0, len = coords.length; i < len; i++) {
 			latlng = levelsDeep ?
 					this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) :
 					this.coordsToLatLng(coords[i], reverse);
 			latlngs.push(latlng);
 		}
+
 		return latlngs;
 	}
 });
diff --git a/src/layer/ImageOverlay.js b/src/layer/ImageOverlay.js
index fac5ec8..0b78055 100644
--- a/src/layer/ImageOverlay.js
+++ b/src/layer/ImageOverlay.js
@@ -13,7 +13,7 @@ L.ImageOverlay = L.Class.extend({
 			this._initImage();
 		}
 
-		map.getPanes().overlayPane.appendChild(this._image);
+		map._panes.overlayPane.appendChild(this._image);
 
 		map.on('viewreset', this._reset, this);
 		this._reset();
@@ -41,14 +41,14 @@ L.ImageOverlay = L.Class.extend({
 	},
 
 	_reset: function () {
-		var topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
-			bottomRight = this._map.latLngToLayerPoint(this._bounds.getSouthEast()),
-			size = bottomRight.subtract(topLeft);
+		var image   = this._image,
+		    topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
+		    size    = this._map.latLngToLayerPoint(this._bounds.getSouthEast()).subtract(topLeft);
 
-		L.DomUtil.setPosition(this._image, topLeft);
+		L.DomUtil.setPosition(image, topLeft);
 
-		this._image.style.width = size.x + 'px';
-		this._image.style.height = size.y + 'px';
+		image.style.width  = size.x + 'px';
+		image.style.height = size.y + 'px';
 	},
 
 	_onImageLoad: function () {
diff --git a/src/layer/LayerGroup.js b/src/layer/LayerGroup.js
index 864541f..06c3623 100644
--- a/src/layer/LayerGroup.js
+++ b/src/layer/LayerGroup.js
@@ -6,8 +6,10 @@ L.LayerGroup = L.Class.extend({
 	initialize: function (layers) {
 		this._layers = {};
 
+		var i, len;
+
 		if (layers) {
-			for (var i = 0, len = layers.length; i < len; i++) {
+			for (i = 0, len = layers.length; i < len; i++) {
 				this.addLayer(layers[i]);
 			}
 		}
@@ -15,21 +17,25 @@ L.LayerGroup = L.Class.extend({
 
 	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) {
 		var id = L.Util.stamp(layer);
+
 		delete this._layers[id];
 
 		if (this._map) {
 			this._map.removeLayer(layer);
 		}
+
 		return this;
 	},
 
@@ -51,6 +57,7 @@ L.LayerGroup = L.Class.extend({
 				}
 			}
 		}
+
 		return this;
 	},
 
@@ -61,7 +68,7 @@ L.LayerGroup = L.Class.extend({
 
 	onRemove: function (map) {
 		this._iterateLayers(map.removeLayer, map);
-		delete this._map;
+		this._map = null;
 	},
 
 	_iterateLayers: function (method, context) {
diff --git a/src/layer/Popup.js b/src/layer/Popup.js
index 5b7e9fc..550b720 100644
--- a/src/layer/Popup.js
+++ b/src/layer/Popup.js
@@ -1,10 +1,15 @@
 
+L.Map.mergeOptions({
+	closePopupOnClick: true
+});
+
 L.Popup = L.Class.extend({
 	includes: L.Mixin.Events,
 
 	options: {
 		minWidth: 50,
 		maxWidth: 300,
+		maxHeight: null,
 		autoPan: true,
 		closeButton: true,
 		offset: new L.Point(0, 2),
@@ -20,79 +25,93 @@ L.Popup = L.Class.extend({
 
 	onAdd: function (map) {
 		this._map = map;
+
 		if (!this._container) {
 			this._initLayout();
 		}
 		this._updateContent();
 
 		this._container.style.opacity = '0';
+		map._panes.popupPane.appendChild(this._container);
+
+		map.on('viewreset', this._updatePosition, this);
 
-		this._map._panes.popupPane.appendChild(this._container);
-		this._map.on('viewreset', this._updatePosition, this);
+		if (L.Browser.any3d) {
+			map.on('zoomanim', this._zoomAnimation, this);
+		}
 
-		if (this._map.options.closePopupOnClick) {
-			this._map.on('preclick', this._close, this);
+		if (map.options.closePopupOnClick) {
+			map.on('preclick', this._close, this);
 		}
 
 		this._update();
 
 		this._container.style.opacity = '1'; //TODO fix ugly opacity hack
-
-		this._opened = true;
 	},
 
 	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);
+		map.off('viewreset', this._updatePosition, this)
+		   .off('preclick', this._close, this)
+		   .off('zoomanim', this._zoomAnimation, this);
 
 		this._container.style.opacity = '0';
 
-		this._opened = false;
+		this._map = null;
 	},
 
 	setLatLng: function (latlng) {
 		this._latlng = latlng;
-		if (this._opened) {
-			this._update();
-		}
+		this._update();
 		return this;
 	},
 
 	setContent: function (content) {
 		this._content = content;
-		if (this._opened) {
-			this._update();
-		}
+		this._update();
 		return this;
 	},
 
 	_close: function () {
-		if (this._opened) {
-			this._map.closePopup();
+		var map = this._map;
+
+		if (map) {
+			map._popup = null;
+
+			map
+				.removeLayer(this)
+				.fire('popupclose', {popup: this});
 		}
 	},
 
 	_initLayout: function () {
-		this._container = L.DomUtil.create('div', 'leaflet-popup ' + this.options.className);
+		var prefix = 'leaflet-popup',
+			container = this._container = L.DomUtil.create('div', prefix + ' ' + this.options.className + ' leaflet-zoom-animated'),
+			closeButton;
 
 		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);
+			closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container);
+			closeButton.href = '#close';
+
+			L.DomEvent.addListener(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);
+		var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container);
+		L.DomEvent.disableClickPropagation(wrapper);
+
+		this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
+		L.DomEvent.addListener(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation);
 
-		this._tipContainer = L.DomUtil.create('div', 'leaflet-popup-tip-container', this._container);
-		this._tip = L.DomUtil.create('div', 'leaflet-popup-tip', this._tipContainer);
+		this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
+		this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
 	},
 
 	_update: function () {
+		if (!this._map) { return; }
+
 		this._container.style.visibility = 'hidden';
 
 		this._updateContent();
@@ -105,60 +124,85 @@ L.Popup = L.Class.extend({
 	},
 
 	_updateContent: function () {
-		if (!this._content) {
-			return;
-		}
+		if (!this._content) { return; }
 
 		if (typeof this._content === 'string') {
 			this._contentNode.innerHTML = this._content;
 		} else {
-			this._contentNode.innerHTML = '';
+			while (this._contentNode.hasChildNodes()) {
+				this._contentNode.removeChild(this._contentNode.firstChild);
+			}
 			this._contentNode.appendChild(this._content);
 		}
+		this.fire('contentupdate');
 	},
 
 	_updateLayout: function () {
-		this._container.style.width = '';
-		this._container.style.whiteSpace = 'nowrap';
+		var container = this._contentNode;
+
+		container.style.width = '';
+		container.style.whiteSpace = 'nowrap';
+
+		var width = container.offsetWidth;
+		width = Math.min(width, this.options.maxWidth);
+		width = Math.max(width, this.options.minWidth);
 
-		var width = this._container.offsetWidth;
+		container.style.width = (width + 1) + 'px';
+		container.style.whiteSpace = '';
 
-		this._container.style.width = (width > this.options.maxWidth ?
-				this.options.maxWidth : (width < this.options.minWidth ? this.options.minWidth : width)) + 'px';
-		this._container.style.whiteSpace = '';
+		container.style.height = '';
+
+		var height = container.offsetHeight,
+			maxHeight = this.options.maxHeight,
+			scrolledClass = ' leaflet-popup-scrolled';
+
+		if (maxHeight && height > maxHeight) {
+			container.style.height = maxHeight + 'px';
+			container.className += scrolledClass;
+		} else {
+			container.className = container.className.replace(scrolledClass, '');
+		}
 
 		this._containerWidth = this._container.offsetWidth;
+		this._containerBottom = -this.options.offset.y;
+		this._containerLeft = -Math.round(this._containerWidth / 2) + this.options.offset.x;
 	},
 
 	_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._container.style.bottom = this._containerBottom + 'px';
 		this._container.style.left = this._containerLeft + 'px';
+
+		L.DomUtil.setPosition(this._container, pos);
+	},
+	
+	_zoomAnimation: function (opt) {
+		var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center)._round();
+
+		L.DomUtil.setPosition(this._container, pos);
 	},
 
 	_adjustPan: function () {
-		if (!this.options.autoPan) {
-			return;
-		}
+		if (!this.options.autoPan) { return; }
+
+		var map = this._map,
+			containerHeight = this._container.offsetHeight,
+			containerWidth = this._containerWidth,
+
+			layerPos = L.DomUtil.getPosition(this._container).add(
+				new L.Point(this._containerLeft, -containerHeight - this._containerBottom)),
 
-		var containerHeight = this._container.offsetHeight,
-			layerPos = new L.Point(
-				this._containerLeft,
-				-containerHeight - this._containerBottom),
-			containerPos = this._map.layerPointToContainerPoint(layerPos),
+			containerPos = map.layerPointToContainerPoint(layerPos),
 			adjustOffset = new L.Point(0, 0),
-			padding = this.options.autoPanPadding,
-			size = this._map.getSize();
+			padding      = this.options.autoPanPadding,
+			size         = map.getSize();
 
 		if (containerPos.x < 0) {
 			adjustOffset.x = containerPos.x - padding.x;
 		}
-		if (containerPos.x + this._containerWidth > size.x) {
-			adjustOffset.x = containerPos.x + this._containerWidth - size.x + padding.x;
+		if (containerPos.x + containerWidth > size.x) {
+			adjustOffset.x = containerPos.x + containerWidth - size.x + padding.x;
 		}
 		if (containerPos.y < 0) {
 			adjustOffset.y = containerPos.y - padding.y;
@@ -168,7 +212,7 @@ L.Popup = L.Class.extend({
 		}
 
 		if (adjustOffset.x || adjustOffset.y) {
-			this._map.panBy(adjustOffset);
+			map.panBy(adjustOffset);
 		}
 	},
 
diff --git a/src/layer/marker/DivIcon.js b/src/layer/marker/DivIcon.js
new file mode 100644
index 0000000..228eefb
--- /dev/null
+++ b/src/layer/marker/DivIcon.js
@@ -0,0 +1,20 @@
+L.DivIcon = L.Icon.extend({
+	options: {
+		iconSize: new L.Point(12, 12), // also can be set through CSS
+		/*
+		iconAnchor: (Point)
+		popupAnchor: (Point)
+		*/
+		className: 'leaflet-div-icon'
+	},
+
+	createIcon: function () {
+		var div = document.createElement('div');
+		this._setIconStyles(div, 'icon');
+		return div;
+	},
+
+	createShadow: function () {
+		return null;
+	}
+});
diff --git a/src/layer/marker/Icon.js b/src/layer/marker/Icon.js
index dc7502d..9473610 100644
--- a/src/layer/marker/Icon.js
+++ b/src/layer/marker/Icon.js
@@ -1,17 +1,18 @@
 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),
+	options: {
+		/*
+		iconUrl: (String) (required)
+		iconSize: (Point) (can be set through CSS)
+		iconAnchor: (Point) (centered by default if size is specified, can be set in CSS with negative margins)
+		popupAnchor: (Point) (if not specified, popup opens in the anchor point)
+		shadowUrl: (Point) (no shadow by default)
+		shadowSize: (Point)
+		*/
+		className: ''
+	},
 
-	initialize: function (iconUrl) {
-		if (iconUrl) {
-			this.iconUrl = iconUrl;
-		}
+	initialize: function (options) {
+		L.Util.setOptions(this, options);
 	},
 
 	createIcon: function () {
@@ -23,35 +24,45 @@ L.Icon = L.Class.extend({
 	},
 
 	_createIcon: function (name) {
-		var size = this[name + 'Size'],
-			src = this[name + 'Url'];
-		if (!src && name === 'shadow') {
-			return null;
-		}
+		var src = this._getIconUrl(name);
+
+		if (!src) { return null; }
+		
+		var img = this._createImg(src);
+		this._setIconStyles(img, name);
+
+		return img;
+	},
+
+	_setIconStyles: function (img, name) {
+		var options = this.options,
+			size = options[name + 'Size'],
+			anchor = options.iconAnchor;
 
-		var img;
-		if (!src) {
-			img = this._createDiv();
+		if (!anchor && size) {
+			anchor = size.divideBy(2, true);
 		}
-		else {
-			img = this._createImg(src);
+
+		if (name === 'shadow' && anchor && options.shadowOffset) {
+			anchor._add(options.shadowOffset);
 		}
 
-		img.className = 'leaflet-marker-' + name;
+		img.className = 'leaflet-marker-' + name + ' ' + options.className + ' leaflet-zoom-animated';
 
-		img.style.marginLeft = (-this.iconAnchor.x) + 'px';
-		img.style.marginTop = (-this.iconAnchor.y) + 'px';
+		if (anchor) {
+			img.style.marginLeft = (-anchor.x) + 'px';
+			img.style.marginTop  = (-anchor.y) + 'px';
+		}
 
 		if (size) {
-			img.style.width = size.x + 'px';
+			img.style.width  = size.x + 'px';
 			img.style.height = size.y + 'px';
 		}
-
-		return img;
 	},
 
 	_createImg: function (src) {
 		var el;
+
 		if (!L.Browser.ie6) {
 			el = document.createElement('img');
 			el.src = src;
@@ -62,7 +73,45 @@ L.Icon = L.Class.extend({
 		return el;
 	},
 
-	_createDiv: function () {
-		return document.createElement('div');
+	_getIconUrl: function (name) {
+		return this.options[name + 'Url'];
 	}
 });
+
+
+// TODO move to a separate file
+
+L.Icon.Default = L.Icon.extend({
+	options: {
+		iconSize: new L.Point(25, 41),
+		iconAnchor: new L.Point(13, 41),
+		popupAnchor: new L.Point(0, -33),
+
+		shadowSize: new L.Point(41, 41)
+	},
+
+	_getIconUrl: function (name) {
+		var path = L.Icon.Default.imagePath;
+		if (!path) {
+			throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually.");
+		}
+
+		return path + '/marker-' + name + '.png';
+	}
+});
+
+L.Icon.Default.imagePath = (function () {
+	var scripts = document.getElementsByTagName('script'),
+	    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) {
+			return src.split(leafletRe)[0] + '/images';
+		}
+	}
+}());
\ No newline at end of file
diff --git a/src/layer/marker/Marker.Drag.js b/src/layer/marker/Marker.Drag.js
index 17df68c..73c29cf 100644
--- a/src/layer/marker/Marker.Drag.js
+++ b/src/layer/marker/Marker.Drag.js
@@ -10,9 +10,7 @@ L.Handler.MarkerDrag = L.Handler.extend({
 	addHooks: function () {
 		var icon = this._marker._icon;
 		if (!this._draggable) {
-			this._draggable = new L.Draggable(icon, icon);
-
-			this._draggable
+			this._draggable = new L.Draggable(icon, icon)
 				.on('dragstart', this._onDragStart, this)
 				.on('drag', this._onDrag, this)
 				.on('dragend', this._onDragEnd, this);
diff --git a/src/layer/marker/Marker.Popup.js b/src/layer/marker/Marker.Popup.js
index 3d4768d..554310c 100644
--- a/src/layer/marker/Marker.Popup.js
+++ b/src/layer/marker/Marker.Popup.js
@@ -4,8 +4,8 @@
 
 L.Marker.include({
 	openPopup: function () {
-		this._popup.setLatLng(this._latlng);
-		if (this._map) {
+		if (this._popup && this._map) {
+			this._popup.setLatLng(this._latlng);
 			this._map.openPopup(this._popup);
 		}
 
@@ -20,14 +20,20 @@ L.Marker.include({
 	},
 
 	bindPopup: function (content, options) {
-		options = L.Util.extend({offset: this.options.icon.popupAnchor}, options);
+		var anchor = this.options.icon.options.popupAnchor || new L.Point(0, 0);
+
+		if (options && options.offset) {
+			anchor = anchor.add(options.offset);
+		}
+
+		options = L.Util.extend({offset: anchor}, options);
 
 		if (!this._popup) {
 			this.on('click', this.openPopup, this);
 		}
 
-		this._popup = new L.Popup(options, this);
-		this._popup.setContent(content);
+		this._popup = new L.Popup(options, this)
+			.setContent(content);
 
 		return this;
 	},
diff --git a/src/layer/marker/Marker.js b/src/layer/marker/Marker.js
index 795cb81..c6b3b58 100644
--- a/src/layer/marker/Marker.js
+++ b/src/layer/marker/Marker.js
@@ -7,11 +7,12 @@ L.Marker = L.Class.extend({
 	includes: L.Mixin.Events,
 
 	options: {
-		icon: new L.Icon(),
+		icon: new L.Icon.Default(),
 		title: '',
 		clickable: true,
 		draggable: false,
-		zIndexOffset: 0
+		zIndexOffset: 0,
+		opacity: 1
 	},
 
 	initialize: function (latlng, options) {
@@ -22,9 +23,13 @@ L.Marker = L.Class.extend({
 	onAdd: function (map) {
 		this._map = map;
 
-		this._initIcon();
-
 		map.on('viewreset', this._reset, this);
+
+		if (map.options.zoomAnimation && map.options.animateMarkerZoom) {
+			map.on('zoomanim', this._zoomAnimation, this);
+		}
+
+		this._initIcon();
 		this._reset();
 	},
 
@@ -36,9 +41,10 @@ L.Marker = L.Class.extend({
 			this.closePopup();
 		}
 
-		this._map = null;
+		map.off('viewreset', this._reset, this)
+		   .off('zoomanim', this._zoomAnimation, this);
 
-		map.off('viewreset', this._reset, this);
+		this._map = null;
 	},
 
 	getLatLng: function () {
@@ -47,20 +53,17 @@ L.Marker = L.Class.extend({
 
 	setLatLng: function (latlng) {
 		this._latlng = latlng;
-		if (this._icon) {
-			this._reset();
 
-			if (this._popup) {
-				this._popup.setLatLng(this._latlng);
-			}
+		this._reset();
+
+		if (this._popup) {
+			this._popup.setLatLng(latlng);
 		}
 	},
 
 	setZIndexOffset: function (offset) {
 		this.options.zIndexOffset = offset;
-		if (this._icon) {
-			this._reset();
-		}
+		this._reset();
 	},
 
 	setIcon: function (icon) {
@@ -77,37 +80,53 @@ L.Marker = L.Class.extend({
 	},
 
 	_initIcon: function () {
+		var options = this.options;
+
 		if (!this._icon) {
-			this._icon = this.options.icon.createIcon();
+			this._icon = options.icon.createIcon();
 
-			if (this.options.title) {
-				this._icon.title = this.options.title;
+			if (options.title) {
+				this._icon.title = options.title;
 			}
 
 			this._initInteraction();
+			this._updateOpacity();
 		}
 		if (!this._shadow) {
-			this._shadow = this.options.icon.createShadow();
+			this._shadow = options.icon.createShadow();
 		}
 
-		this._map._panes.markerPane.appendChild(this._icon);
+		var panes = this._map._panes;
+
+		panes.markerPane.appendChild(this._icon);
+
 		if (this._shadow) {
-			this._map._panes.shadowPane.appendChild(this._shadow);
+			panes.shadowPane.appendChild(this._shadow);
 		}
 	},
 
 	_removeIcon: function () {
-		this._map._panes.markerPane.removeChild(this._icon);
+		var panes = this._map._panes;
+
+		panes.markerPane.removeChild(this._icon);
+
 		if (this._shadow) {
-			this._map._panes.shadowPane.removeChild(this._shadow);
+			panes.shadowPane.removeChild(this._shadow);
 		}
+
 		this._icon = this._shadow = null;
 	},
 
 	_reset: function () {
+		if (!this._icon) { return; }
+
 		var pos = this._map.latLngToLayerPoint(this._latlng).round();
+		this._setPos(pos);
+	},
 
+	_setPos: function (pos) {
 		L.DomUtil.setPosition(this._icon, pos);
+
 		if (this._shadow) {
 			L.DomUtil.setPosition(this._shadow, pos);
 		}
@@ -115,16 +134,25 @@ L.Marker = L.Class.extend({
 		this._icon.style.zIndex = pos.y + this.options.zIndexOffset;
 	},
 
+	_zoomAnimation: function (opt) {
+		var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
+
+		this._setPos(pos);
+	},
+
 	_initInteraction: function () {
-		if (this.options.clickable) {
-			this._icon.className += ' leaflet-clickable';
+		if (!this.options.clickable) {
+			return;
+		}
 
-			L.DomEvent.addListener(this._icon, 'click', this._onMouseClick, this);
+		var icon = this._icon,
+			events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
 
-			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
-			for (var i = 0; i < events.length; i++) {
-				L.DomEvent.addListener(this._icon, events[i], this._fireMouseEvent, this);
-			}
+		icon.className += ' leaflet-clickable';
+		L.DomEvent.addListener(icon, 'click', this._onMouseClick, this);
+
+		for (var i = 0; i < events.length; i++) {
+			L.DomEvent.addListener(icon, events[i], this._fireMouseEvent, this);
 		}
 
 		if (L.Handler.MarkerDrag) {
@@ -139,11 +167,29 @@ L.Marker = L.Class.extend({
 	_onMouseClick: function (e) {
 		L.DomEvent.stopPropagation(e);
 		if (this.dragging && this.dragging.moved()) { return; }
-		this.fire(e.type);
+		if (this._map.dragging && this._map.dragging.moved()) { return; }
+		this.fire(e.type, {
+			originalEvent: e
+		});
 	},
 
 	_fireMouseEvent: function (e) {
-		this.fire(e.type);
-		L.DomEvent.stopPropagation(e);
+		this.fire(e.type, {
+			originalEvent: e
+		});
+		if (e.type !== 'mousedown') {
+			L.DomEvent.stopPropagation(e);
+		}
+	},
+
+	setOpacity: function (opacity) {
+		this.options.opacity = opacity;
+		if (this._map) {
+			this._updateOpacity();
+		}
+	},
+
+	_updateOpacity: function (opacity) {
+		L.DomUtil.setOpacity(this._icon, this.options.opacity);
 	}
 });
diff --git a/src/layer/tile/TileLayer.Canvas.js b/src/layer/tile/TileLayer.Canvas.js
index d1cc4c3..559d8ef 100644
--- a/src/layer/tile/TileLayer.Canvas.js
+++ b/src/layer/tile/TileLayer.Canvas.js
@@ -8,9 +8,13 @@ L.TileLayer.Canvas = L.TileLayer.extend({
 	},
 
 	redraw: function () {
-		for (var i in this._tiles) {
-			var tile = this._tiles[i];
-			this._redrawTile(tile);
+		var i,
+			tiles = this._tiles;
+
+		for (i in tiles) {
+			if (tiles.hasOwnProperty(i)) {
+				this._redrawTile(tiles[i]);
+			}
 		}
 	},
 
@@ -19,11 +23,11 @@ L.TileLayer.Canvas = L.TileLayer.extend({
 	},
 
 	_createTileProto: function () {
-		this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');
+		var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');
 
 		var tileSize = this.options.tileSize;
-		this._canvasProto.width = tileSize;
-		this._canvasProto.height = tileSize;
+		proto.width = tileSize;
+		proto.height = tileSize;
 	},
 
 	_createTile: function () {
diff --git a/src/layer/tile/TileLayer.WMS.js b/src/layer/tile/TileLayer.WMS.js
index 17d0103..737bcd7 100644
--- a/src/layer/tile/TileLayer.WMS.js
+++ b/src/layer/tile/TileLayer.WMS.js
@@ -9,37 +9,46 @@ L.TileLayer.WMS = L.TileLayer.extend({
 		transparent: false
 	},
 
-	initialize: function (/*String*/ url, /*Object*/ options) {
+	initialize: function (url, options) { // (String, Object)
 		this._url = url;
 
-		this.wmsParams = L.Util.extend({}, this.defaultWmsParams);
-		this.wmsParams.width = this.wmsParams.height = this.options.tileSize;
+		var wmsParams = L.Util.extend({}, this.defaultWmsParams);
+		wmsParams.width = 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)) {
-				this.wmsParams[i] = options[i];
+				wmsParams[i] = options[i];
 			}
 		}
 
+		this.wmsParams = wmsParams;
+
 		L.Util.setOptions(this, options);
 	},
 
-	onAdd: function (map) {
-		var projectionKey = (parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs');
+	onAdd: function (map, insertAtTheBottom) {
+		var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs';
 		this.wmsParams[projectionKey] = map.options.crs.code;
 
-		L.TileLayer.prototype.onAdd.call(this, map);
+		L.TileLayer.prototype.onAdd.call(this, map, insertAtTheBottom);
 	},
 
-	getTileUrl: function (/*Point*/ tilePoint, /*Number*/ zoom)/*-> String*/ {
-		var tileSize = this.options.tileSize,
+	getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String
+		var map = this._map,
+			crs = map.options.crs,
+
+			tileSize = this.options.tileSize,
+
 			nwPoint = tilePoint.multiplyBy(tileSize),
 			sePoint = nwPoint.add(new L.Point(tileSize, tileSize)),
-			nwMap = this._map.unproject(nwPoint, this._zoom, true),
-			seMap = this._map.unproject(sePoint, this._zoom, true),
-			nw = this._map.options.crs.project(nwMap),
-			se = this._map.options.crs.project(seMap),
+
+			nwMap = map.unproject(nwPoint, zoom, true),
+			seMap = map.unproject(sePoint, zoom, true),
+
+			nw = crs.project(nwMap),
+			se = crs.project(seMap),
+
 			bbox = [nw.x, se.y, se.x, nw.y].join(',');
 
 		return this._url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox;
diff --git a/src/layer/tile/TileLayer.js b/src/layer/tile/TileLayer.js
index e5f228b..230d9c2 100644
--- a/src/layer/tile/TileLayer.js
+++ b/src/layer/tile/TileLayer.js
@@ -18,20 +18,34 @@ L.TileLayer = L.Class.extend({
 		noWrap: false,
 		zoomOffset: 0,
 		zoomReverse: false,
+		detectRetina: false,
 
 		unloadInvisibleTiles: L.Browser.mobile,
 		updateWhenIdle: L.Browser.mobile,
 		reuseTiles: false
 	},
 
-	initialize: function (url, options, urlParams) {
-		L.Util.setOptions(this, options);
+	initialize: function (url, options) {
+		options = L.Util.setOptions(this, options);
+
+		// detecting retina displays, adjusting tileSize and zoom levels
+		if (options.detectRetina && window.devicePixelRatio > 1 && options.maxZoom > 0) {
+
+			options.tileSize = Math.floor(options.tileSize / 2);
+			options.zoomOffset++;
+
+			if (options.minZoom > 0) {
+				options.minZoom--;
+			}
+			this.options.maxZoom--;
+		}
 
 		this._url = url;
-		this._urlParams = urlParams;
 
-		if (typeof this.options.subdomains === 'string') {
-			this.options.subdomains = this.options.subdomains.split('');
+		var subdomains = this.options.subdomains;
+
+		if (typeof subdomains === 'string') {
+			this.options.subdomains = subdomains.split('');
 		}
 	},
 
@@ -47,10 +61,9 @@ L.TileLayer = L.Class.extend({
 
 		// set up events
 		map.on('viewreset', this._resetCallback, this);
+		map.on('moveend', this._update, this);
 
-		if (this.options.updateWhenIdle) {
-			map.on('moveend', this._update, this);
-		} else {
+		if (!this.options.updateWhenIdle) {
 			this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
 			map.on('move', this._limitedUpdate, this);
 		}
@@ -60,16 +73,17 @@ L.TileLayer = L.Class.extend({
 	},
 
 	onRemove: function (map) {
-		this._map.getPanes().tilePane.removeChild(this._container);
-		this._container = null;
+		map._panes.tilePane.removeChild(this._container);
 
-		this._map.off('viewreset', this._resetCallback, this);
+		map.off('viewreset', this._resetCallback, this);
+		map.off('moveend', this._update, this);
 
-		if (this.options.updateWhenIdle) {
-			this._map.off('moveend', this._update, this);
-		} else {
-			this._map.off('move', this._limitedUpdate, this);
+		if (!this.options.updateWhenIdle) {
+			map.off('move', this._limitedUpdate, this);
 		}
+
+		this._container = null;
+		this._map = null;
 	},
 
 	getAttribution: function () {
@@ -79,26 +93,29 @@ L.TileLayer = L.Class.extend({
 	setOpacity: function (opacity) {
 		this.options.opacity = opacity;
 
-		this._setOpacity(opacity);
+		if (this._map) {
+			this._updateOpacity();
+		}
 
 		// stupid webkit hack to force redrawing of tiles
+		var i,
+			tiles = this._tiles;
+
 		if (L.Browser.webkit) {
-			for (var i in this._tiles) {
-				if (this._tiles.hasOwnProperty(i)) {
-					this._tiles[i].style.webkitTransform += ' translate(0,0)';
+			for (i in tiles) {
+				if (tiles.hasOwnProperty(i)) {
+					tiles[i].style.webkitTransform += ' translate(0,0)';
 				}
 			}
 		}
 	},
 
-	_setOpacity: function (opacity) {
-		if (opacity < 1) {
-			L.DomUtil.setOpacity(this._container, opacity);
-		}
+	_updateOpacity: function () {
+		L.DomUtil.setOpacity(this._container, this.options.opacity);
 	},
 
 	_initContainer: function () {
-		var tilePane = this._map.getPanes().tilePane,
+		var tilePane = this._map._panes.tilePane,
 			first = tilePane.firstChild;
 
 		if (!this._container || tilePane.empty) {
@@ -110,7 +127,9 @@ L.TileLayer = L.Class.extend({
 				tilePane.appendChild(this._container);
 			}
 
-			this._setOpacity(this.options.opacity);
+			if (this.options.opacity < 1) {
+				this._updateOpacity();
+			}
 		}
 	},
 
@@ -119,12 +138,15 @@ L.TileLayer = L.Class.extend({
 	},
 
 	_reset: function (clearOldContainer) {
-		var key;
-		for (key in this._tiles) {
-			if (this._tiles.hasOwnProperty(key)) {
-				this.fire("tileunload", {tile: this._tiles[key]});
+		var key,
+			tiles = this._tiles;
+
+		for (key in tiles) {
+			if (tiles.hasOwnProperty(key)) {
+				this.fire('tileunload', {tile: tiles[key]});
 			}
 		}
+
 		this._tiles = {};
 
 		if (this.options.reuseTiles) {
@@ -134,13 +156,16 @@ L.TileLayer = L.Class.extend({
 		if (clearOldContainer && this._container) {
 			this._container.innerHTML = "";
 		}
+
 		this._initContainer();
 	},
 
-	_update: function () {
-		var bounds = this._map.getPixelBounds(),
-			zoom = this._map.getZoom(),
-			tileSize = this.options.tileSize;
+	_update: function (e) {
+		if (this._map._panTransition && this._map._panTransition._inProgress) { return; }
+
+		var bounds   = this._map.getPixelBounds(),
+		    zoom     = this._map.getZoom(),
+		    tileSize = this.options.tileSize;
 
 		if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
 			return;
@@ -165,12 +190,12 @@ L.TileLayer = L.Class.extend({
 		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;
+		var j, i;
+		for (j = bounds.min.y; j <= bounds.max.y; j++) {
+			for (i = bounds.min.x; i <= bounds.max.x; i++) {
+				if (!((i + ':' + j) in this._tiles)) {
+					queue.push(new L.Point(i, j));
 				}
-				queue.push(new L.Point(i, j));
 			}
 		}
 
@@ -182,7 +207,9 @@ L.TileLayer = L.Class.extend({
 		var fragment = document.createDocumentFragment();
 
 		this._tilesToLoad = queue.length;
-		for (var k = 0, len = this._tilesToLoad; k < len; k++) {
+
+		var k, len;
+		for (k = 0, len = this._tilesToLoad; k < len; k++) {
 			this._addTile(queue[k], fragment);
 		}
 
@@ -200,40 +227,45 @@ L.TileLayer = L.Class.extend({
 
 				// 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._removeTile(key);
+				}
+			}
+		}
+	},
 
-					tile = this._tiles[key];
-					this.fire("tileunload", {tile: tile, url: tile.src});
+	_removeTile: function (key) {
+		var tile = this._tiles[key];
 
-					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=';
+		this.fire("tileunload", {tile: tile, url: tile.src});
 
-					delete this._tiles[key];
-				}
-			}
+		if (tile.parentNode === this._container) {
+			this._container.removeChild(tile);
 		}
+		if (this.options.reuseTiles) {
+			this._unusedTiles.push(tile);
+		}
+
+		tile.src = L.Util.emptyImageUrl;
+
+		delete this._tiles[key];
 	},
 
 	_addTile: function (tilePoint, container) {
 		var tilePos = this._getTilePos(tilePoint),
 			zoom = this._map.getZoom(),
-			key = tilePoint.x + ':' + tilePoint.y,
-			tileLimit = Math.pow(2, this._getOffsetZoom(zoom));
+		    key = tilePoint.x + ':' + tilePoint.y,
+		    limit = Math.pow(2, this._getOffsetZoom(zoom));
 
 		// wrap tile coordinates
 		if (!this.options.continuousWorld) {
 			if (!this.options.noWrap) {
-				tilePoint.x = ((tilePoint.x % tileLimit) + tileLimit) % tileLimit;
-			} else if (tilePoint.x < 0 || tilePoint.x >= tileLimit) {
+				tilePoint.x = ((tilePoint.x % limit) + limit) % limit;
+			} else if (tilePoint.x < 0 || tilePoint.x >= limit) {
 				this._tilesToLoad--;
 				return;
 			}
 
-			if (tilePoint.y < 0 || tilePoint.y >= tileLimit) {
+			if (tilePoint.y < 0 || tilePoint.y >= limit) {
 				this._tilesToLoad--;
 				return;
 			}
@@ -246,7 +278,7 @@ L.TileLayer = L.Class.extend({
 		this._tiles[key] = tile;
 
 		if (this.options.scheme === 'tms') {
-			tilePoint.y = tileLimit - tilePoint.y - 1;
+			tilePoint.y = limit - tilePoint.y - 1;
 		}
 
 		this._loadTile(tile, tilePoint, zoom);
@@ -255,8 +287,9 @@ L.TileLayer = L.Class.extend({
 	},
 
 	_getOffsetZoom: function (zoom) {
-		zoom = this.options.zoomReverse ? this.options.maxZoom - zoom : zoom;
-		return zoom + this.options.zoomOffset;
+		var options = this.options;
+		zoom = options.zoomReverse ? options.maxZoom - zoom : zoom;
+		return zoom + options.zoomOffset;
 	},
 
 	_getTilePos: function (tilePoint) {
@@ -270,23 +303,24 @@ L.TileLayer = L.Class.extend({
 
 	getTileUrl: function (tilePoint, zoom) {
 		var subdomains = this.options.subdomains,
-			s = this.options.subdomains[(tilePoint.x + tilePoint.y) % subdomains.length];
+			index = (tilePoint.x + tilePoint.y) % subdomains.length,
+			s = this.options.subdomains[index];
 
 		return L.Util.template(this._url, L.Util.extend({
 			s: s,
 			z: this._getOffsetZoom(zoom),
 			x: tilePoint.x,
 			y: tilePoint.y
-		}, this._urlParams));
+		}, this.options));
 	},
 
 	_createTileProto: function () {
-		this._tileImg = L.DomUtil.create('img', 'leaflet-tile');
-		this._tileImg.galleryimg = 'no';
+		var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile');
+		img.galleryimg = 'no';
 
 		var tileSize = this.options.tileSize;
-		this._tileImg.style.width = tileSize + 'px';
-		this._tileImg.style.height = tileSize + 'px';
+		img.style.width = tileSize + 'px';
+		img.style.height = tileSize + 'px';
 	},
 
 	_getTile: function () {
@@ -309,33 +343,46 @@ L.TileLayer = L.Class.extend({
 	},
 
 	_loadTile: function (tile, tilePoint, zoom) {
-		tile._layer = this;
-		tile.onload = this._tileOnLoad;
+		tile._layer  = this;
+		tile.onload  = this._tileOnLoad;
 		tile.onerror = this._tileOnError;
-		tile.src = this.getTileUrl(tilePoint, zoom);
+
+		tile.src     = this.getTileUrl(tilePoint, zoom);
 	},
 
+    _tileLoaded: function () {
+        this._tilesToLoad--;
+        if (!this._tilesToLoad) {
+            this.fire('load');
+        }
+    },
+
 	_tileOnLoad: function (e) {
 		var layer = this._layer;
 
 		this.className += ' leaflet-tile-loaded';
 
-		layer.fire('tileload', {tile: this, url: this.src});
+		layer.fire('tileload', {
+			tile: this,
+			url: this.src
+		});
 
-		layer._tilesToLoad--;
-		if (!layer._tilesToLoad) {
-			layer.fire('load');
-		}
+        layer._tileLoaded();
 	},
 
 	_tileOnError: function (e) {
 		var layer = this._layer;
 
-		layer.fire('tileerror', {tile: this, url: this.src});
+		layer.fire('tileerror', {
+			tile: this,
+			url: this.src
+		});
 
 		var newUrl = layer.options.errorTileUrl;
 		if (newUrl) {
 			this.src = newUrl;
 		}
-	}
+
+        layer._tileLoaded();
+    }
 });
diff --git a/src/layer/vector/Circle.js b/src/layer/vector/Circle.js
index b48e1f1..99989e1 100644
--- a/src/layer/vector/Circle.js
+++ b/src/layer/vector/Circle.js
@@ -16,26 +16,38 @@ L.Circle = L.Path.extend({
 
 	setLatLng: function (latlng) {
 		this._latlng = latlng;
-		this._redraw();
-		return this;
+		return this.redraw();
 	},
 
 	setRadius: function (radius) {
 		this._mRadius = radius;
-		this._redraw();
-		return this;
+		return this.redraw();
 	},
 
 	projectLatlngs: function () {
-		var equatorLength = 40075017,
-			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),
+		var lngRadius = this._getLngRadius(),
+			latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngRadius, true),
 			point2 = this._map.latLngToLayerPoint(latlng2);
 
 		this._point = this._map.latLngToLayerPoint(this._latlng);
-		this._radius = Math.round(this._point.x - point2.x);
+		this._radius = Math.max(Math.round(this._point.x - point2.x), 1);
+	},
+
+	getBounds: function () {
+		var map = this._map,
+			delta = this._radius * Math.cos(Math.PI / 4),
+			point = map.project(this._latlng),
+			swPoint = new L.Point(point.x - delta, point.y + delta),
+			nePoint = new L.Point(point.x + delta, point.y - delta),
+			zoom = map.getZoom(),
+			sw = map.unproject(swPoint, zoom, true),
+			ne = map.unproject(nePoint, zoom, true);
+
+		return new L.LatLngBounds(sw, ne);
+	},
+	
+	getLatLng: function () {
+		return this._latlng;
 	},
 
 	getPathString: function () {
@@ -56,8 +68,22 @@ L.Circle = L.Path.extend({
 			return "AL " + p.x + "," + p.y + " " + r + "," + r + " 0," + (65535 * 360);
 		}
 	},
+	
+	getRadius: function () {
+		return this._mRadius;
+	},
+
+	_getLngRadius: function () {
+		var equatorLength = 40075017,
+			hLength = equatorLength * Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
+
+		return (this._mRadius / hLength) * 360;
+	},
 
 	_checkIfEmpty: function () {
+		if (!this._map) {
+			return false;
+		}
 		var vp = this._map._pathViewport,
 			r = this._radius,
 			p = this._point;
diff --git a/src/layer/vector/CircleMarker.js b/src/layer/vector/CircleMarker.js
index 0b60d3d..8163df1 100644
--- a/src/layer/vector/CircleMarker.js
+++ b/src/layer/vector/CircleMarker.js
@@ -19,7 +19,6 @@ L.CircleMarker = L.Circle.extend({
 
 	setRadius: function (radius) {
 		this._radius = radius;
-		this._redraw();
-		return this;
+		return this.redraw();
 	}
 });
diff --git a/src/layer/vector/MultiPoly.js b/src/layer/vector/MultiPoly.js
index 0109c30..49d3954 100644
--- a/src/layer/vector/MultiPoly.js
+++ b/src/layer/vector/MultiPoly.js
@@ -25,6 +25,8 @@
 				while (i < len) {
 					this.addLayer(new Klass(latlngs[i++], this._options));
 				}
+
+				return this;
 			}
 		});
 	}
diff --git a/src/layer/vector/Path.SVG.js b/src/layer/vector/Path.SVG.js
index 329e2e9..f9d0988 100644
--- a/src/layer/vector/Path.SVG.js
+++ b/src/layer/vector/Path.SVG.js
@@ -4,16 +4,17 @@ L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.S
 
 L.Path = L.Path.extend({
 	statics: {
-		SVG: L.Browser.svg,
-		_createElement: function (name) {
-			return document.createElementNS(L.Path.SVG_NS, name);
-		}
+		SVG: L.Browser.svg
 	},
 
 	getPathString: function () {
 		// form path string here
 	},
 
+	_createElement: function (name) {
+		return document.createElementNS(L.Path.SVG_NS, name);
+	},
+
 	_initElements: function () {
 		this._map._initPathRoot();
 		this._initPath();
@@ -21,9 +22,9 @@ L.Path = L.Path.extend({
 	},
 
 	_initPath: function () {
-		this._container = L.Path._createElement('g');
+		this._container = this._createElement('g');
 
-		this._path = L.Path._createElement('path');
+		this._path = this._createElement('path');
 		this._container.appendChild(this._path);
 
 		this._map._pathRoot.appendChild(this._container);
@@ -36,8 +37,6 @@ L.Path = L.Path.extend({
 		}
 		if (this.options.fill) {
 			this._path.setAttribute('fill-rule', 'evenodd');
-		} else {
-			this._path.setAttribute('fill', 'none');
 		}
 		this._updateStyle();
 	},
@@ -47,10 +46,14 @@ L.Path = L.Path.extend({
 			this._path.setAttribute('stroke', this.options.color);
 			this._path.setAttribute('stroke-opacity', this.options.opacity);
 			this._path.setAttribute('stroke-width', this.options.weight);
+		} else {
+			this._path.setAttribute('stroke', 'none');
 		}
 		if (this.options.fill) {
 			this._path.setAttribute('fill', this.options.fillColor || this.options.color);
 			this._path.setAttribute('fill-opacity', this.options.fillOpacity);
+		} else {
+			this._path.setAttribute('fill', 'none');
 		}
 	},
 
@@ -66,13 +69,13 @@ L.Path = L.Path.extend({
 	// TODO remove duplication with L.Map
 	_initEvents: function () {
 		if (this.options.clickable) {
-			if (!L.Browser.vml) {
+			if (L.Browser.svg || !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'];
+			var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'mousemove', 'contextmenu'];
 			for (var i = 0; i < events.length; i++) {
 				L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this);
 			}
@@ -83,6 +86,11 @@ L.Path = L.Path.extend({
 		if (this._map.dragging && this._map.dragging.moved()) {
 			return;
 		}
+
+		if (e.type === 'contextmenu') {
+			L.DomEvent.preventDefault(e);
+		}
+
 		this._fireMouseEvent(e);
 	},
 
@@ -90,10 +98,18 @@ L.Path = L.Path.extend({
 		if (!this.hasEventListeners(e.type)) {
 			return;
 		}
+		var map = this._map,
+			containerPoint = map.mouseEventToContainerPoint(e),
+			layerPoint = map.containerPointToLayerPoint(containerPoint),
+			latlng = map.layerPointToLatLng(layerPoint);
+
 		this.fire(e.type, {
-			latlng: this._map.mouseEventToLatLng(e),
-			layerPoint: this._map.mouseEventToLayerPoint(e)
+			latlng: latlng,
+			layerPoint: layerPoint,
+			containerPoint: containerPoint,
+			originalEvent: e
 		});
+
 		L.DomEvent.stopPropagation(e);
 	}
 });
@@ -101,15 +117,45 @@ L.Path = L.Path.extend({
 L.Map.include({
 	_initPathRoot: function () {
 		if (!this._pathRoot) {
-			this._pathRoot = L.Path._createElement('svg');
+			this._pathRoot = L.Path.prototype._createElement('svg');
 			this._panes.overlayPane.appendChild(this._pathRoot);
 
+			if (this.options.zoomAnimation) {
+				this._pathRoot.setAttribute('class', ' leaflet-zoom-animated');
+				this.on('zoomanim', this._animatePathZoom);
+				this.on('zoomend', this._endPathZoom);
+			}
+
 			this.on('moveend', this._updateSvgViewport);
 			this._updateSvgViewport();
 		}
 	},
 
+	_animatePathZoom: function (opt) {
+		var centerOffset = this._getNewTopLeftPoint(opt.center).subtract(this._getTopLeftPoint()),
+			scale = Math.pow(2, opt.zoom - this._zoom),
+			offset = centerOffset.divideBy(1 - 1 / scale),
+			centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(-2)),
+			origin = centerPoint.add(offset).round(),
+			pathRootStyle = this._pathRoot.style;
+
+		pathRootStyle[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString((origin.multiplyBy(-1).add(L.DomUtil.getPosition(this._pathRoot)).multiplyBy(scale).add(origin))) + ' scale(' + scale + ') ';
+
+		this._pathZooming = true;
+	},
+
+	_endPathZoom: function () {
+		this._pathZooming = false;
+	},
+
 	_updateSvgViewport: function () {
+		if (this._pathZooming) {
+			//Do not update SVGs while a zoom animation is going on otherwise the animation will break.
+			//When the zoom animation ends we will be updated again anyway
+			//This fixes the case where you do a momentum move and zoom while the move is still ongoing.
+			return;
+		}
+
 		this._updatePathViewport();
 
 		var vp = this._pathViewport,
@@ -122,7 +168,7 @@ L.Map.include({
 
 		// 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) {
+		if (L.Browser.mobileWebkit) {
 			pane.removeChild(root);
 		}
 
@@ -131,7 +177,7 @@ L.Map.include({
 		root.setAttribute('height', height);
 		root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
 
-		if (L.Browser.webkit) {
+		if (L.Browser.mobileWebkit) {
 			pane.appendChild(root);
 		}
 	}
diff --git a/src/layer/vector/Path.VML.js b/src/layer/vector/Path.VML.js
index 631a3dd..14d03f0 100644
--- a/src/layer/vector/Path.VML.js
+++ b/src/layer/vector/Path.VML.js
@@ -4,90 +4,104 @@
  */
 
 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)';
+	var div = document.createElement('div');
+	div.innerHTML = '<v:shape adj="1"/>';
 
-	return (s && (typeof s.adj === 'object'));
+	var shape = div.firstChild;
+	shape.style.behavior = 'url(#default#VML)';
+
+	return shape && (typeof shape.adj === 'object');
 }());
 
 L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({
 	statics: {
 		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">');
-				};
-			}
-		}())
+		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">');
+			};
+		}
+	}()),
+
 	_initPath: function () {
-		this._container = L.Path._createElement('shape');
-		this._container.className += ' leaflet-vml-shape' +
+		var container = this._container = this._createElement('shape');
+		container.className += ' leaflet-vml-shape' +
 				(this.options.clickable ? ' leaflet-clickable' : '');
-		this._container.coordsize = '1 1';
+		container.coordsize = '1 1';
 
-		this._path = L.Path._createElement('path');
-		this._container.appendChild(this._path);
+		this._path = this._createElement('path');
+		container.appendChild(this._path);
 
-		this._map._pathRoot.appendChild(this._container);
+		this._map._pathRoot.appendChild(container);
 	},
 
 	_initStyle: function () {
+		var container = this._container,
+			stroke,
+			fill;
+
 		if (this.options.stroke) {
-			this._stroke = L.Path._createElement('stroke');
-			this._stroke.endcap = 'round';
-			this._container.appendChild(this._stroke);
-		} else {
-			this._container.stroked = false;
+			stroke = this._stroke = this._createElement('stroke');
+			stroke.endcap = 'round';
+			container.appendChild(stroke);
 		}
+
 		if (this.options.fill) {
-			this._container.filled = true;
-			this._fill = L.Path._createElement('fill');
-			this._container.appendChild(this._fill);
-		} else {
-			this._container.filled = false;
+			fill = this._fill = this._createElement('fill');
+			container.appendChild(fill);
 		}
+
 		this._updateStyle();
 	},
 
 	_updateStyle: function () {
-		if (this.options.stroke) {
-			this._stroke.weight = this.options.weight + 'px';
-			this._stroke.color = this.options.color;
-			this._stroke.opacity = this.options.opacity;
+		var stroke = this._stroke,
+			fill = this._fill,
+			options = this.options,
+			container = this._container;
+
+		container.stroked = options.stroke;
+		container.filled = options.fill;
+
+		if (options.stroke) {
+			stroke.weight  = options.weight + 'px';
+			stroke.color   = options.color;
+			stroke.opacity = options.opacity;
 		}
-		if (this.options.fill) {
-			this._fill.color = this.options.fillColor || this.options.color;
-			this._fill.opacity = this.options.fillOpacity;
+
+		if (options.fill) {
+			fill.color   = options.fillColor || options.color;
+			fill.opacity = options.fillOpacity;
 		}
 	},
 
 	_updatePath: function () {
-		this._container.style.display = 'none';
+		var style = this._container.style;
+
+		style.display = 'none';
 		this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug
-		this._container.style.display = '';
+		style.display = '';
 	}
 });
 
 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);
+		if (this._pathRoot) { return; }
 
-			this.on('moveend', this._updatePathViewport);
-			this._updatePathViewport();
-		}
+		var root = this._pathRoot = document.createElement('div');
+		root.className = 'leaflet-vml-container';
+		this._panes.overlayPane.appendChild(root);
+
+		this.on('moveend', this._updatePathViewport);
+		this._updatePathViewport();
 	}
 });
diff --git a/src/layer/vector/Path.js b/src/layer/vector/Path.js
index bcaf4ff..4f68c04 100644
--- a/src/layer/vector/Path.js
+++ b/src/layer/vector/Path.js
@@ -21,10 +21,7 @@ L.Path = L.Class.extend({
 		fillColor: null, //same as color by default
 		fillOpacity: 0.2,
 
-		clickable: true,
-
-		// TODO remove this, as all paths now update on moveend
-		updateOnMoveEnd: true
+		clickable: true
 	},
 
 	initialize: function (options) {
@@ -39,10 +36,9 @@ L.Path = L.Class.extend({
 		this.projectLatlngs();
 		this._updatePath();
 
-		map.on('viewreset', this.projectLatlngs, this);
-
-		this._updateTrigger = this.options.updateOnMoveEnd ? 'moveend' : 'viewreset';
-		map.on(this._updateTrigger, this._updatePath, this);
+		map
+			.on('viewreset', this.projectLatlngs, this)
+			.on('moveend', this._updatePath, this);
 	},
 
 	onRemove: function (map) {
@@ -50,8 +46,9 @@ L.Path = L.Class.extend({
 
 		map._pathRoot.removeChild(this._container);
 
-		map.off('viewreset', this.projectLatlngs, this);
-		map.off(this._updateTrigger, this._updatePath, this);
+		map
+			.off('viewreset', this.projectLatlngs, this)
+			.off('moveend', this._updatePath, this);
 	},
 
 	projectLatlngs: function () {
@@ -60,17 +57,20 @@ L.Path = L.Class.extend({
 
 	setStyle: function (style) {
 		L.Util.setOptions(this, style);
+
 		if (this._container) {
 			this._updateStyle();
 		}
+
 		return this;
 	},
 
-	_redraw: function () {
+	redraw: function () {
 		if (this._map) {
 			this.projectLatlngs();
 			this._updatePath();
 		}
+		return this;
 	}
 });
 
@@ -78,9 +78,8 @@ L.Map.include({
 	_updatePathViewport: function () {
 		var p = L.Path.CLIP_PADDING,
 			size = this.getSize(),
-			//TODO this._map._getMapPanePos()
 			panePos = L.DomUtil.getPosition(this._mapPane),
-			min = panePos.multiplyBy(-1).subtract(size.multiplyBy(p)),
+			min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)),
 			max = min.add(size.multiplyBy(1 + p * 2));
 
 		this._pathViewport = new L.Bounds(min, max);
diff --git a/src/layer/vector/Polyline.Edit.js b/src/layer/vector/Polyline.Edit.js
new file mode 100644
index 0000000..1c21022
--- /dev/null
+++ b/src/layer/vector/Polyline.Edit.js
@@ -0,0 +1,210 @@
+L.Handler.PolyEdit = L.Handler.extend({
+	options: {
+		icon: new L.DivIcon({
+			iconSize: new L.Point(8, 8),
+			className: 'leaflet-div-icon leaflet-editing-icon'
+		})
+	},
+
+	initialize: function (poly, options) {
+		this._poly = poly;
+		L.Util.setOptions(this, options);
+	},
+
+	addHooks: function () {
+		if (this._poly._map) {
+			if (!this._markerGroup) {
+				this._initMarkers();
+			}
+			this._poly._map.addLayer(this._markerGroup);
+		}
+	},
+
+	removeHooks: function () {
+		if (this._poly._map) {
+			this._poly._map.removeLayer(this._markerGroup);
+			delete this._markerGroup;
+			delete this._markers;
+		}
+	},
+
+	updateMarkers: function () {
+		this._markerGroup.clearLayers();
+		this._initMarkers();
+	},
+
+	_initMarkers: function () {
+		if (!this._markerGroup) {
+			this._markerGroup = new L.LayerGroup();
+		}
+		this._markers = [];
+
+		var latlngs = this._poly._latlngs,
+		    i, j, len, marker;
+
+		// TODO refactor holes implementation in Polygon to support it here
+
+		for (i = 0, len = latlngs.length; i < len; i++) {
+
+			marker = this._createMarker(latlngs[i], i);
+			marker.on('click', this._onMarkerClick, this);
+			this._markers.push(marker);
+		}
+
+		var markerLeft, markerRight;
+
+		for (i = 0, j = len - 1; i < len; j = i++) {
+			if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) {
+				continue;
+			}
+
+			markerLeft = this._markers[j];
+			markerRight = this._markers[i];
+
+			this._createMiddleMarker(markerLeft, markerRight);
+			this._updatePrevNext(markerLeft, markerRight);
+		}
+	},
+
+	_createMarker: function (latlng, index) {
+		var marker = new L.Marker(latlng, {
+			draggable: true,
+			icon: this.options.icon
+		});
+
+		marker._origLatLng = latlng;
+		marker._index = index;
+
+		marker.on('drag', this._onMarkerDrag, this);
+		marker.on('dragend', this._fireEdit, this);
+
+		this._markerGroup.addLayer(marker);
+
+		return marker;
+	},
+
+	_fireEdit: function () {
+		this._poly.fire('edit');
+	},
+
+	_onMarkerDrag: function (e) {
+		var marker = e.target;
+
+		L.Util.extend(marker._origLatLng, marker._latlng);
+
+		if (marker._middleLeft) {
+			marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker));
+		}
+		if (marker._middleRight) {
+			marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next));
+		}
+
+		this._poly.redraw();
+	},
+
+	_onMarkerClick: function (e) {
+		// Default action on marker click is to remove that marker, but if we remove the marker when latlng count < 3, we don't have a valid polyline anymore
+		if (this._poly._latlngs.length < 3) {
+			return;
+		}
+		
+		var marker = e.target,
+		    i = marker._index;
+		
+		// Check existence of previous and next markers since they wouldn't exist for edge points on the polyline
+		if (marker._prev && marker._next) {
+			this._createMiddleMarker(marker._prev, marker._next);
+			this._updatePrevNext(marker._prev, marker._next);
+		}
+
+		// The marker itself is guaranteed to exist and present in the layer, since we managed to click on it
+		this._markerGroup.removeLayer(marker);
+		// Check for the existence of middle left or middle right
+		if (marker._middleLeft) {
+			this._markerGroup.removeLayer(marker._middleLeft);
+		}
+		if (marker._middleRight) {
+			this._markerGroup.removeLayer(marker._middleRight);
+		}
+		this._markers.splice(i, 1);
+		this._poly.spliceLatLngs(i, 1);
+		this._updateIndexes(i, -1);
+		this._poly.fire('edit');
+	},
+
+	_updateIndexes: function (index, delta) {
+		this._markerGroup._iterateLayers(function (marker) {
+			if (marker._index > index) {
+				marker._index += delta;
+			}
+		});
+	},
+
+	_createMiddleMarker: function (marker1, marker2) {
+		var latlng = this._getMiddleLatLng(marker1, marker2),
+			marker = this._createMarker(latlng),
+			onClick,
+			onDragStart,
+			onDragEnd;
+
+		marker.setOpacity(0.6);
+
+		marker1._middleRight = marker2._middleLeft = marker;
+
+		onDragStart = function () {
+			var i = marker2._index;
+
+			marker._index = i;
+
+			marker
+				.off('click', onClick)
+				.on('click', this._onMarkerClick, this);
+
+			latlng.lat = marker.getLatLng().lat;
+			latlng.lng = marker.getLatLng().lng;
+			this._poly.spliceLatLngs(i, 0, latlng);
+			this._markers.splice(i, 0, marker);
+
+			marker.setOpacity(1);
+
+			this._updateIndexes(i, 1);
+			marker2._index++;
+			this._updatePrevNext(marker1, marker);
+			this._updatePrevNext(marker, marker2);
+		};
+
+		onDragEnd = function () {
+			marker.off('dragstart', onDragStart, this);
+			marker.off('dragend', onDragEnd, this);
+
+			this._createMiddleMarker(marker1, marker);
+			this._createMiddleMarker(marker, marker2);
+		};
+
+		onClick = function () {
+			onDragStart.call(this);
+			onDragEnd.call(this);
+			this._poly.fire('edit');
+		};
+
+		marker
+			.on('click', onClick, this)
+			.on('dragstart', onDragStart, this)
+			.on('dragend', onDragEnd, this);
+
+		this._markerGroup.addLayer(marker);
+	},
+
+	_updatePrevNext: function (marker1, marker2) {
+		marker1._next = marker2;
+		marker2._prev = marker1;
+	},
+
+	_getMiddleLatLng: function (marker1, marker2) {
+		var map = this._poly._map,
+		    p1 = map.latLngToLayerPoint(marker1.getLatLng()),
+		    p2 = map.latLngToLayerPoint(marker2.getLatLng());
+
+		return map.layerPointToLatLng(p1._add(p2).divideBy(2));
+	}
+});
diff --git a/src/layer/vector/Polyline.js b/src/layer/vector/Polyline.js
index 4113658..2f876fc 100644
--- a/src/layer/vector/Polyline.js
+++ b/src/layer/vector/Polyline.js
@@ -1,17 +1,23 @@
-
 L.Polyline = L.Path.extend({
 	initialize: function (latlngs, options) {
 		L.Path.prototype.initialize.call(this, options);
 		this._latlngs = latlngs;
+
+		// TODO refactor: move to Polyline.Edit.js
+		if (L.Handler.PolyEdit) {
+			this.editing = new L.Handler.PolyEdit(this);
+
+			if (this.options.editable) {
+				this.editing.enable();
+			}
+		}
 	},
 
 	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
+		noClip: false
 	},
 
 	projectLatlngs: function () {
@@ -35,19 +41,17 @@ L.Polyline = L.Path.extend({
 
 	setLatLngs: function (latlngs) {
 		this._latlngs = latlngs;
-		this._redraw();
-		return this;
+		return this.redraw();
 	},
 
 	addLatLng: function (latlng) {
 		this._latlngs.push(latlng);
-		this._redraw();
-		return this;
+		return this.redraw();
 	},
 
 	spliceLatLngs: function (index, howMany) {
 		var removed = [].splice.apply(this._latlngs, arguments);
-		this._redraw();
+		this.redraw();
 		return removed;
 	},
 
@@ -81,6 +85,27 @@ L.Polyline = L.Path.extend({
 		return b;
 	},
 
+	// TODO refactor: move to Polyline.Edit.js
+	onAdd: function (map) {
+		L.Path.prototype.onAdd.call(this, map);
+
+		if (this.editing && this.editing.enabled()) {
+			this.editing.addHooks();
+		}
+	},
+
+	onRemove: function (map) {
+		if (this.editing && this.editing.enabled()) {
+			this.editing.removeHooks();
+		}
+
+		L.Path.prototype.onRemove.call(this, map);
+	},
+
+	_initEvents: function () {
+		L.Path.prototype._initEvents.call(this);
+	},
+
 	_getPathPartStr: function (points) {
 		var round = L.Path.VML;
 
diff --git a/src/layer/vector/Rectangle.js b/src/layer/vector/Rectangle.js
new file mode 100644
index 0000000..2ea985b
--- /dev/null
+++ b/src/layer/vector/Rectangle.js
@@ -0,0 +1,23 @@
+/*
+ * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds
+ */
+
+L.Rectangle = L.Polygon.extend({
+	initialize: function (latLngBounds, options) {
+		L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
+	},
+
+	setBounds: function (latLngBounds) {
+		this.setLatLngs(this._boundsToLatLngs(latLngBounds));
+	},
+	
+	_boundsToLatLngs: function (latLngBounds) {
+	    return [
+	        latLngBounds.getSouthWest(),
+	        latLngBounds.getNorthWest(),
+	        latLngBounds.getNorthEast(),
+	        latLngBounds.getSouthEast(),
+	        latLngBounds.getSouthWest()
+	    ];
+	}
+});
diff --git a/src/layer/vector/canvas/Circle.Canvas.js b/src/layer/vector/canvas/Circle.Canvas.js
index 8a79f84..96ebebf 100644
--- a/src/layer/vector/canvas/Circle.Canvas.js
+++ b/src/layer/vector/canvas/Circle.Canvas.js
@@ -6,7 +6,7 @@ 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);
+		this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false);
 	},
 
 	_containsPoint: function (p) {
diff --git a/src/layer/vector/canvas/Path.Canvas.js b/src/layer/vector/canvas/Path.Canvas.js
index 019d46c..6ff49c0 100644
--- a/src/layer/vector/canvas/Path.Canvas.js
+++ b/src/layer/vector/canvas/Path.Canvas.js
@@ -13,22 +13,20 @@ L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path :
 		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;
+		var options = this.options;
+
+		if (options.stroke) {
+			this._ctx.lineWidth = options.weight;
+			this._ctx.strokeStyle = options.color;
 		}
-		if (this.options.fill) {
-			this._ctx.fillStyle = this.options.fillColor || this.options.color;
+		if (options.fill) {
+			this._ctx.fillStyle = options.fillColor || options.color;
 		}
 	},
 
@@ -56,34 +54,30 @@ L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path :
 	},
 
 	_updatePath: function () {
-		if (this._checkIfEmpty()) {
-			return;
-		}
-
-		this._drawPath();
+		if (this._checkIfEmpty()) { return; }
 
-		this._ctx.save();
+		var ctx = this._ctx,
+			options = this.options;
 
+		this._drawPath();
+		ctx.save();
 		this._updateStyle();
 
-		var opacity = this.options.opacity,
-			fillOpacity = this.options.fillOpacity;
-
-		if (this.options.fill) {
-			if (fillOpacity < 1) {
-				this._ctx.globalAlpha = fillOpacity;
+		if (options.fill) {
+			if (options.fillOpacity < 1) {
+				ctx.globalAlpha = options.fillOpacity;
 			}
-			this._ctx.fill();
+			ctx.fill();
 		}
 
-		if (this.options.stroke) {
-			if (opacity < 1) {
-				this._ctx.globalAlpha = opacity;
+		if (options.stroke) {
+			if (options.opacity < 1) {
+				ctx.globalAlpha = options.opacity;
 			}
-			this._ctx.stroke();
+			ctx.stroke();
 		}
 
-		this._ctx.restore();
+		ctx.restore();
 
 		// TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
 	},
@@ -103,9 +97,10 @@ L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path :
 	},
 
     onRemove: function (map) {
-        map.off('viewreset', this._projectLatlngs, this);
-        map.off(this._updateTrigger, this._updatePath, this);
-        map.fire(this._updateTrigger);
+        map
+	        .off('viewreset', this._projectLatlngs, this)
+            .off('moveend', this._updatePath, this)
+            .fire('moveend');
     }
 });
 
@@ -124,12 +119,21 @@ L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {}
 
 			this._panes.overlayPane.appendChild(root);
 
+			if (this.options.zoomAnimation) {
+				this._pathRoot.className = 'leaflet-zoom-animated';
+				this.on('zoomanim', this._animatePathZoom);
+				this.on('zoomend', this._endPathZoom);
+			}
 			this.on('moveend', this._updateCanvasViewport);
 			this._updateCanvasViewport();
 		}
 	},
 
 	_updateCanvasViewport: function () {
+		if (this._pathZooming) {
+			//Don't redraw while zooming. See _updateSvgViewport for more details
+			return;
+		}
 		this._updatePathViewport();
 
 		var vp = this._pathViewport,
@@ -137,7 +141,7 @@ L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {}
 			size = vp.max.subtract(min),
 			root = this._pathRoot;
 
-		//TODO check if it's works properly on mobile webkit
+		//TODO check if this works properly on mobile webkit
 		L.DomUtil.setPosition(root, min);
 		root.width = size.x;
 		root.height = size.y;
diff --git a/src/map/Map.js b/src/map/Map.js
index 3b0f25e..1ed4f95 100644
--- a/src/map/Map.js
+++ b/src/map/Map.js
@@ -6,78 +6,36 @@ L.Map = L.Class.extend({
 	includes: L.Mixin.Events,
 
 	options: {
-		// projection
-		crs: L.CRS.EPSG3857 || L.CRS.EPSG4326,
-		scale: function (zoom) {
-			return 256 * Math.pow(2, zoom);
-		},
-
-		// state
-		center: null,
-		zoom: null,
-		layers: [],
-
-		// interaction
-		dragging: true,
-		touchZoom: L.Browser.touch && !L.Browser.android,
-		scrollWheelZoom: !L.Browser.touch,
-		doubleClickZoom: 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,
+		crs: L.CRS.EPSG3857,
+
+		/*
+		center: LatLng,
+		zoom: Number,
+		layers: Array,
+		*/
 
-		// misc
+		fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android,
 		trackResize: true,
-		closePopupOnClick: true,
-		worldCopyJump: true
+		animateMarkerZoom: true
 	},
 
-
-	// constructor
-
 	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;
+		options = L.Util.setOptions(this, options);
 
+		this._initContainer(id);
 		this._initLayout();
+		this._initHooks();
+		this._initEvents();
 
-		if (L.DomEvent) {
-			this._initEvents();
-			if (L.Handler) {
-				this._initInteraction();
-			}
-			if (L.Control) {
-				this._initControls();
-			}
+		if (options.maxBounds) {
+			this.setMaxBounds(options.maxBounds);
 		}
 
-		if (this.options.maxBounds) {
-			this.setMaxBounds(this.options.maxBounds);
+		if (options.center && typeof options.zoom !== 'undefined') {
+			this.setView(options.center, options.zoom, true);
 		}
 
-		var center = this.options.center,
-			zoom = this.options.zoom;
-
-		if (center !== null && zoom !== null) {
-			this.setView(center, zoom, true);
-		}
-
-		var layers = this.options.layers;
-		layers = (layers instanceof Array ? layers : [layers]);
-		this._tileLayersNum = 0;
-		this._initLayers(layers);
+		this._initLayers(options.layers);
 	},
 
 
@@ -85,7 +43,6 @@ L.Map = L.Class.extend({
 
 	// replaced by animation-powered implementation in Map.PanAnimation.js
 	setView: function (center, zoom) {
-		// reset the map view
 		this._resetView(center, this._limitZoom(zoom));
 		return this;
 	},
@@ -109,7 +66,8 @@ L.Map = L.Class.extend({
 
 	fitWorld: function () {
 		var sw = new L.LatLng(-60, -170),
-			ne = new L.LatLng(85, 179);
+		    ne = new L.LatLng(85, 179);
+
 		return this.fitBounds(new L.LatLngBounds(sw, ne));
 	},
 
@@ -124,9 +82,7 @@ L.Map = L.Class.extend({
 		this._rawPanBy(offset);
 
 		this.fire('move');
-		this.fire('moveend');
-
-		return this;
+		return this.fire('moveend');
 	},
 
 	setMaxBounds: function (bounds) {
@@ -148,17 +104,18 @@ L.Map = L.Class.extend({
 				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;
+		    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;
@@ -176,33 +133,32 @@ L.Map = L.Class.extend({
 		return this.panBy(new L.Point(dx, dy, true));
 	},
 
-	addLayer: function (layer, insertAtTheTop) {
+	addLayer: function (layer, insertAtTheBottom) {
+		// TODO method is too big, refactor
+
 		var id = L.Util.stamp(layer);
 
-		if (this._layers[id]) {
-			return this;
-		}
+		if (this._layers[id]) { return this; }
 
 		this._layers[id] = layer;
 
+		// TODO getMaxZoom, getMinZoom in ILayer (instead of options)
 		if (layer.options && !isNaN(layer.options.maxZoom)) {
 			this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);
 		}
 		if (layer.options && !isNaN(layer.options.minZoom)) {
 			this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);
 		}
-		//TODO getMaxZoom, getMinZoom in ILayer (instead of options)
 
+		// TODO looks ugly, refactor!!!
 		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
 			this._tileLayersNum++;
-			layer.on('load', this._onTileLayerLoad, this);
-		}
-		if (this.attributionControl && layer.getAttribution) {
-			this.attributionControl.addAttribution(layer.getAttribution());
+            this._tileLayersToLoad++;
+            layer.on('load', this._onTileLayerLoad, this);
 		}
 
 		var onMapLoad = function () {
-			layer.onAdd(this, insertAtTheTop);
+			layer.onAdd(this, insertAtTheBottom);
 			this.fire('layeradd', {layer: layer});
 		};
 
@@ -218,21 +174,20 @@ L.Map = L.Class.extend({
 	removeLayer: function (layer) {
 		var id = L.Util.stamp(layer);
 
-		if (this._layers[id]) {
-			layer.onRemove(this);
-			delete this._layers[id];
+		if (!this._layers[id]) { return; }
 
-			if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
-				this._tileLayersNum--;
-				layer.off('load', this._onTileLayerLoad, this);
-			}
-			if (this.attributionControl && layer.getAttribution) {
-				this.attributionControl.removeAttribution(layer.getAttribution());
-			}
+		layer.onRemove(this);
+
+		delete this._layers[id];
 
-			this.fire('layerremove', {layer: layer});
+		// TODO looks ugly, refactor
+		if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
+			this._tileLayersNum--;
+            this._tileLayersToLoad--;
+            layer.off('load', this._onTileLayerLoad, this);
 		}
-		return this;
+
+		return this.fire('layerremove', {layer: layer});
 	},
 
 	hasLayer: function (layer) {
@@ -249,18 +204,28 @@ L.Map = L.Class.extend({
 			this.setMaxBounds(this.options.maxBounds);
 		}
 
-		if (!this._loaded) {
-			return this;
-		}
+		if (!this._loaded) { return this; }
 
-		this._rawPanBy(oldSize.subtract(this.getSize()).divideBy(2, true));
+		var offset = oldSize.subtract(this.getSize()).divideBy(2, true);
+		this._rawPanBy(offset);
 
 		this.fire('move');
 
 		clearTimeout(this._sizeTimer);
-		this._sizeTimer = setTimeout(L.Util.bind(function () {
-			this.fire('moveend');
-		}, this), 200);
+		this._sizeTimer = setTimeout(L.Util.bind(this.fire, this, 'moveend'), 200);
+
+		return this;
+	},
+
+	// TODO handler.addTo
+	addHandler: function (name, HandlerClass) {
+		if (!HandlerClass) { return; }
+
+		this[name] = new HandlerClass(this);
+
+		if (this.options[name]) {
+			this[name].enable();
+		}
 
 		return this;
 	},
@@ -268,9 +233,10 @@ L.Map = L.Class.extend({
 
 	// public methods for getting map state
 
-	getCenter: function (unbounded) { // (Boolean)
+	getCenter: function (unbounded) { // (Boolean) -> LatLng
 		var viewHalf = this.getSize().divideBy(2),
-			centerPoint = this._getTopLeftPoint().add(viewHalf);
+		    centerPoint = this._getTopLeftPoint().add(viewHalf);
+
 		return this.unproject(centerPoint, this._zoom, unbounded);
 	},
 
@@ -280,36 +246,37 @@ L.Map = L.Class.extend({
 
 	getBounds: function () {
 		var bounds = this.getPixelBounds(),
-			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);
+		    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 () {
 		var z1 = this.options.minZoom || 0,
-			z2 = this._layersMinZoom || 0,
-			z3 = this._boundsMinZoom || 0;
+		    z2 = this._layersMinZoom || 0,
+		    z3 = this._boundsMinZoom || 0;
 
 		return Math.max(z1, z2, z3);
 	},
 
 	getMaxZoom: function () {
-		var z1 = isNaN(this.options.maxZoom) ? Infinity : this.options.maxZoom,
-			z2 = this._layersMaxZoom || Infinity;
+		var z1 = typeof this.options.maxZoom === 'undefined' ? Infinity : this.options.maxZoom,
+		    z2 = typeof this._layersMaxZoom  === 'undefined' ? Infinity : this._layersMaxZoom;
 
 		return Math.min(z1, z2);
 	},
 
-	getBoundsZoom: function (bounds, inside) { // (LatLngBounds)
+	getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number
 		var size = this.getSize(),
-			zoom = this.options.minZoom || 0,
-			maxZoom = this.getMaxZoom(),
-			ne = bounds.getNorthEast(),
-			sw = bounds.getSouthWest(),
-			boundsSize,
-			nePoint,
-			swPoint,
-			zoomNotFound = true;
+		    zoom = this.options.minZoom || 0,
+		    maxZoom = this.getMaxZoom(),
+		    ne = bounds.getNorthEast(),
+		    sw = bounds.getSouthWest(),
+		    boundsSize,
+		    nePoint,
+		    swPoint,
+		    zoomNotFound = true;
 
 		if (inside) {
 			zoom--;
@@ -322,11 +289,11 @@ L.Map = L.Class.extend({
 			boundsSize = new L.Point(nePoint.x - swPoint.x, swPoint.y - nePoint.y);
 
 			if (!inside) {
-				zoomNotFound = (boundsSize.x <= size.x) && (boundsSize.y <= size.y);
+				zoomNotFound = boundsSize.x <= size.x && boundsSize.y <= size.y;
 			} else {
-				zoomNotFound = (boundsSize.x < size.x) || (boundsSize.y < size.y);
+				zoomNotFound = boundsSize.x < size.x || boundsSize.y < size.y;
 			}
-		} while (zoomNotFound && (zoom <= maxZoom));
+		} while (zoomNotFound && zoom <= maxZoom);
 
 		if (zoomNotFound && inside) {
 			return null;
@@ -337,16 +304,18 @@ L.Map = L.Class.extend({
 
 	getSize: function () {
 		if (!this._size || this._sizeChanged) {
-			this._size = new L.Point(this._container.clientWidth, this._container.clientHeight);
+			this._size = new L.Point(
+				this._container.clientWidth,
+				this._container.clientHeight);
+
 			this._sizeChanged = false;
 		}
 		return this._size;
 	},
 
 	getPixelBounds: function () {
-		var topLeftPoint = this._getTopLeftPoint(),
-			size = this.getSize();
-		return new L.Bounds(topLeftPoint, topLeftPoint.add(size));
+		var topLeftPoint = this._getTopLeftPoint();
+		return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
 	},
 
 	getPixelOrigin: function () {
@@ -356,6 +325,10 @@ L.Map = L.Class.extend({
 	getPanes: function () {
 		return this._panes;
 	},
+	
+	getContainer: function () {
+		return this._container;
+	},
 
 
 	// conversion methods
@@ -388,32 +361,55 @@ L.Map = L.Class.extend({
 		return this.project(latlng)._round()._subtract(this._initialTopLeftPoint);
 	},
 
+	containerPointToLatLng: function (point) {
+		return this.layerPointToLatLng(this.containerPointToLayerPoint(point));
+	},
+
+	latLngToContainerPoint: function (latlng) {
+		return this.layerPointToContainerPoint(this.latLngToLayerPoint(latlng));
+	},
+
 	project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
-		zoom = (typeof zoom === 'undefined' ? this._zoom : zoom);
-		return this.options.crs.latLngToPoint(latlng, this.options.scale(zoom));
+		zoom = typeof zoom === 'undefined' ? this._zoom : zoom;
+		return this.options.crs.latLngToPoint(latlng, 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);
+		// TODO remove unbounded, making it true all the time?
+		zoom = typeof zoom === 'undefined' ? this._zoom : zoom;
+		return this.options.crs.pointToLatLng(point, zoom, unbounded);
 	},
 
 
 	// private methods that modify map state
 
+	_initContainer: function (id) {
+		var container = this._container = L.DomUtil.get(id);
+
+		if (container._leaflet) {
+			throw new Error("Map container is already initialized.");
+		}
+
+		container._leaflet = true;
+	},
+
 	_initLayout: function () {
 		var container = this._container;
 
 		container.innerHTML = '';
-
 		container.className += ' leaflet-container';
 
+		if (L.Browser.touch) {
+			container.className += ' leaflet-touch';
+		}
+
 		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' && position !== 'fixed') {
 			container.style.position = 'relative';
 		}
 
@@ -436,13 +432,29 @@ L.Map = L.Class.extend({
 		panes.overlayPane = this._createPane('leaflet-overlay-pane');
 		panes.markerPane = this._createPane('leaflet-marker-pane');
 		panes.popupPane = this._createPane('leaflet-popup-pane');
+
+		if (!this.options.animateMarkerZoom) {
+			panes.markerPane.className += ' leaflet-zoom-hide';
+			panes.shadowPane.className += ' leaflet-zoom-hide';
+			panes.popupPane.className += ' leaflet-zoom-hide';
+		}
 	},
 
 	_createPane: function (className, container) {
 		return L.DomUtil.create('div', className, container || this._objectsPane);
 	},
 
+	_initializers: [],
+
+	_initHooks: function () {
+		var i, len;
+		for (i = 0, len = this._initializers.length; i < len; i++) {
+			this._initializers[i].call(this);
+		}
+	},
+
 	_resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {
+
 		var zoomChanged = (this._zoom !== zoom);
 
 		if (!afterZoomAnim) {
@@ -460,17 +472,19 @@ L.Map = L.Class.extend({
 		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._initialTopLeftPoint._add(L.DomUtil.getPosition(this._mapPane));
 		}
 
 		this._tileLayersToLoad = this._tileLayersNum;
+
 		this.fire('viewreset', {hard: !preserveMapOffset});
 
 		this.fire('move');
+
 		if (zoomChanged || afterZoomAnim) {
 			this.fire('zoomend');
 		}
+
 		this.fire('moveend');
 
 		if (!this._loaded) {
@@ -480,7 +494,10 @@ L.Map = L.Class.extend({
 	},
 
 	_initLayers: function (layers) {
+		layers = layers ? (layers instanceof Array ? layers : [layers]) : [];
+
 		this._layers = {};
+		this._tileLayersNum = 0;
 
 		var i, len;
 
@@ -489,25 +506,17 @@ L.Map = L.Class.extend({
 		}
 	},
 
-	_initControls: function () {
-		if (this.options.zoomControl) {
-			this.addControl(new L.Control.Zoom());
-		}
-		if (this.options.attributionControl) {
-			this.attributionControl = new L.Control.Attribution();
-			this.addControl(this.attributionControl);
-		}
-	},
-
 	_rawPanBy: function (offset) {
-		var mapPaneOffset = L.DomUtil.getPosition(this._mapPane);
-		L.DomUtil.setPosition(this._mapPane, mapPaneOffset.subtract(offset));
+		var newPos = L.DomUtil.getPosition(this._mapPane).subtract(offset);
+		L.DomUtil.setPosition(this._mapPane, newPos);
 	},
 
 
 	// map events
 
 	_initEvents: function () {
+		if (!L.DomEvent) { return; }
+
 		L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
 
 		var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu'];
@@ -528,58 +537,39 @@ L.Map = L.Class.extend({
 	},
 
 	_onMouseClick: function (e) {
-		if (!this._loaded || (this.dragging && this.dragging.moved())) {
-			return;
-		}
+		if (!this._loaded || (this.dragging && this.dragging.moved())) { return; }
 
 		this.fire('pre' + e.type);
 		this._fireMouseEvent(e);
 	},
 
 	_fireMouseEvent: function (e) {
-		if (!this._loaded) {
-			return;
-		}
+		if (!this._loaded) { return; }
 
 		var type = e.type;
+
 		type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
 
-		if (!this.hasEventListeners(type)) {
-			return;
-		}
+		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 () {
-		var handlers = {
-			dragging: L.Map.Drag,
-			touchZoom: L.Map.TouchZoom,
-			doubleClickZoom: L.Map.DoubleClickZoom,
-			scrollWheelZoom: L.Map.ScrollWheelZoom,
-			boxZoom: L.Map.BoxZoom
-		};
+		var containerPoint = this.mouseEventToContainerPoint(e),
+			layerPoint = this.containerPointToLayerPoint(containerPoint),
+			latlng = this.layerPointToLatLng(layerPoint);
 
-		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();
-				}
-				// TODO move enabling to handler contructor
-			}
-		}
+		this.fire(type, {
+			latlng: latlng,
+			layerPoint: layerPoint,
+			containerPoint: containerPoint,
+			originalEvent: e
+		});
 	},
 
 	_onTileLayerLoad: function () {
+		// TODO super-ugly, refactor!!!
 		// clear scaled tiles after all new tiles are loaded (for performance)
 		this._tileLayersToLoad--;
 		if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {
@@ -596,18 +586,37 @@ L.Map = L.Class.extend({
 			throw new Error('Set map center and zoom first.');
 		}
 
-		var offset = L.DomUtil.getPosition(this._mapPane);
-		return this._initialTopLeftPoint.subtract(offset);
+		var mapPanePos = L.DomUtil.getPosition(this._mapPane);
+		return this._initialTopLeftPoint.subtract(mapPanePos);
 	},
 
-	_getNewTopLeftPoint: function (center) {
+	_getNewTopLeftPoint: function (center, zoom) {
 		var viewHalf = this.getSize().divideBy(2);
-		return this.project(center).subtract(viewHalf).round();
+		// TODO round on display, not calculation to increase precision?
+		return this.project(center, zoom)._subtract(viewHalf)._round();
+	},
+
+	_latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {
+		var mapPaneOffset = L.DomUtil.getPosition(this._mapPane),
+			topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(mapPaneOffset);
+
+		return this.project(latlng, newZoom)._round()._subtract(topLeft);
 	},
 
 	_limitZoom: function (zoom) {
-		var min = this.getMinZoom();
-		var max = this.getMaxZoom();
+		var min = this.getMinZoom(),
+			max = this.getMaxZoom();
+
 		return Math.max(min, Math.min(max, zoom));
 	}
 });
+
+L.Map.addInitHook = function (fn) {
+	var args = Array.prototype.slice.call(arguments, 1);
+
+	var init = typeof fn === 'function' ? fn : function () {
+		this[fn].apply(this, args);
+	};
+
+	this.prototype._initializers.push(init);
+};
\ No newline at end of file
diff --git a/src/map/anim/Map.PanAnimation.js b/src/map/anim/Map.PanAnimation.js
index 56467b3..44765de 100644
--- a/src/map/anim/Map.PanAnimation.js
+++ b/src/map/anim/Map.PanAnimation.js
@@ -1,6 +1,8 @@
+
 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) {
@@ -10,8 +12,8 @@ L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
 			center = new L.LatLng(center.lat, center.lng);
 
 			var done = (zoomChanged ?
-						!!this._zoomToIfCenterInView && this._zoomToIfCenterInView(center, zoom, offset) :
-						this._panByIfClose(offset));
+					this._zoomToIfCenterInView && this._zoomToIfCenterInView(center, zoom, offset) :
+					this._panByIfClose(offset));
 
 			// exit if animated pan or zoom started
 			if (done) {
@@ -25,19 +27,24 @@ L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
 		return this;
 	},
 
-	panBy: function (offset) {
+	panBy: function (offset, options) {
 		if (!(offset.x || offset.y)) {
 			return this;
 		}
 
 		if (!this._panTransition) {
-			this._panTransition = new L.Transition(this._mapPane, {duration: 0.3});
+			this._panTransition = new L.Transition(this._mapPane);
 
 			this._panTransition.on('step', this._onPanTransitionStep, this);
 			this._panTransition.on('end', this._onPanTransitionEnd, this);
 		}
+
+		L.Util.setOptions(this._panTransition, L.Util.extend({duration: 0.25}, options));
+
 		this.fire('movestart');
 
+		this._mapPane.className += ' leaflet-pan-anim';
+
 		this._panTransition.run({
 			position: L.DomUtil.getPosition(this._mapPane).subtract(offset)
 		});
@@ -50,6 +57,7 @@ L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
 	},
 
 	_onPanTransitionEnd: function () {
+		this._mapPane.className = this._mapPane.className.replace(/ leaflet-pan-anim/g, '');
 		this.fire('moveend');
 	},
 
@@ -64,6 +72,7 @@ L.Map.include(!(L.Transition && L.Transition.implemented()) ? {} : {
 	_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/anim/Map.ZoomAnimation.js b/src/map/anim/Map.ZoomAnimation.js
index bdaa8bf..8726caf 100644
--- a/src/map/anim/Map.ZoomAnimation.js
+++ b/src/map/anim/Map.ZoomAnimation.js
@@ -1,3 +1,7 @@
+L.Map.mergeOptions({
+	zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android && !L.Browser.mobileOpera
+});
+
 L.Map.include(!L.DomUtil.TRANSITION ? {} : {
 	_zoomToIfCenterInView: function (center, zoom, centerOffset) {
 
@@ -8,107 +12,146 @@ L.Map.include(!L.DomUtil.TRANSITION ? {} : {
 			return false;
 		}
 
-		var zoomDelta = zoom - this._zoom,
-			scale = Math.pow(2, zoomDelta),
+		var scale = Math.pow(2, zoom - this._zoom),
 			offset = centerOffset.divideBy(1 - 1 / scale);
 
-		//if offset does not exceed half of the view
+		// if offset does not exceed half of the view
 		if (!this._offsetIsWithinView(offset, 1)) {
 			return false;
 		}
 
 		this._mapPane.className += ' leaflet-zoom-anim';
 
-        this
+		this
 			.fire('movestart')
 			.fire('zoomstart');
 
+		//Hack: Disable this for android due to it not supporting double translate (mentioned in _runAnimation below)
+		//if Foreground layer doesn't have many tiles but bg layer does, keep the existing bg layer
+		if (!L.Browser.android && this._tileBg && this._getLoadedTilesPercentage(this._tileBg) > 0.5 && this._getLoadedTilesPercentage(this._tilePane) < 0.5) {
+			//Leave current bg and just zoom it some more
+
+			this._tilePane.style.visibility = 'hidden';
+			this._tilePane.empty = true;
+			this._stopLoadingImages(this._tilePane);
+		} else {
+			this._prepareTileBg();
+		}
+
 		var centerPoint = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
 			origin = centerPoint.add(offset);
 
-		this._prepareTileBg();
-
+		this.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
+		
 		this._runAnimation(center, zoom, scale, origin);
 
 		return true;
 	},
 
 
-	_runAnimation: function (center, zoom, scale, origin) {
+	_runAnimation: function (center, zoom, scale, origin, backwardsTransform) {
 		this._animatingZoom = true;
 
 		this._animateToCenter = center;
 		this._animateToZoom = zoom;
 
-		var transform = L.DomUtil.TRANSFORM;
+		var transform = L.DomUtil.TRANSFORM,
+			tileBg = this._tileBg;
 
 		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)';
+			tileBg.style[transform] += ' translate(0,0)';
 		}
 
 		var scaleStr;
 
-		// Android doesn't like translate/scale chains, transformOrigin + scale works better but
+		// Android 2.* 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';
+			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
+		L.Util.falseFn(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);
+		if (backwardsTransform) {
+			options[transform] = tileBg.style[transform] + ' ' + scaleStr;
+		} else {
+			options[transform] = scaleStr + ' ' + tileBg.style[transform];
+		}
+
+		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;
 
+		if (!tileBg) {
+			tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
+			tileBg.style.zIndex = 1;
+		}
+
 		// 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;
+		tileBg.empty = true; //new FG
+		tilePane.empty = false; //new BG
 
+		//Switch out the current layer to be the new bg layer (And vice-versa)
 		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);
+		var newTileBg = this._tileBg = tilePane;
+
+		if (!newTileBg.transition) {
+			// TODO move to Map options
+			newTileBg.transition = new L.Transition(newTileBg, {
+				duration: 0.25,
+				easing: 'cubic-bezier(0.25,0.1,0.25,0.75)'
+			});
+			newTileBg.transition.on('end', this._onZoomTransitionEnd, this);
 		}
 
-		this._stopLoadingBgTiles();
+		this._stopLoadingImages(newTileBg);
+	},
+
+	_getLoadedTilesPercentage: function (container) {
+		var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
+			i, len, count = 0;
+
+		for (i = 0, len = tiles.length; i < len; i++) {
+			if (tiles[i].complete) {
+				count++;
+			}
+		}
+		return count / len;
 	},
 
 	// stops loading all tiles in the background layer
-	_stopLoadingBgTiles: function () {
-		var tiles = [].slice.call(this._tileBg.getElementsByTagName('img'));
+	_stopLoadingImages: function (container) {
+		var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
+			i, len, tile;
+
+		for (i = 0, len = tiles.length; i < len; i++) {
+			tile = tiles[i];
 
-		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=';
+			if (!tile.complete) {
+				tile.onload = L.Util.falseFn;
+				tile.onerror = L.Util.falseFn;
+				tile.src = L.Util.emptyImageUrl;
 
-				tiles[i].parentNode.removeChild(tiles[i]);
-				tiles[i] = null;
+				tile.parentNode.removeChild(tile);
 			}
 		}
 	},
diff --git a/src/map/ext/Map.Control.js b/src/map/ext/Map.Control.js
index 59c688e..c0d6f17 100644
--- a/src/map/ext/Map.Control.js
+++ b/src/map/ext/Map.Control.js
@@ -1,50 +1,30 @@
 L.Map.include({
 	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) {
-			corner.insertBefore(container, corner.firstChild);
-		} else {
-			corner.appendChild(container);
-		}
+		control.addTo(this);
 		return this;
 	},
 
 	removeControl: function (control) {
-		var pos = control.getPosition(),
-			corner = this._controlCorners[pos],
-			container = control.getContainer();
-
-		corner.removeChild(container);
-
-		if (control.onRemove) {
-			control.onRemove(this);
-		}
+		control.removeFrom(this);
 		return this;
 	},
 
 	_initControlPos: function () {
 		var corners = this._controlCorners = {},
-			classPart = 'leaflet-',
-			top = classPart + 'top',
-			bottom = classPart + 'bottom',
-			left = classPart + 'left',
-			right = classPart + 'right',
-			controlContainer = L.DomUtil.create('div', classPart + 'control-container', this._container);
+		    l = 'leaflet-',
+		    container = this._controlContainer =
+				L.DomUtil.create('div', l + 'control-container', this._container);
+
+		function createCorner(vSide, hSide) {
+			var className = l + vSide + ' ' + l + hSide;
 
-		if (L.Browser.touch) {
-			controlContainer.className += ' ' + classPart + 'big-buttons';
+			corners[vSide + hSide] =
+					L.DomUtil.create('div', className, container);
 		}
 
-		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);
+		createCorner('top', 'left');
+		createCorner('top', 'right');
+		createCorner('bottom', 'left');
+		createCorner('bottom', 'right');
 	}
 });
diff --git a/src/map/ext/Map.Geolocation.js b/src/map/ext/Map.Geolocation.js
index 567222c..69e6c5f 100644
--- a/src/map/ext/Map.Geolocation.js
+++ b/src/map/ext/Map.Geolocation.js
@@ -3,16 +3,18 @@
  */
 
 L.Map.include({
+	_defaultLocateOptions: {
+		watch: false,
+		setView: false,
+		maxZoom: Infinity,
+		timeout: 10000,
+		maximumAge: 0,
+		enableHighAccuracy: false
+	},
+
 	locate: function (/*Object*/ options) {
 
-		this._locationOptions = options = L.Util.extend({
-			watch: false,
-			setView: false,
-			maxZoom: Infinity,
-			timeout: 10000,
-			maximumAge: 0,
-			enableHighAccuracy: false
-		}, options);
+		options = this._locationOptions = L.Util.extend(this._defaultLocateOptions, options);
 
 		if (!navigator.geolocation) {
 			return this.fire('locationerror', {
@@ -36,19 +38,13 @@ L.Map.include({
 		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);
+		return this;
 	},
 
 	_handleGeolocationError: function (error) {
 		var c = error.code,
-			message = (c === 1 ? "permission denied" :
+			message =
+				(c === 1 ? "permission denied" :
 				(c === 2 ? "position unavailable" : "timeout"));
 
 		if (this._locationOptions.setView && !this._loaded) {
@@ -64,16 +60,19 @@ L.Map.include({
 	_handleGeolocationResponse: function (pos) {
 		var latAccuracy = 180 * pos.coords.accuracy / 4e7,
 			lngAccuracy = latAccuracy * 2,
+
 			lat = pos.coords.latitude,
 			lng = pos.coords.longitude,
-			latlng = new L.LatLng(lat, lng);
+			latlng = new L.LatLng(lat, lng),
 
-		var sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy),
+			sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy),
 			ne = new L.LatLng(lat + latAccuracy, lng + lngAccuracy),
-			bounds = new L.LatLngBounds(sw, ne);
+			bounds = new L.LatLngBounds(sw, ne),
+
+			options = this._locationOptions;
 
-		if (this._locationOptions.setView) {
-			var zoom = Math.min(this.getBoundsZoom(bounds), this._locationOptions.maxZoom);
+		if (options.setView) {
+			var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
 			this.setView(latlng, zoom);
 		}
 
diff --git a/src/map/ext/Map.Popup.js b/src/map/ext/Map.Popup.js
index 4d19f0d..c9e1752 100644
--- a/src/map/ext/Map.Popup.js
+++ b/src/map/ext/Map.Popup.js
@@ -2,19 +2,18 @@
 L.Map.include({
 	openPopup: function (popup) {
 		this.closePopup();
+
 		this._popup = popup;
-		this.addLayer(popup);
-		this.fire('popupopen', { popup: this._popup });
-	
-		return this;
+
+		return this
+			.addLayer(popup)
+			.fire('popupopen', {popup: this._popup});
 	},
 
 	closePopup: function () {
 		if (this._popup) {
-			this.removeLayer(this._popup);
-			this.fire('popupclose', { popup: this._popup });
-			this._popup = null;
+			this._popup._close();
 		}
 		return this;
 	}
-});
+});
\ No newline at end of file
diff --git a/src/map/handler/Map.BoxZoom.js b/src/map/handler/Map.BoxZoom.js
index 9b74aea..c8cd078 100644
--- a/src/map/handler/Map.BoxZoom.js
+++ b/src/map/handler/Map.BoxZoom.js
@@ -2,6 +2,10 @@
  * L.Handler.ShiftDragZoom is used internally by L.Map to add shift-drag zoom (zoom to a selected bounding box).
  */
 
+L.Map.mergeOptions({
+	boxZoom: true
+});
+
 L.Map.BoxZoom = L.Handler.extend({
 	initialize: function (map) {
 		this._map = map;
@@ -18,9 +22,7 @@ L.Map.BoxZoom = L.Handler.extend({
 	},
 
 	_onMouseDown: function (e) {
-		if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) {
-			return false;
-		}
+		if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
 
 		L.DomUtil.disableTextSelection();
 
@@ -29,28 +31,33 @@ L.Map.BoxZoom = L.Handler.extend({
 		this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane);
 		L.DomUtil.setPosition(this._box, this._startLayerPoint);
 
-		//TODO move cursor to styles
+		//TODO refactor: 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);
+		L.DomEvent
+			.addListener(document, 'mousemove', this._onMouseMove, this)
+			.addListener(document, 'mouseup', this._onMouseUp, this)
+			.preventDefault(e);
+			
+		this._map.fire("boxzoomstart");
 	},
 
 	_onMouseMove: function (e) {
-		var layerPoint = this._map.mouseEventToLayerPoint(e),
-			dx = layerPoint.x - this._startLayerPoint.x,
-			dy = layerPoint.y - this._startLayerPoint.y;
+		var startPoint = this._startLayerPoint,
+			box = this._box,
 
-		var newX = Math.min(layerPoint.x, this._startLayerPoint.x),
-			newY = Math.min(layerPoint.y, this._startLayerPoint.y),
-			newPos = new L.Point(newX, newY);
+			layerPoint = this._map.mouseEventToLayerPoint(e),
+			offset = layerPoint.subtract(startPoint),
 
-		L.DomUtil.setPosition(this._box, newPos);
+			newPos = new L.Point(
+				Math.min(layerPoint.x, startPoint.x),
+				Math.min(layerPoint.y, startPoint.y));
 
-		this._box.style.width = (Math.abs(dx) - 4) + 'px';
-		this._box.style.height = (Math.abs(dy) - 4) + 'px';
+		L.DomUtil.setPosition(box, newPos);
+
+		// TODO refactor: remove hardcoded 4 pixels
+		box.style.width  = (Math.abs(offset.x) - 4) + 'px';
+		box.style.height = (Math.abs(offset.y) - 4) + 'px';
 	},
 
 	_onMouseUp: function (e) {
@@ -59,15 +66,23 @@ L.Map.BoxZoom = L.Handler.extend({
 
 		L.DomUtil.enableTextSelection();
 
-		L.DomEvent.removeListener(document, 'mousemove', this._onMouseMove);
-		L.DomEvent.removeListener(document, 'mouseup', this._onMouseUp);
+		L.DomEvent
+			.removeListener(document, 'mousemove', this._onMouseMove)
+			.removeListener(document, 'mouseup', this._onMouseUp);
 
-		var layerPoint = this._map.mouseEventToLayerPoint(e);
+		var map = this._map,
+			layerPoint = map.mouseEventToLayerPoint(e);
 
 		var bounds = new L.LatLngBounds(
-				this._map.layerPointToLatLng(this._startLayerPoint),
-				this._map.layerPointToLatLng(layerPoint));
-
-		this._map.fitBounds(bounds);
+				map.layerPointToLatLng(this._startLayerPoint),
+				map.layerPointToLatLng(layerPoint));
+
+		map.fitBounds(bounds);
+		
+		map.fire("boxzoomend", {
+			boxZoomBounds: bounds
+		});
 	}
 });
+
+L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom);
diff --git a/src/map/handler/Map.DoubleClickZoom.js b/src/map/handler/Map.DoubleClickZoom.js
index 2edf794..7a7850d 100644
--- a/src/map/handler/Map.DoubleClickZoom.js
+++ b/src/map/handler/Map.DoubleClickZoom.js
@@ -2,10 +2,13 @@
  * L.Handler.DoubleClickZoom is used internally by L.Map to add double-click zooming.
  */
 
+L.Map.mergeOptions({
+	doubleClickZoom: true
+});
+
 L.Map.DoubleClickZoom = L.Handler.extend({
 	addHooks: function () {
 		this._map.on('dblclick', this._onDoubleClick);
-		// TODO remove 3d argument?
 	},
 
 	removeHooks: function () {
@@ -16,3 +19,5 @@ L.Map.DoubleClickZoom = L.Handler.extend({
 		this.setView(e.latlng, this._zoom + 1);
 	}
 });
+
+L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom);
\ No newline at end of file
diff --git a/src/map/handler/Map.Drag.js b/src/map/handler/Map.Drag.js
index fab6cd4..76a7ef9 100644
--- a/src/map/handler/Map.Drag.js
+++ b/src/map/handler/Map.Drag.js
@@ -2,6 +2,19 @@
  * L.Handler.MapDrag is used internally by L.Map to make the map draggable.
  */
 
+L.Map.mergeOptions({
+	dragging: true,
+
+	inertia: !L.Browser.android,
+	inertiaDeceleration: L.Browser.touch ? 3000 : 2000, // px/s^2
+	inertiaMaxSpeed:     L.Browser.touch ? 1500 : 1000, // px/s
+	inertiaThreshold:    L.Browser.touch ? 32   : 16, // ms
+
+	// TODO refactor, move to CRS
+	worldCopyJump: true,
+	continuousWorld: false
+});
+
 L.Map.Drag = L.Handler.extend({
 	addHooks: function () {
 		if (!this._draggable) {
@@ -31,12 +44,36 @@ L.Map.Drag = L.Handler.extend({
 	},
 
 	_onDragStart: function () {
-		this._map
+		var map = this._map;
+
+		map
 			.fire('movestart')
 			.fire('dragstart');
+
+		if (map._panTransition) {
+			map._panTransition._onTransitionEnd(true);
+		}
+
+		if (map.options.inertia) {
+			this._positions = [];
+			this._times = [];
+		}
 	},
 
 	_onDrag: function () {
+		if (this._map.options.inertia) {
+			var time = this._lastTime = +new Date(),
+			    pos = this._lastPos = this._draggable._newPos;
+
+			this._positions.push(pos);
+			this._times.push(time);
+
+			if (time - this._times[0] > 200) {
+				this._positions.shift();
+				this._times.shift();
+			}
+		}
+
 		this._map
 			.fire('move')
 			.fire('drag');
@@ -51,7 +88,7 @@ L.Map.Drag = L.Handler.extend({
 
 	_onPreDrag: function () {
 		var map = this._map,
-			worldWidth = map.options.scale(map.getZoom()),
+			worldWidth = map.options.crs.scale(map.getZoom()),
 			halfWidth = Math.round(worldWidth / 2),
 			dx = this._initialWorldOffset.x,
 			x = this._draggable._newPos.x,
@@ -63,13 +100,44 @@ L.Map.Drag = L.Handler.extend({
 	},
 
 	_onDragEnd: function () {
-		var map = this._map;
+		var map = this._map,
+			options = map.options,
+			delay = +new Date() - this._lastTime,
 
-		map
-			.fire('moveend')
-			.fire('dragend');
+			noInertia = !options.inertia ||
+					delay > options.inertiaThreshold ||
+					typeof this._positions[0] === 'undefined';
+
+		if (noInertia) {
+			map.fire('moveend');
+
+		} else {
+
+			var direction = this._lastPos.subtract(this._positions[0]),
+				duration = (this._lastTime + delay - this._times[0]) / 1000,
+
+				speedVector = direction.multiplyBy(0.58 / duration),
+				speed = speedVector.distanceTo(new L.Point(0, 0)),
 
-		if (map.options.maxBounds) {
+				limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
+				limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
+
+				decelerationDuration = limitedSpeed / options.inertiaDeceleration,
+				offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
+
+			var panOptions = {
+				duration: decelerationDuration,
+				easing: 'ease-out'
+			};
+
+			L.Util.requestAnimFrame(L.Util.bind(function () {
+				this._map.panBy(offset, panOptions);
+			}, this));
+		}
+
+		map.fire('dragend');
+
+		if (options.maxBounds) {
 			// TODO predrag validation instead of animation
 			L.Util.requestAnimFrame(this._panInsideMaxBounds, map, true, map._container);
 		}
@@ -79,3 +147,5 @@ L.Map.Drag = L.Handler.extend({
 		this.panInsideBounds(this.options.maxBounds);
 	}
 });
+
+L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);
\ No newline at end of file
diff --git a/src/map/handler/Map.ScrollWheelZoom.js b/src/map/handler/Map.ScrollWheelZoom.js
index 58da770..c0aaf90 100644
--- a/src/map/handler/Map.ScrollWheelZoom.js
+++ b/src/map/handler/Map.ScrollWheelZoom.js
@@ -2,6 +2,10 @@
  * L.Handler.ScrollWheelZoom is used internally by L.Map to enable mouse scroll wheel zooming on the map.
  */
 
+L.Map.mergeOptions({
+	scrollWheelZoom: !L.Browser.touch
+});
+
 L.Map.ScrollWheelZoom = L.Handler.extend({
 	addHooks: function () {
 		L.DomEvent.addListener(this._map._container, 'mousewheel', this._onWheelScroll, this);
@@ -14,6 +18,7 @@ L.Map.ScrollWheelZoom = L.Handler.extend({
 
 	_onWheelScroll: function (e) {
 		var delta = L.DomEvent.getWheelDelta(e);
+
 		this._delta += delta;
 		this._lastMousePos = this._map.mouseEventToContainerPoint(e);
 
@@ -33,9 +38,7 @@ L.Map.ScrollWheelZoom = L.Handler.extend({
 
 		this._delta = 0;
 
-		if (!delta) {
-			return;
-		}
+		if (!delta) { return; }
 
 		var newCenter = this._getCenterForScrollWheelZoom(this._lastMousePos, delta),
 			newZoom = zoom + delta;
@@ -53,3 +56,5 @@ L.Map.ScrollWheelZoom = L.Handler.extend({
 		return map.unproject(newCenterPoint, map._zoom, true);
 	}
 });
+
+L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);
\ No newline at end of file
diff --git a/src/map/handler/Map.TouchZoom.js b/src/map/handler/Map.TouchZoom.js
index daae66e..3147e27 100644
--- a/src/map/handler/Map.TouchZoom.js
+++ b/src/map/handler/Map.TouchZoom.js
@@ -2,6 +2,10 @@
  * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
  */
 
+L.Map.mergeOptions({
+	touchZoom: L.Browser.touch && !L.Browser.android
+});
+
 L.Map.TouchZoom = L.Handler.extend({
 	addHooks: function () {
 		L.DomEvent.addListener(this._map._container, 'touchstart', this._onTouchStart, this);
@@ -12,82 +16,101 @@ L.Map.TouchZoom = L.Handler.extend({
 	},
 
 	_onTouchStart: function (e) {
-		if (!e.touches || e.touches.length !== 2 || this._map._animatingZoom) {
-			return;
-		}
+		var map = this._map;
+
+		if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { 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));
+		var p1 = map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = map.mouseEventToLayerPoint(e.touches[1]),
+			viewCenter = map.containerPointToLayerPoint(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
+			.addListener(document, 'touchmove', this._onTouchMove, this)
+			.addListener(document, 'touchend', this._onTouchEnd, this);
 
 		L.DomEvent.preventDefault(e);
 	},
 
 	_onTouchMove: function (e) {
-		if (!e.touches || e.touches.length !== 2) {
-			return;
-		}
+		if (!e.touches || e.touches.length !== 2) { return; }
+
+		var map = this._map;
+
+		var p1 = map.mouseEventToLayerPoint(e.touches[0]),
+			p2 = map.mouseEventToLayerPoint(e.touches[1]);
+
+		this._scale = p1.distanceTo(p2) / this._startDist;
+		this._delta = p1.add(p2).divideBy(2, true).subtract(this._startCenter);
+
+		if (this._scale === 1) { return; }
+
+		var zoom = this._map._zoom + Math.log(this._scale) / Math.LN2;
+
+		var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
+			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
+			center = this._map.unproject(centerPoint);
 
 		if (!this._moved) {
-			this._map._mapPane.className += ' leaflet-zoom-anim';
+			map._mapPane.className += ' leaflet-zoom-anim leaflet-touching';
 
-			this._map
-				.fire('zoomstart')
+			map
 				.fire('movestart')
+				.fire('zoomstart')
 				._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);
+		map.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
 
 		// 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(" ");
+		map._tileBg.style[L.DomUtil.TRANSFORM] =
+			L.DomUtil.getTranslateString(this._delta) + ' ' +
+            L.DomUtil.getScaleString(this._scale, this._startCenter);
 
 		L.DomEvent.preventDefault(e);
 	},
 
 	_onTouchEnd: function (e) {
-		if (!this._moved || !this._zooming) {
-			return;
-		}
+		if (!this._moved || !this._zooming) { return; }
+
 		this._zooming = false;
+		this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-touching', ''); //TODO toggleClass util
 
-		var oldZoom = this._map.getZoom(),
+		L.DomEvent
+			.removeListener(document, 'touchmove', this._onTouchMove)
+			.removeListener(document, 'touchend', this._onTouchEnd);
+
+		var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale),
+			centerPoint = this._map.getPixelOrigin().add(this._startCenter).add(centerOffset),
+			center = this._map.unproject(centerPoint),
+
+			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);
+			finalScale = Math.pow(2, zoom - oldZoom);
 
-		var finalScale = Math.pow(2, zoomDelta);
+		this._map.fire('zoomanim', {
+			center: center,
+			zoom: zoom
+		});
 
-		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset));
+		this._map._runAnimation(center, zoom, finalScale / this._scale, this._startCenter.add(centerOffset), true);
 	}
 });
+
+L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/leaflet.git



More information about the Pkg-javascript-commits mailing list