]> git.openstreetmap.org Git - rails.git/blob - vendor/assets/leaflet/leaflet.js
Update leaflet.locationfilter
[rails.git] / vendor / assets / leaflet / leaflet.js
1 /*
2  Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin
3  Leaflet is an open-source JavaScript library for mobile-friendly interactive maps.
4  http://leaflet.cloudmade.com
5 */
6 (function (window, undefined) {
7
8 var L, originalL;
9
10 if (typeof exports !== undefined + '') {
11         L = exports;
12 } else {
13         originalL = window.L;
14         L = {};
15
16         L.noConflict = function () {
17                 window.L = originalL;
18                 return this;
19         };
20
21         window.L = L;
22 }
23
24 L.version = '0.4.4';
25
26
27 /*
28  * L.Util is a namespace for various utility functions.
29  */
30
31 L.Util = {
32         extend: function (/*Object*/ dest) /*-> Object*/ {      // merge src properties into dest
33                 var sources = Array.prototype.slice.call(arguments, 1);
34                 for (var j = 0, len = sources.length, src; j < len; j++) {
35                         src = sources[j] || {};
36                         for (var i in src) {
37                                 if (src.hasOwnProperty(i)) {
38                                         dest[i] = src[i];
39                                 }
40                         }
41                 }
42                 return dest;
43         },
44
45         bind: function (fn, obj) { // (Function, Object) -> Function
46                 var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null;
47                 return function () {
48                         return fn.apply(obj, args || arguments);
49                 };
50         },
51
52         stamp: (function () {
53                 var lastId = 0, key = '_leaflet_id';
54                 return function (/*Object*/ obj) {
55                         obj[key] = obj[key] || ++lastId;
56                         return obj[key];
57                 };
58         }()),
59
60         limitExecByInterval: function (fn, time, context) {
61                 var lock, execOnUnlock;
62
63                 return function wrapperFn() {
64                         var args = arguments;
65
66                         if (lock) {
67                                 execOnUnlock = true;
68                                 return;
69                         }
70
71                         lock = true;
72
73                         setTimeout(function () {
74                                 lock = false;
75
76                                 if (execOnUnlock) {
77                                         wrapperFn.apply(context, args);
78                                         execOnUnlock = false;
79                                 }
80                         }, time);
81
82                         fn.apply(context, args);
83                 };
84         },
85
86         falseFn: function () {
87                 return false;
88         },
89
90         formatNum: function (num, digits) {
91                 var pow = Math.pow(10, digits || 5);
92                 return Math.round(num * pow) / pow;
93         },
94
95         splitWords: function (str) {
96                 return str.replace(/^\s+|\s+$/g, '').split(/\s+/);
97         },
98
99         setOptions: function (obj, options) {
100                 obj.options = L.Util.extend({}, obj.options, options);
101                 return obj.options;
102         },
103
104         getParamString: function (obj) {
105                 var params = [];
106                 for (var i in obj) {
107                         if (obj.hasOwnProperty(i)) {
108                                 params.push(i + '=' + obj[i]);
109                         }
110                 }
111                 return '?' + params.join('&');
112         },
113
114         template: function (str, data) {
115                 return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
116                         var value = data[key];
117                         if (!data.hasOwnProperty(key)) {
118                                 throw new Error('No value provided for variable ' + str);
119                         }
120                         return value;
121                 });
122         },
123
124         emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
125 };
126
127 (function () {
128
129         // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/
130
131         function getPrefixed(name) {
132                 var i, fn,
133                         prefixes = ['webkit', 'moz', 'o', 'ms'];
134
135                 for (i = 0; i < prefixes.length && !fn; i++) {
136                         fn = window[prefixes[i] + name];
137                 }
138
139                 return fn;
140         }
141
142         var lastTime = 0;
143
144         function timeoutDefer(fn) {
145                 var time = +new Date(),
146                         timeToCall = Math.max(0, 16 - (time - lastTime));
147
148                 lastTime = time + timeToCall;
149                 return window.setTimeout(fn, timeToCall);
150         }
151
152         var requestFn = window.requestAnimationFrame ||
153                         getPrefixed('RequestAnimationFrame') || timeoutDefer;
154
155         var cancelFn = window.cancelAnimationFrame ||
156                         getPrefixed('CancelAnimationFrame') ||
157                         getPrefixed('CancelRequestAnimationFrame') ||
158                         function (id) {
159                                 window.clearTimeout(id);
160                         };
161
162
163         L.Util.requestAnimFrame = function (fn, context, immediate, element) {
164                 fn = L.Util.bind(fn, context);
165
166                 if (immediate && requestFn === timeoutDefer) {
167                         fn();
168                 } else {
169                         return requestFn.call(window, fn, element);
170                 }
171         };
172
173         L.Util.cancelAnimFrame = function (id) {
174                 if (id) {
175                         cancelFn.call(window, id);
176                 }
177         };
178
179 }());
180
181
182 /*
183  * Class powers the OOP facilities of the library. Thanks to John Resig and Dean Edwards for inspiration!
184  */
185
186 L.Class = function () {};
187
188 L.Class.extend = function (/*Object*/ props) /*-> Class*/ {
189
190         // extended class with the new prototype
191         var NewClass = function () {
192                 if (this.initialize) {
193                         this.initialize.apply(this, arguments);
194                 }
195         };
196
197         // instantiate class without calling constructor
198         var F = function () {};
199         F.prototype = this.prototype;
200
201         var proto = new F();
202         proto.constructor = NewClass;
203
204         NewClass.prototype = proto;
205
206         //inherit parent's statics
207         for (var i in this) {
208                 if (this.hasOwnProperty(i) && i !== 'prototype') {
209                         NewClass[i] = this[i];
210                 }
211         }
212
213         // mix static properties into the class
214         if (props.statics) {
215                 L.Util.extend(NewClass, props.statics);
216                 delete props.statics;
217         }
218
219         // mix includes into the prototype
220         if (props.includes) {
221                 L.Util.extend.apply(null, [proto].concat(props.includes));
222                 delete props.includes;
223         }
224
225         // merge options
226         if (props.options && proto.options) {
227                 props.options = L.Util.extend({}, proto.options, props.options);
228         }
229
230         // mix given properties into the prototype
231         L.Util.extend(proto, props);
232
233         return NewClass;
234 };
235
236
237 // method for adding properties to prototype
238 L.Class.include = function (props) {
239         L.Util.extend(this.prototype, props);
240 };
241
242 L.Class.mergeOptions = function (options) {
243         L.Util.extend(this.prototype.options, options);
244 };
245
246 /*
247  * L.Mixin.Events adds custom events functionality to Leaflet classes
248  */
249
250 var key = '_leaflet_events';
251
252 L.Mixin = {};
253
254 L.Mixin.Events = {
255         
256         addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object])
257                 var events = this[key] = this[key] || {},
258                         type, i, len;
259                 
260                 // Types can be a map of types/handlers
261                 if (typeof types === 'object') {
262                         for (type in types) {
263                                 if (types.hasOwnProperty(type)) {
264                                         this.addEventListener(type, types[type], fn);
265                                 }
266                         }
267                         
268                         return this;
269                 }
270                 
271                 types = L.Util.splitWords(types);
272                 
273                 for (i = 0, len = types.length; i < len; i++) {
274                         events[types[i]] = events[types[i]] || [];
275                         events[types[i]].push({
276                                 action: fn,
277                                 context: context || this
278                         });
279                 }
280                 
281                 return this;
282         },
283
284         hasEventListeners: function (type) { // (String) -> Boolean
285                 return (key in this) && (type in this[key]) && (this[key][type].length > 0);
286         },
287
288         removeEventListener: function (types, fn, context) { // (String[, Function, Object]) or (Object[, Object])
289                 var events = this[key],
290                         type, i, len, listeners, j;
291                 
292                 if (typeof types === 'object') {
293                         for (type in types) {
294                                 if (types.hasOwnProperty(type)) {
295                                         this.removeEventListener(type, types[type], fn);
296                                 }
297                         }
298                         
299                         return this;
300                 }
301                 
302                 types = L.Util.splitWords(types);
303
304                 for (i = 0, len = types.length; i < len; i++) {
305
306                         if (this.hasEventListeners(types[i])) {
307                                 listeners = events[types[i]];
308                                 
309                                 for (j = listeners.length - 1; j >= 0; j--) {
310                                         if (
311                                                 (!fn || listeners[j].action === fn) &&
312                                                 (!context || (listeners[j].context === context))
313                                         ) {
314                                                 listeners.splice(j, 1);
315                                         }
316                                 }
317                         }
318                 }
319                 
320                 return this;
321         },
322
323         fireEvent: function (type, data) { // (String[, Object])
324                 if (!this.hasEventListeners(type)) {
325                         return this;
326                 }
327
328                 var event = L.Util.extend({
329                         type: type,
330                         target: this
331                 }, data);
332
333                 var listeners = this[key][type].slice();
334
335                 for (var i = 0, len = listeners.length; i < len; i++) {
336                         listeners[i].action.call(listeners[i].context || this, event);
337                 }
338
339                 return this;
340         }
341 };
342
343 L.Mixin.Events.on = L.Mixin.Events.addEventListener;
344 L.Mixin.Events.off = L.Mixin.Events.removeEventListener;
345 L.Mixin.Events.fire = L.Mixin.Events.fireEvent;
346
347
348 (function () {
349
350         var ie = !!window.ActiveXObject,
351                 // http://tanalin.com/en/articles/ie-version-js/
352                 ie6 = ie && !window.XMLHttpRequest,
353                 ie7 = ie && !document.querySelector,
354
355                 // terrible browser detection to work around Safari / iOS / Android browser bugs
356                 // see TileLayer._addTile and debug/hacks/jitter.html
357
358                 ua = navigator.userAgent.toLowerCase(),
359                 webkit = ua.indexOf("webkit") !== -1,
360                 chrome = ua.indexOf("chrome") !== -1,
361                 android = ua.indexOf("android") !== -1,
362                 android23 = ua.search("android [23]") !== -1,
363
364                 mobile = typeof orientation !== undefined + '',
365                 msTouch = (window.navigator && window.navigator.msPointerEnabled && window.navigator.msMaxTouchPoints),
366                 retina = (('devicePixelRatio' in window && window.devicePixelRatio > 1) ||
367                                 ('matchMedia' in window && window.matchMedia("(min-resolution:144dpi)").matches)),
368
369                 doc = document.documentElement,
370                 ie3d = ie && ('transition' in doc.style),
371                 webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()),
372                 gecko3d = 'MozPerspective' in doc.style,
373                 opera3d = 'OTransition' in doc.style,
374                 any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d);
375
376
377         var touch = !window.L_NO_TOUCH && (function () {
378
379                 var startName = 'ontouchstart';
380
381                 // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.MsTouch) or WebKit, etc.
382                 if (msTouch || (startName in doc)) {
383                         return true;
384                 }
385
386                 // Firefox/Gecko
387                 var div = document.createElement('div'),
388                         supported = false;
389
390                 if (!div.setAttribute) {
391                         return false;
392                 }
393                 div.setAttribute(startName, 'return;');
394
395                 if (typeof div[startName] === 'function') {
396                         supported = true;
397                 }
398
399                 div.removeAttribute(startName);
400                 div = null;
401
402                 return supported;
403         }());
404
405
406         L.Browser = {
407                 ie6: ie6,
408                 ie7: ie7,
409                 webkit: webkit,
410
411                 android: android,
412                 android23: android23,
413
414                 chrome: chrome,
415
416                 ie3d: ie3d,
417                 webkit3d: webkit3d,
418                 gecko3d: gecko3d,
419                 opera3d: opera3d,
420                 any3d: any3d,
421
422                 mobile: mobile,
423                 mobileWebkit: mobile && webkit,
424                 mobileWebkit3d: mobile && webkit3d,
425                 mobileOpera: mobile && window.opera,
426
427                 touch: touch,
428                 msTouch: msTouch,
429
430                 retina: retina
431         };
432
433 }());
434
435
436 /*
437  * L.Point represents a point with x and y coordinates.
438  */
439
440 L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
441         this.x = (round ? Math.round(x) : x);
442         this.y = (round ? Math.round(y) : y);
443 };
444
445 L.Point.prototype = {
446
447         clone: function () {
448                 return new L.Point(this.x, this.y);
449         },
450
451         // non-destructive, returns a new point
452         add: function (point) {
453                 return this.clone()._add(L.point(point));
454         },
455
456         // destructive, used directly for performance in situations where it's safe to modify existing point
457         _add: function (point) {
458                 this.x += point.x;
459                 this.y += point.y;
460                 return this;
461         },
462
463         subtract: function (point) {
464                 return this.clone()._subtract(L.point(point));
465         },
466
467         _subtract: function (point) {
468                 this.x -= point.x;
469                 this.y -= point.y;
470                 return this;
471         },
472
473         divideBy: function (num) {
474                 return this.clone()._divideBy(num);
475         },
476
477         _divideBy: function (num) {
478                 this.x /= num;
479                 this.y /= num;
480                 return this;
481         },
482
483         multiplyBy: function (num) {
484                 return this.clone()._multiplyBy(num);
485         },
486
487         _multiplyBy: function (num) {
488                 this.x *= num;
489                 this.y *= num;
490                 return this;
491         },
492
493         round: function () {
494                 return this.clone()._round();
495         },
496
497         _round: function () {
498                 this.x = Math.round(this.x);
499                 this.y = Math.round(this.y);
500                 return this;
501         },
502
503         floor: function () {
504                 return this.clone()._floor();
505         },
506
507         _floor: function () {
508                 this.x = Math.floor(this.x);
509                 this.y = Math.floor(this.y);
510                 return this;
511         },
512
513         distanceTo: function (point) {
514                 point = L.point(point);
515
516                 var x = point.x - this.x,
517                         y = point.y - this.y;
518
519                 return Math.sqrt(x * x + y * y);
520         },
521
522         toString: function () {
523                 return 'Point(' +
524                                 L.Util.formatNum(this.x) + ', ' +
525                                 L.Util.formatNum(this.y) + ')';
526         }
527 };
528
529 L.point = function (x, y, round) {
530         if (x instanceof L.Point) {
531                 return x;
532         }
533         if (x instanceof Array) {
534                 return new L.Point(x[0], x[1]);
535         }
536         if (isNaN(x)) {
537                 return x;
538         }
539         return new L.Point(x, y, round);
540 };
541
542
543 /*
544  * L.Bounds represents a rectangular area on the screen in pixel coordinates.
545  */
546
547 L.Bounds = L.Class.extend({
548
549         initialize: function (a, b) {   //(Point, Point) or Point[]
550                 if (!a) { return; }
551
552                 var points = b ? [a, b] : a;
553
554                 for (var i = 0, len = points.length; i < len; i++) {
555                         this.extend(points[i]);
556                 }
557         },
558
559         // extend the bounds to contain the given point
560         extend: function (point) { // (Point)
561                 point = L.point(point);
562
563                 if (!this.min && !this.max) {
564                         this.min = point.clone();
565                         this.max = point.clone();
566                 } else {
567                         this.min.x = Math.min(point.x, this.min.x);
568                         this.max.x = Math.max(point.x, this.max.x);
569                         this.min.y = Math.min(point.y, this.min.y);
570                         this.max.y = Math.max(point.y, this.max.y);
571                 }
572                 return this;
573         },
574
575         getCenter: function (round) { // (Boolean) -> Point
576                 return new L.Point(
577                                 (this.min.x + this.max.x) / 2,
578                                 (this.min.y + this.max.y) / 2, round);
579         },
580
581         getBottomLeft: function () { // -> Point
582                 return new L.Point(this.min.x, this.max.y);
583         },
584
585         getTopRight: function () { // -> Point
586                 return new L.Point(this.max.x, this.min.y);
587         },
588
589         contains: function (obj) { // (Bounds) or (Point) -> Boolean
590                 var min, max;
591
592                 if (typeof obj[0] === 'number' || obj instanceof L.Point) {
593                         obj = L.point(obj);
594                 } else {
595                         obj = L.bounds(obj);
596                 }
597
598                 if (obj instanceof L.Bounds) {
599                         min = obj.min;
600                         max = obj.max;
601                 } else {
602                         min = max = obj;
603                 }
604
605                 return (min.x >= this.min.x) &&
606                                 (max.x <= this.max.x) &&
607                                 (min.y >= this.min.y) &&
608                                 (max.y <= this.max.y);
609         },
610
611         intersects: function (bounds) { // (Bounds) -> Boolean
612                 bounds = L.bounds(bounds);
613
614                 var min = this.min,
615                         max = this.max,
616                         min2 = bounds.min,
617                         max2 = bounds.max;
618
619                 var xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
620                         yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
621
622                 return xIntersects && yIntersects;
623         },
624
625         isValid: function () {
626                 return !!(this.min && this.max);
627         }
628 });
629
630 L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
631         if (!a || a instanceof L.Bounds) {
632                 return a;
633         }
634         return new L.Bounds(a, b);
635 };
636
637
638 /*
639  * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix.
640  */
641
642 L.Transformation = L.Class.extend({
643         initialize: function (/*Number*/ a, /*Number*/ b, /*Number*/ c, /*Number*/ d) {
644                 this._a = a;
645                 this._b = b;
646                 this._c = c;
647                 this._d = d;
648         },
649
650         transform: function (point, scale) {
651                 return this._transform(point.clone(), scale);
652         },
653
654         // destructive transform (faster)
655         _transform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
656                 scale = scale || 1;
657                 point.x = scale * (this._a * point.x + this._b);
658                 point.y = scale * (this._c * point.y + this._d);
659                 return point;
660         },
661
662         untransform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {
663                 scale = scale || 1;
664                 return new L.Point(
665                         (point.x / scale - this._b) / this._a,
666                         (point.y / scale - this._d) / this._c);
667         }
668 });
669
670
671 /*
672  * L.DomUtil contains various utility functions for working with DOM.
673  */
674
675 L.DomUtil = {
676         get: function (id) {
677                 return (typeof id === 'string' ? document.getElementById(id) : id);
678         },
679
680         getStyle: function (el, style) {
681
682                 var value = el.style[style];
683
684                 if (!value && el.currentStyle) {
685                         value = el.currentStyle[style];
686                 }
687
688                 if ((!value || value === 'auto') && document.defaultView) {
689                         var css = document.defaultView.getComputedStyle(el, null);
690                         value = css ? css[style] : null;
691                 }
692
693                 return value === 'auto' ? null : value;
694         },
695
696         getViewportOffset: function (element) {
697
698                 var top = 0,
699                         left = 0,
700                         el = element,
701                         docBody = document.body,
702                         pos,
703                         ie7 = L.Browser.ie7;
704
705                 do {
706                         top  += el.offsetTop  || 0;
707                         left += el.offsetLeft || 0;
708                         pos = L.DomUtil.getStyle(el, 'position');
709
710                         if (el.offsetParent === docBody && pos === 'absolute') { break; }
711
712                         if (pos === 'fixed') {
713                                 top  += docBody.scrollTop  || 0;
714                                 left += docBody.scrollLeft || 0;
715                                 break;
716                         }
717                         el = el.offsetParent;
718
719                 } while (el);
720
721                 el = element;
722
723                 do {
724                         if (el === docBody) { break; }
725
726                         top  -= el.scrollTop  || 0;
727                         left -= el.scrollLeft || 0;
728
729                         //Webkit (and ie <= 7) handles RTL scrollLeft different to everyone else
730                         // https://code.google.com/p/closure-library/source/browse/trunk/closure/goog/style/bidi.js
731                         if (!L.DomUtil.documentIsLtr() && (L.Browser.webkit || ie7)) {
732                                 left += el.scrollWidth - el.clientWidth;
733
734                                 //ie7 shows the scrollbar by default and provides clientWidth counting it, so we need to add it back in if it is visible
735                                 // Scrollbar is on the left as we are RTL
736                                 if (ie7 && L.DomUtil.getStyle(el, 'overflow-y') !== 'hidden' && L.DomUtil.getStyle(el, 'overflow') !== 'hidden') {
737                                         left += 17;
738                                 }
739                         }
740
741                         el = el.parentNode;
742                 } while (el);
743
744                 return new L.Point(left, top);
745         },
746
747         documentIsLtr: function () {
748                 if (!L.DomUtil._docIsLtrCached) {
749                         L.DomUtil._docIsLtrCached = true;
750                         L.DomUtil._docIsLtr = L.DomUtil.getStyle(document.body, 'direction') === "ltr";
751                 }
752                 return L.DomUtil._docIsLtr;
753         },
754
755         create: function (tagName, className, container) {
756
757                 var el = document.createElement(tagName);
758                 el.className = className;
759
760                 if (container) {
761                         container.appendChild(el);
762                 }
763
764                 return el;
765         },
766
767         disableTextSelection: function () {
768                 if (document.selection && document.selection.empty) {
769                         document.selection.empty();
770                 }
771                 if (!this._onselectstart) {
772                         this._onselectstart = document.onselectstart;
773                         document.onselectstart = L.Util.falseFn;
774                 }
775         },
776
777         enableTextSelection: function () {
778                 if (document.onselectstart === L.Util.falseFn) {
779                         document.onselectstart = this._onselectstart;
780                         this._onselectstart = null;
781                 }
782         },
783
784         hasClass: function (el, name) {
785                 return (el.className.length > 0) &&
786                                 new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);
787         },
788
789         addClass: function (el, name) {
790                 if (!L.DomUtil.hasClass(el, name)) {
791                         el.className += (el.className ? ' ' : '') + name;
792                 }
793         },
794
795         removeClass: function (el, name) {
796
797                 function replaceFn(w, match) {
798                         if (match === name) { return ''; }
799                         return w;
800                 }
801
802                 el.className = el.className
803                                 .replace(/(\S+)\s*/g, replaceFn)
804                                 .replace(/(^\s+|\s+$)/, '');
805         },
806
807         setOpacity: function (el, value) {
808
809                 if ('opacity' in el.style) {
810                         el.style.opacity = value;
811
812                 } else if ('filter' in el.style) {
813
814                         var filter = false,
815                                 filterName = 'DXImageTransform.Microsoft.Alpha';
816
817                         // filters collection throws an error if we try to retrieve a filter that doesn't exist
818                         try { filter = el.filters.item(filterName); } catch (e) {}
819
820                         value = Math.round(value * 100);
821
822                         if (filter) {
823                                 filter.Enabled = (value !== 100);
824                                 filter.Opacity = value;
825                         } else {
826                                 el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
827                         }
828                 }
829         },
830
831         testProp: function (props) {
832
833                 var style = document.documentElement.style;
834
835                 for (var i = 0; i < props.length; i++) {
836                         if (props[i] in style) {
837                                 return props[i];
838                         }
839                 }
840                 return false;
841         },
842
843         getTranslateString: function (point) {
844                 // on WebKit browsers (Chrome/Safari/iOS Safari/Android) using translate3d instead of translate
845                 // makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care
846                 // (same speed either way), Opera 12 doesn't support translate3d
847
848                 var is3d = L.Browser.webkit3d,
849                         open = 'translate' + (is3d ? '3d' : '') + '(',
850                         close = (is3d ? ',0' : '') + ')';
851
852                 return open + point.x + 'px,' + point.y + 'px' + close;
853         },
854
855         getScaleString: function (scale, origin) {
856
857                 var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))),
858                     scaleStr = ' scale(' + scale + ') ';
859
860                 return preTranslateStr + scaleStr;
861         },
862
863         setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean])
864
865                 el._leaflet_pos = point;
866
867                 if (!disable3D && L.Browser.any3d) {
868                         el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
869
870                         // workaround for Android 2/3 stability (https://github.com/CloudMade/Leaflet/issues/69)
871                         if (L.Browser.mobileWebkit3d) {
872                                 el.style.WebkitBackfaceVisibility = 'hidden';
873                         }
874                 } else {
875                         el.style.left = point.x + 'px';
876                         el.style.top = point.y + 'px';
877                 }
878         },
879
880         getPosition: function (el) {
881                 // this method is only used for elements previously positioned using setPosition,
882                 // so it's safe to cache the position for performance
883                 return el._leaflet_pos;
884         }
885 };
886
887
888 // prefix style property names
889
890 L.DomUtil.TRANSFORM = L.DomUtil.testProp(
891                 ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
892
893 L.DomUtil.TRANSITION = L.DomUtil.testProp(
894                 ['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']);
895
896 L.DomUtil.TRANSITION_END =
897                 L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ?
898                 L.DomUtil.TRANSITION + 'End' : 'transitionend';
899
900
901 /*
902         CM.LatLng represents a geographical point with latitude and longtitude coordinates.
903 */
904
905 L.LatLng = function (rawLat, rawLng, noWrap) { // (Number, Number[, Boolean])
906         var lat = parseFloat(rawLat),
907                 lng = parseFloat(rawLng);
908
909         if (isNaN(lat) || isNaN(lng)) {
910                 throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')');
911         }
912
913         if (noWrap !== true) {
914                 lat = Math.max(Math.min(lat, 90), -90);                                 // clamp latitude into -90..90
915                 lng = (lng + 180) % 360 + ((lng < -180 || lng === 180) ? 180 : -180);   // wrap longtitude into -180..180
916         }
917
918         this.lat = lat;
919         this.lng = lng;
920 };
921
922 L.Util.extend(L.LatLng, {
923         DEG_TO_RAD: Math.PI / 180,
924         RAD_TO_DEG: 180 / Math.PI,
925         MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check
926 });
927
928 L.LatLng.prototype = {
929         equals: function (obj) { // (LatLng) -> Boolean
930                 if (!obj) { return false; }
931
932                 obj = L.latLng(obj);
933
934                 var margin = Math.max(Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng));
935                 return margin <= L.LatLng.MAX_MARGIN;
936         },
937
938         toString: function (precision) { // -> String
939                 return 'LatLng(' +
940                                 L.Util.formatNum(this.lat, precision) + ', ' +
941                                 L.Util.formatNum(this.lng, precision) + ')';
942         },
943
944         // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula
945         distanceTo: function (other) { // (LatLng) -> Number
946                 other = L.latLng(other);
947
948                 var R = 6378137, // earth radius in meters
949                         d2r = L.LatLng.DEG_TO_RAD,
950                         dLat = (other.lat - this.lat) * d2r,
951                         dLon = (other.lng - this.lng) * d2r,
952                         lat1 = this.lat * d2r,
953                         lat2 = other.lat * d2r,
954                         sin1 = Math.sin(dLat / 2),
955                         sin2 = Math.sin(dLon / 2);
956
957                 var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2);
958
959                 return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
960         }
961 };
962
963 L.latLng = function (a, b, c) { // (LatLng) or ([Number, Number]) or (Number, Number, Boolean)
964         if (a instanceof L.LatLng) {
965                 return a;
966         }
967         if (a instanceof Array) {
968                 return new L.LatLng(a[0], a[1]);
969         }
970         if (isNaN(a)) {
971                 return a;
972         }
973         return new L.LatLng(a, b, c);
974 };
975
976
977
978 /*
979  * L.LatLngBounds represents a rectangular area on the map in geographical coordinates.
980  */
981
982 L.LatLngBounds = L.Class.extend({
983         initialize: function (southWest, northEast) {   // (LatLng, LatLng) or (LatLng[])
984                 if (!southWest) { return; }
985
986                 var latlngs = northEast ? [southWest, northEast] : southWest;
987
988                 for (var i = 0, len = latlngs.length; i < len; i++) {
989                         this.extend(latlngs[i]);
990                 }
991         },
992
993         // extend the bounds to contain the given point or bounds
994         extend: function (obj) { // (LatLng) or (LatLngBounds)
995                 if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {
996                         obj = L.latLng(obj);
997                 } else {
998                         obj = L.latLngBounds(obj);
999                 }
1000
1001                 if (obj instanceof L.LatLng) {
1002                         if (!this._southWest && !this._northEast) {
1003                                 this._southWest = new L.LatLng(obj.lat, obj.lng, true);
1004                                 this._northEast = new L.LatLng(obj.lat, obj.lng, true);
1005                         } else {
1006                                 this._southWest.lat = Math.min(obj.lat, this._southWest.lat);
1007                                 this._southWest.lng = Math.min(obj.lng, this._southWest.lng);
1008
1009                                 this._northEast.lat = Math.max(obj.lat, this._northEast.lat);
1010                                 this._northEast.lng = Math.max(obj.lng, this._northEast.lng);
1011                         }
1012                 } else if (obj instanceof L.LatLngBounds) {
1013                         this.extend(obj._southWest);
1014             this.extend(obj._northEast);
1015                 }
1016                 return this;
1017         },
1018
1019         // extend the bounds by a percentage
1020         pad: function (bufferRatio) { // (Number) -> LatLngBounds
1021                 var sw = this._southWest,
1022                         ne = this._northEast,
1023                         heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
1024                         widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
1025
1026                 return new L.LatLngBounds(
1027                         new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
1028                         new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
1029         },
1030
1031         getCenter: function () { // -> LatLng
1032                 return new L.LatLng(
1033                                 (this._southWest.lat + this._northEast.lat) / 2,
1034                                 (this._southWest.lng + this._northEast.lng) / 2);
1035         },
1036
1037         getSouthWest: function () {
1038                 return this._southWest;
1039         },
1040
1041         getNorthEast: function () {
1042                 return this._northEast;
1043         },
1044
1045         getNorthWest: function () {
1046                 return new L.LatLng(this._northEast.lat, this._southWest.lng, true);
1047         },
1048
1049         getSouthEast: function () {
1050                 return new L.LatLng(this._southWest.lat, this._northEast.lng, true);
1051         },
1052
1053         contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
1054                 if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {
1055                         obj = L.latLng(obj);
1056                 } else {
1057                         obj = L.latLngBounds(obj);
1058                 }
1059
1060                 var sw = this._southWest,
1061                         ne = this._northEast,
1062                         sw2, ne2;
1063
1064                 if (obj instanceof L.LatLngBounds) {
1065                         sw2 = obj.getSouthWest();
1066                         ne2 = obj.getNorthEast();
1067                 } else {
1068                         sw2 = ne2 = obj;
1069                 }
1070
1071                 return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
1072                                 (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
1073         },
1074
1075         intersects: function (bounds) { // (LatLngBounds)
1076                 bounds = L.latLngBounds(bounds);
1077
1078                 var sw = this._southWest,
1079                         ne = this._northEast,
1080                         sw2 = bounds.getSouthWest(),
1081                         ne2 = bounds.getNorthEast();
1082
1083                 var latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
1084                         lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
1085
1086                 return latIntersects && lngIntersects;
1087         },
1088
1089         toBBoxString: function () {
1090                 var sw = this._southWest,
1091                         ne = this._northEast;
1092                 return [sw.lng, sw.lat, ne.lng, ne.lat].join(',');
1093         },
1094
1095         equals: function (bounds) { // (LatLngBounds)
1096                 if (!bounds) { return false; }
1097
1098                 bounds = L.latLngBounds(bounds);
1099
1100                 return this._southWest.equals(bounds.getSouthWest()) &&
1101                        this._northEast.equals(bounds.getNorthEast());
1102         },
1103
1104         isValid: function () {
1105                 return !!(this._southWest && this._northEast);
1106         }
1107 });
1108
1109 //TODO International date line?
1110
1111 L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng)
1112         if (!a || a instanceof L.LatLngBounds) {
1113                 return a;
1114         }
1115         return new L.LatLngBounds(a, b);
1116 };
1117
1118
1119 /*
1120  * L.Projection contains various geographical projections used by CRS classes.
1121  */
1122
1123 L.Projection = {};
1124
1125
1126
1127 L.Projection.SphericalMercator = {
1128         MAX_LATITUDE: 85.0511287798,
1129
1130         project: function (latlng) { // (LatLng) -> Point
1131                 var d = L.LatLng.DEG_TO_RAD,
1132                         max = this.MAX_LATITUDE,
1133                         lat = Math.max(Math.min(max, latlng.lat), -max),
1134                         x = latlng.lng * d,
1135                         y = lat * d;
1136                 y = Math.log(Math.tan((Math.PI / 4) + (y / 2)));
1137
1138                 return new L.Point(x, y);
1139         },
1140
1141         unproject: function (point) { // (Point, Boolean) -> LatLng
1142                 var d = L.LatLng.RAD_TO_DEG,
1143                         lng = point.x * d,
1144                         lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;
1145
1146                 // TODO refactor LatLng wrapping
1147                 return new L.LatLng(lat, lng, true);
1148         }
1149 };
1150
1151
1152
1153 L.Projection.LonLat = {
1154         project: function (latlng) {
1155                 return new L.Point(latlng.lng, latlng.lat);
1156         },
1157
1158         unproject: function (point) {
1159                 return new L.LatLng(point.y, point.x, true);
1160         }
1161 };
1162
1163
1164
1165 L.CRS = {
1166         latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point
1167                 var projectedPoint = this.projection.project(latlng),
1168                     scale = this.scale(zoom);
1169
1170                 return this.transformation._transform(projectedPoint, scale);
1171         },
1172
1173         pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng
1174                 var scale = this.scale(zoom),
1175                     untransformedPoint = this.transformation.untransform(point, scale);
1176
1177                 return this.projection.unproject(untransformedPoint);
1178         },
1179
1180         project: function (latlng) {
1181                 return this.projection.project(latlng);
1182         },
1183
1184         scale: function (zoom) {
1185                 return 256 * Math.pow(2, zoom);
1186         }
1187 };
1188
1189
1190
1191 L.CRS.Simple = L.Util.extend({}, L.CRS, {
1192         projection: L.Projection.LonLat,
1193         transformation: new L.Transformation(1, 0, 1, 0)
1194 });
1195
1196
1197
1198 L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, {
1199         code: 'EPSG:3857',
1200
1201         projection: L.Projection.SphericalMercator,
1202         transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5),
1203
1204         project: function (latlng) { // (LatLng) -> Point
1205                 var projectedPoint = this.projection.project(latlng),
1206                         earthRadius = 6378137;
1207                 return projectedPoint.multiplyBy(earthRadius);
1208         }
1209 });
1210
1211 L.CRS.EPSG900913 = L.Util.extend({}, L.CRS.EPSG3857, {
1212         code: 'EPSG:900913'
1213 });
1214
1215
1216
1217 L.CRS.EPSG4326 = L.Util.extend({}, L.CRS, {
1218         code: 'EPSG:4326',
1219
1220         projection: L.Projection.LonLat,
1221         transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5)
1222 });
1223
1224
1225 /*
1226  * L.Map is the central class of the API - it is used to create a map.
1227  */
1228
1229 L.Map = L.Class.extend({
1230
1231         includes: L.Mixin.Events,
1232
1233         options: {
1234                 crs: L.CRS.EPSG3857,
1235
1236                 /*
1237                 center: LatLng,
1238                 zoom: Number,
1239                 layers: Array,
1240                 */
1241
1242                 fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23,
1243                 trackResize: true,
1244                 markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d
1245         },
1246
1247         initialize: function (id, options) { // (HTMLElement or String, Object)
1248                 options = L.Util.setOptions(this, options);
1249
1250                 this._initContainer(id);
1251                 this._initLayout();
1252                 this._initHooks();
1253                 this._initEvents();
1254
1255                 if (options.maxBounds) {
1256                         this.setMaxBounds(options.maxBounds);
1257                 }
1258
1259                 if (options.center && options.zoom !== undefined) {
1260                         this.setView(L.latLng(options.center), options.zoom, true);
1261                 }
1262
1263                 this._initLayers(options.layers);
1264         },
1265
1266
1267         // public methods that modify map state
1268
1269         // replaced by animation-powered implementation in Map.PanAnimation.js
1270         setView: function (center, zoom) {
1271                 this._resetView(L.latLng(center), this._limitZoom(zoom));
1272                 return this;
1273         },
1274
1275         setZoom: function (zoom) { // (Number)
1276                 return this.setView(this.getCenter(), zoom);
1277         },
1278
1279         zoomIn: function (delta) {
1280                 return this.setZoom(this._zoom + (delta || 1));
1281         },
1282
1283         zoomOut: function (delta) {
1284                 return this.setZoom(this._zoom - (delta || 1));
1285         },
1286
1287         fitBounds: function (bounds) { // (LatLngBounds)
1288                 var zoom = this.getBoundsZoom(bounds);
1289                 return this.setView(L.latLngBounds(bounds).getCenter(), zoom);
1290         },
1291
1292         fitWorld: function () {
1293                 var sw = new L.LatLng(-60, -170),
1294                     ne = new L.LatLng(85, 179);
1295
1296                 return this.fitBounds(new L.LatLngBounds(sw, ne));
1297         },
1298
1299         panTo: function (center) { // (LatLng)
1300                 return this.setView(center, this._zoom);
1301         },
1302
1303         panBy: function (offset) { // (Point)
1304                 // replaced with animated panBy in Map.Animation.js
1305                 this.fire('movestart');
1306
1307                 this._rawPanBy(L.point(offset));
1308
1309                 this.fire('move');
1310                 return this.fire('moveend');
1311         },
1312
1313         setMaxBounds: function (bounds) {
1314                 bounds = L.latLngBounds(bounds);
1315
1316                 this.options.maxBounds = bounds;
1317
1318                 if (!bounds) {
1319                         this._boundsMinZoom = null;
1320                         return this;
1321                 }
1322
1323                 var minZoom = this.getBoundsZoom(bounds, true);
1324
1325                 this._boundsMinZoom = minZoom;
1326
1327                 if (this._loaded) {
1328                         if (this._zoom < minZoom) {
1329                                 this.setView(bounds.getCenter(), minZoom);
1330                         } else {
1331                                 this.panInsideBounds(bounds);
1332                         }
1333                 }
1334
1335                 return this;
1336         },
1337
1338         panInsideBounds: function (bounds) {
1339                 bounds = L.latLngBounds(bounds);
1340
1341                 var viewBounds = this.getBounds(),
1342                     viewSw = this.project(viewBounds.getSouthWest()),
1343                     viewNe = this.project(viewBounds.getNorthEast()),
1344                     sw = this.project(bounds.getSouthWest()),
1345                     ne = this.project(bounds.getNorthEast()),
1346                     dx = 0,
1347                     dy = 0;
1348
1349                 if (viewNe.y < ne.y) { // north
1350                         dy = ne.y - viewNe.y;
1351                 }
1352                 if (viewNe.x > ne.x) { // east
1353                         dx = ne.x - viewNe.x;
1354                 }
1355                 if (viewSw.y > sw.y) { // south
1356                         dy = sw.y - viewSw.y;
1357                 }
1358                 if (viewSw.x < sw.x) { // west
1359                         dx = sw.x - viewSw.x;
1360                 }
1361
1362                 return this.panBy(new L.Point(dx, dy, true));
1363         },
1364
1365         addLayer: function (layer) {
1366                 // TODO method is too big, refactor
1367
1368                 var id = L.Util.stamp(layer);
1369
1370                 if (this._layers[id]) { return this; }
1371
1372                 this._layers[id] = layer;
1373
1374                 // TODO getMaxZoom, getMinZoom in ILayer (instead of options)
1375                 if (layer.options && !isNaN(layer.options.maxZoom)) {
1376                         this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);
1377                 }
1378                 if (layer.options && !isNaN(layer.options.minZoom)) {
1379                         this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);
1380                 }
1381
1382                 // TODO looks ugly, refactor!!!
1383                 if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
1384                         this._tileLayersNum++;
1385             this._tileLayersToLoad++;
1386             layer.on('load', this._onTileLayerLoad, this);
1387                 }
1388
1389                 this.whenReady(function () {
1390                         layer.onAdd(this);
1391                         this.fire('layeradd', {layer: layer});
1392                 }, this);
1393
1394                 return this;
1395         },
1396
1397         removeLayer: function (layer) {
1398                 var id = L.Util.stamp(layer);
1399
1400                 if (!this._layers[id]) { return; }
1401
1402                 layer.onRemove(this);
1403
1404                 delete this._layers[id];
1405
1406                 // TODO looks ugly, refactor
1407                 if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
1408                         this._tileLayersNum--;
1409             this._tileLayersToLoad--;
1410             layer.off('load', this._onTileLayerLoad, this);
1411                 }
1412
1413                 return this.fire('layerremove', {layer: layer});
1414         },
1415
1416         hasLayer: function (layer) {
1417                 var id = L.Util.stamp(layer);
1418                 return this._layers.hasOwnProperty(id);
1419         },
1420
1421         invalidateSize: function (animate) {
1422                 var oldSize = this.getSize();
1423
1424                 this._sizeChanged = true;
1425
1426                 if (this.options.maxBounds) {
1427                         this.setMaxBounds(this.options.maxBounds);
1428                 }
1429
1430                 if (!this._loaded) { return this; }
1431
1432                 var offset = oldSize._subtract(this.getSize())._divideBy(2)._round();
1433
1434                 if (animate === true) {
1435                         this.panBy(offset);
1436                 } else {
1437                         this._rawPanBy(offset);
1438
1439                         this.fire('move');
1440
1441                         clearTimeout(this._sizeTimer);
1442                         this._sizeTimer = setTimeout(L.Util.bind(this.fire, this, 'moveend'), 200);
1443                 }
1444                 return this;
1445         },
1446
1447         // TODO handler.addTo
1448         addHandler: function (name, HandlerClass) {
1449                 if (!HandlerClass) { return; }
1450
1451                 this[name] = new HandlerClass(this);
1452
1453                 if (this.options[name]) {
1454                         this[name].enable();
1455                 }
1456
1457                 return this;
1458         },
1459
1460
1461         // public methods for getting map state
1462
1463         getCenter: function () { // (Boolean) -> LatLng
1464                 return this.layerPointToLatLng(this._getCenterLayerPoint());
1465         },
1466
1467         getZoom: function () {
1468                 return this._zoom;
1469         },
1470
1471         getBounds: function () {
1472                 var bounds = this.getPixelBounds(),
1473                     sw = this.unproject(bounds.getBottomLeft()),
1474                     ne = this.unproject(bounds.getTopRight());
1475
1476                 return new L.LatLngBounds(sw, ne);
1477         },
1478
1479         getMinZoom: function () {
1480                 var z1 = this.options.minZoom || 0,
1481                     z2 = this._layersMinZoom || 0,
1482                     z3 = this._boundsMinZoom || 0;
1483
1484                 return Math.max(z1, z2, z3);
1485         },
1486
1487         getMaxZoom: function () {
1488                 var z1 = this.options.maxZoom === undefined ? Infinity : this.options.maxZoom,
1489                     z2 = this._layersMaxZoom  === undefined ? Infinity : this._layersMaxZoom;
1490
1491                 return Math.min(z1, z2);
1492         },
1493
1494         getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number
1495                 bounds = L.latLngBounds(bounds);
1496
1497                 var size = this.getSize(),
1498                     zoom = this.options.minZoom || 0,
1499                     maxZoom = this.getMaxZoom(),
1500                     ne = bounds.getNorthEast(),
1501                     sw = bounds.getSouthWest(),
1502                     boundsSize,
1503                     nePoint,
1504                     swPoint,
1505                     zoomNotFound = true;
1506
1507                 if (inside) {
1508                         zoom--;
1509                 }
1510
1511                 do {
1512                         zoom++;
1513                         nePoint = this.project(ne, zoom);
1514                         swPoint = this.project(sw, zoom);
1515                         boundsSize = new L.Point(Math.abs(nePoint.x - swPoint.x), Math.abs(swPoint.y - nePoint.y));
1516
1517                         if (!inside) {
1518                                 zoomNotFound = boundsSize.x <= size.x && boundsSize.y <= size.y;
1519                         } else {
1520                                 zoomNotFound = boundsSize.x < size.x || boundsSize.y < size.y;
1521                         }
1522                 } while (zoomNotFound && zoom <= maxZoom);
1523
1524                 if (zoomNotFound && inside) {
1525                         return null;
1526                 }
1527
1528                 return inside ? zoom : zoom - 1;
1529         },
1530
1531         getSize: function () {
1532                 if (!this._size || this._sizeChanged) {
1533                         this._size = new L.Point(
1534                                 this._container.clientWidth,
1535                                 this._container.clientHeight);
1536
1537                         this._sizeChanged = false;
1538                 }
1539                 return this._size.clone();
1540         },
1541
1542         getPixelBounds: function () {
1543                 var topLeftPoint = this._getTopLeftPoint();
1544                 return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
1545         },
1546
1547         getPixelOrigin: function () {
1548                 return this._initialTopLeftPoint;
1549         },
1550
1551         getPanes: function () {
1552                 return this._panes;
1553         },
1554
1555         getContainer: function () {
1556                 return this._container;
1557         },
1558
1559
1560         // TODO replace with universal implementation after refactoring projections
1561
1562         getZoomScale: function (toZoom) {
1563                 var crs = this.options.crs;
1564                 return crs.scale(toZoom) / crs.scale(this._zoom);
1565         },
1566
1567         getScaleZoom: function (scale) {
1568                 return this._zoom + (Math.log(scale) / Math.LN2);
1569         },
1570
1571
1572         // conversion methods
1573
1574         project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
1575                 zoom = zoom === undefined ? this._zoom : zoom;
1576                 return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);
1577         },
1578
1579         unproject: function (point, zoom) { // (Point[, Number]) -> LatLng
1580                 zoom = zoom === undefined ? this._zoom : zoom;
1581                 return this.options.crs.pointToLatLng(L.point(point), zoom);
1582         },
1583
1584         layerPointToLatLng: function (point) { // (Point)
1585                 var projectedPoint = L.point(point).add(this._initialTopLeftPoint);
1586                 return this.unproject(projectedPoint);
1587         },
1588
1589         latLngToLayerPoint: function (latlng) { // (LatLng)
1590                 var projectedPoint = this.project(L.latLng(latlng))._round();
1591                 return projectedPoint._subtract(this._initialTopLeftPoint);
1592         },
1593
1594         containerPointToLayerPoint: function (point) { // (Point)
1595                 return L.point(point).subtract(this._getMapPanePos());
1596         },
1597
1598         layerPointToContainerPoint: function (point) { // (Point)
1599                 return L.point(point).add(this._getMapPanePos());
1600         },
1601
1602         containerPointToLatLng: function (point) {
1603                 var layerPoint = this.containerPointToLayerPoint(L.point(point));
1604                 return this.layerPointToLatLng(layerPoint);
1605         },
1606
1607         latLngToContainerPoint: function (latlng) {
1608                 return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng)));
1609         },
1610
1611         mouseEventToContainerPoint: function (e) { // (MouseEvent)
1612                 return L.DomEvent.getMousePosition(e, this._container);
1613         },
1614
1615         mouseEventToLayerPoint: function (e) { // (MouseEvent)
1616                 return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
1617         },
1618
1619         mouseEventToLatLng: function (e) { // (MouseEvent)
1620                 return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
1621         },
1622
1623
1624         // map initialization methods
1625
1626         _initContainer: function (id) {
1627                 var container = this._container = L.DomUtil.get(id);
1628
1629                 if (container._leaflet) {
1630                         throw new Error("Map container is already initialized.");
1631                 }
1632
1633                 container._leaflet = true;
1634         },
1635
1636         _initLayout: function () {
1637                 var container = this._container;
1638
1639                 container.innerHTML = '';
1640                 L.DomUtil.addClass(container, 'leaflet-container');
1641
1642                 if (L.Browser.touch) {
1643                         L.DomUtil.addClass(container, 'leaflet-touch');
1644                 }
1645
1646                 if (this.options.fadeAnimation) {
1647                         L.DomUtil.addClass(container, 'leaflet-fade-anim');
1648                 }
1649
1650                 var position = L.DomUtil.getStyle(container, 'position');
1651
1652                 if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
1653                         container.style.position = 'relative';
1654                 }
1655
1656                 this._initPanes();
1657
1658                 if (this._initControlPos) {
1659                         this._initControlPos();
1660                 }
1661         },
1662
1663         _initPanes: function () {
1664                 var panes = this._panes = {};
1665
1666                 this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);
1667
1668                 this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);
1669                 this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);
1670
1671                 panes.shadowPane = this._createPane('leaflet-shadow-pane');
1672                 panes.overlayPane = this._createPane('leaflet-overlay-pane');
1673                 panes.markerPane = this._createPane('leaflet-marker-pane');
1674                 panes.popupPane = this._createPane('leaflet-popup-pane');
1675
1676                 var zoomHide = ' leaflet-zoom-hide';
1677
1678                 if (!this.options.markerZoomAnimation) {
1679                         L.DomUtil.addClass(panes.markerPane, zoomHide);
1680                         L.DomUtil.addClass(panes.shadowPane, zoomHide);
1681                         L.DomUtil.addClass(panes.popupPane, zoomHide);
1682                 }
1683         },
1684
1685         _createPane: function (className, container) {
1686                 return L.DomUtil.create('div', className, container || this._objectsPane);
1687         },
1688
1689         _initializers: [],
1690
1691         _initHooks: function () {
1692                 var i, len;
1693                 for (i = 0, len = this._initializers.length; i < len; i++) {
1694                         this._initializers[i].call(this);
1695                 }
1696         },
1697
1698         _initLayers: function (layers) {
1699                 layers = layers ? (layers instanceof Array ? layers : [layers]) : [];
1700
1701                 this._layers = {};
1702                 this._tileLayersNum = 0;
1703
1704                 var i, len;
1705
1706                 for (i = 0, len = layers.length; i < len; i++) {
1707                         this.addLayer(layers[i]);
1708                 }
1709         },
1710
1711
1712         // private methods that modify map state
1713
1714         _resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {
1715
1716                 var zoomChanged = (this._zoom !== zoom);
1717
1718                 if (!afterZoomAnim) {
1719                         this.fire('movestart');
1720
1721                         if (zoomChanged) {
1722                                 this.fire('zoomstart');
1723                         }
1724                 }
1725
1726                 this._zoom = zoom;
1727
1728                 this._initialTopLeftPoint = this._getNewTopLeftPoint(center);
1729
1730                 if (!preserveMapOffset) {
1731                         L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
1732                 } else {
1733                         this._initialTopLeftPoint._add(this._getMapPanePos());
1734                 }
1735
1736                 this._tileLayersToLoad = this._tileLayersNum;
1737
1738                 var loading = !this._loaded;
1739                 this._loaded = true;
1740
1741                 this.fire('viewreset', {hard: !preserveMapOffset});
1742
1743                 this.fire('move');
1744
1745                 if (zoomChanged || afterZoomAnim) {
1746                         this.fire('zoomend');
1747                 }
1748
1749                 this.fire('moveend', {hard: !preserveMapOffset});
1750
1751                 if (loading) {
1752                         this.fire('load');
1753                 }
1754         },
1755
1756         _rawPanBy: function (offset) {
1757                 L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
1758         },
1759
1760
1761         // map events
1762
1763         _initEvents: function () {
1764                 if (!L.DomEvent) { return; }
1765
1766                 L.DomEvent.on(this._container, 'click', this._onMouseClick, this);
1767
1768                 var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu'],
1769                         i, len;
1770
1771                 for (i = 0, len = events.length; i < len; i++) {
1772                         L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this);
1773                 }
1774
1775                 if (this.options.trackResize) {
1776                         L.DomEvent.on(window, 'resize', this._onResize, this);
1777                 }
1778         },
1779
1780         _onResize: function () {
1781                 L.Util.cancelAnimFrame(this._resizeRequest);
1782                 this._resizeRequest = L.Util.requestAnimFrame(this.invalidateSize, this, false, this._container);
1783         },
1784
1785         _onMouseClick: function (e) {
1786                 if (!this._loaded || (this.dragging && this.dragging.moved())) { return; }
1787
1788                 this.fire('preclick');
1789                 this._fireMouseEvent(e);
1790         },
1791
1792         _fireMouseEvent: function (e) {
1793                 if (!this._loaded) { return; }
1794
1795                 var type = e.type;
1796
1797                 type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
1798
1799                 if (!this.hasEventListeners(type)) { return; }
1800
1801                 if (type === 'contextmenu') {
1802                         L.DomEvent.preventDefault(e);
1803                 }
1804
1805                 var containerPoint = this.mouseEventToContainerPoint(e),
1806                         layerPoint = this.containerPointToLayerPoint(containerPoint),
1807                         latlng = this.layerPointToLatLng(layerPoint);
1808
1809                 this.fire(type, {
1810                         latlng: latlng,
1811                         layerPoint: layerPoint,
1812                         containerPoint: containerPoint,
1813                         originalEvent: e
1814                 });
1815         },
1816
1817         _onTileLayerLoad: function () {
1818                 // TODO super-ugly, refactor!!!
1819                 // clear scaled tiles after all new tiles are loaded (for performance)
1820                 this._tileLayersToLoad--;
1821                 if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {
1822                         clearTimeout(this._clearTileBgTimer);
1823                         this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500);
1824                 }
1825         },
1826
1827         whenReady: function (callback, context) {
1828                 if (this._loaded) {
1829                         callback.call(context || this, this);
1830                 } else {
1831                         this.on('load', callback, context);
1832                 }
1833                 return this;
1834         },
1835
1836
1837         // private methods for getting map state
1838
1839         _getMapPanePos: function () {
1840                 return L.DomUtil.getPosition(this._mapPane);
1841         },
1842
1843         _getTopLeftPoint: function () {
1844                 if (!this._loaded) {
1845                         throw new Error('Set map center and zoom first.');
1846                 }
1847
1848                 return this._initialTopLeftPoint.subtract(this._getMapPanePos());
1849         },
1850
1851         _getNewTopLeftPoint: function (center, zoom) {
1852                 var viewHalf = this.getSize()._divideBy(2);
1853                 // TODO round on display, not calculation to increase precision?
1854                 return this.project(center, zoom)._subtract(viewHalf)._round();
1855         },
1856
1857         _latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {
1858                 var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos());
1859                 return this.project(latlng, newZoom)._subtract(topLeft);
1860         },
1861
1862         _getCenterLayerPoint: function () {
1863                 return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
1864         },
1865
1866         _getCenterOffset: function (center) {
1867                 return this.latLngToLayerPoint(center).subtract(this._getCenterLayerPoint());
1868         },
1869
1870         _limitZoom: function (zoom) {
1871                 var min = this.getMinZoom(),
1872                         max = this.getMaxZoom();
1873
1874                 return Math.max(min, Math.min(max, zoom));
1875         }
1876 });
1877
1878 L.Map.addInitHook = function (fn) {
1879         var args = Array.prototype.slice.call(arguments, 1);
1880
1881         var init = typeof fn === 'function' ? fn : function () {
1882                 this[fn].apply(this, args);
1883         };
1884
1885         this.prototype._initializers.push(init);
1886 };
1887
1888 L.map = function (id, options) {
1889         return new L.Map(id, options);
1890 };
1891
1892
1893
1894 L.Projection.Mercator = {
1895         MAX_LATITUDE: 85.0840591556,
1896
1897         R_MINOR: 6356752.3142,
1898         R_MAJOR: 6378137,
1899
1900         project: function (latlng) { // (LatLng) -> Point
1901                 var d = L.LatLng.DEG_TO_RAD,
1902                         max = this.MAX_LATITUDE,
1903                         lat = Math.max(Math.min(max, latlng.lat), -max),
1904                         r = this.R_MAJOR,
1905                         r2 = this.R_MINOR,
1906                         x = latlng.lng * d * r,
1907                         y = lat * d,
1908                         tmp = r2 / r,
1909                         eccent = Math.sqrt(1.0 - tmp * tmp),
1910                         con = eccent * Math.sin(y);
1911
1912                 con = Math.pow((1 - con) / (1 + con), eccent * 0.5);
1913
1914                 var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con;
1915                 y = -r2 * Math.log(ts);
1916
1917                 return new L.Point(x, y);
1918         },
1919
1920         unproject: function (point) { // (Point, Boolean) -> LatLng
1921                 var d = L.LatLng.RAD_TO_DEG,
1922                         r = this.R_MAJOR,
1923                         r2 = this.R_MINOR,
1924                         lng = point.x * d / r,
1925                         tmp = r2 / r,
1926                         eccent = Math.sqrt(1 - (tmp * tmp)),
1927                         ts = Math.exp(- point.y / r2),
1928                         phi = (Math.PI / 2) - 2 * Math.atan(ts),
1929                         numIter = 15,
1930                         tol = 1e-7,
1931                         i = numIter,
1932                         dphi = 0.1,
1933                         con;
1934
1935                 while ((Math.abs(dphi) > tol) && (--i > 0)) {
1936                         con = eccent * Math.sin(phi);
1937                         dphi = (Math.PI / 2) - 2 * Math.atan(ts * Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi;
1938                         phi += dphi;
1939                 }
1940
1941                 return new L.LatLng(phi * d, lng, true);
1942         }
1943 };
1944
1945
1946
1947 L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, {
1948         code: 'EPSG:3395',
1949
1950         projection: L.Projection.Mercator,
1951
1952         transformation: (function () {
1953                 var m = L.Projection.Mercator,
1954                         r = m.R_MAJOR,
1955                         r2 = m.R_MINOR;
1956
1957                 return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 0.5);
1958         }())
1959 });
1960
1961
1962 /*
1963  * L.TileLayer is used for standard xyz-numbered tile layers.
1964  */
1965
1966 L.TileLayer = L.Class.extend({
1967         includes: L.Mixin.Events,
1968
1969         options: {
1970                 minZoom: 0,
1971                 maxZoom: 18,
1972                 tileSize: 256,
1973                 subdomains: 'abc',
1974                 errorTileUrl: '',
1975                 attribution: '',
1976                 zoomOffset: 0,
1977                 opacity: 1,
1978                 /* (undefined works too)
1979                 zIndex: null,
1980                 tms: false,
1981                 continuousWorld: false,
1982                 noWrap: false,
1983                 zoomReverse: false,
1984                 detectRetina: false,
1985                 reuseTiles: false,
1986                 */
1987                 unloadInvisibleTiles: L.Browser.mobile,
1988                 updateWhenIdle: L.Browser.mobile
1989         },
1990
1991         initialize: function (url, options) {
1992                 options = L.Util.setOptions(this, options);
1993
1994                 // detecting retina displays, adjusting tileSize and zoom levels
1995                 if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
1996
1997                         options.tileSize = Math.floor(options.tileSize / 2);
1998                         options.zoomOffset++;
1999
2000                         if (options.minZoom > 0) {
2001                                 options.minZoom--;
2002                         }
2003                         this.options.maxZoom--;
2004                 }
2005
2006                 this._url = url;
2007
2008                 var subdomains = this.options.subdomains;
2009
2010                 if (typeof subdomains === 'string') {
2011                         this.options.subdomains = subdomains.split('');
2012                 }
2013         },
2014
2015         onAdd: function (map) {
2016                 this._map = map;
2017
2018                 // create a container div for tiles
2019                 this._initContainer();
2020
2021                 // create an image to clone for tiles
2022                 this._createTileProto();
2023
2024                 // set up events
2025                 map.on({
2026                         'viewreset': this._resetCallback,
2027                         'moveend': this._update
2028                 }, this);
2029
2030                 if (!this.options.updateWhenIdle) {
2031                         this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
2032                         map.on('move', this._limitedUpdate, this);
2033                 }
2034
2035                 this._reset();
2036                 this._update();
2037         },
2038
2039         addTo: function (map) {
2040                 map.addLayer(this);
2041                 return this;
2042         },
2043
2044         onRemove: function (map) {
2045                 map._panes.tilePane.removeChild(this._container);
2046
2047                 map.off({
2048                         'viewreset': this._resetCallback,
2049                         'moveend': this._update
2050                 }, this);
2051
2052                 if (!this.options.updateWhenIdle) {
2053                         map.off('move', this._limitedUpdate, this);
2054                 }
2055
2056                 this._container = null;
2057                 this._map = null;
2058         },
2059
2060         bringToFront: function () {
2061                 var pane = this._map._panes.tilePane;
2062
2063                 if (this._container) {
2064                         pane.appendChild(this._container);
2065                         this._setAutoZIndex(pane, Math.max);
2066                 }
2067
2068                 return this;
2069         },
2070
2071         bringToBack: function () {
2072                 var pane = this._map._panes.tilePane;
2073
2074                 if (this._container) {
2075                         pane.insertBefore(this._container, pane.firstChild);
2076                         this._setAutoZIndex(pane, Math.min);
2077                 }
2078
2079                 return this;
2080         },
2081
2082         getAttribution: function () {
2083                 return this.options.attribution;
2084         },
2085
2086         setOpacity: function (opacity) {
2087                 this.options.opacity = opacity;
2088
2089                 if (this._map) {
2090                         this._updateOpacity();
2091                 }
2092
2093                 return this;
2094         },
2095
2096         setZIndex: function (zIndex) {
2097                 this.options.zIndex = zIndex;
2098                 this._updateZIndex();
2099
2100                 return this;
2101         },
2102
2103         setUrl: function (url, noRedraw) {
2104                 this._url = url;
2105
2106                 if (!noRedraw) {
2107                         this.redraw();
2108                 }
2109
2110                 return this;
2111         },
2112
2113         redraw: function () {
2114                 if (this._map) {
2115                         this._map._panes.tilePane.empty = false;
2116                         this._reset(true);
2117                         this._update();
2118                 }
2119                 return this;
2120         },
2121
2122         _updateZIndex: function () {
2123                 if (this._container && this.options.zIndex !== undefined) {
2124                         this._container.style.zIndex = this.options.zIndex;
2125                 }
2126         },
2127
2128         _setAutoZIndex: function (pane, compare) {
2129
2130                 var layers = pane.getElementsByClassName('leaflet-layer'),
2131                         edgeZIndex = -compare(Infinity, -Infinity), // -Ifinity for max, Infinity for min
2132                         zIndex;
2133
2134                 for (var i = 0, len = layers.length; i < len; i++) {
2135
2136                         if (layers[i] !== this._container) {
2137                                 zIndex = parseInt(layers[i].style.zIndex, 10);
2138
2139                                 if (!isNaN(zIndex)) {
2140                                         edgeZIndex = compare(edgeZIndex, zIndex);
2141                                 }
2142                         }
2143                 }
2144
2145                 this.options.zIndex = this._container.style.zIndex = (isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1);
2146         },
2147
2148         _updateOpacity: function () {
2149                 L.DomUtil.setOpacity(this._container, this.options.opacity);
2150
2151                 // stupid webkit hack to force redrawing of tiles
2152                 var i,
2153                         tiles = this._tiles;
2154
2155                 if (L.Browser.webkit) {
2156                         for (i in tiles) {
2157                                 if (tiles.hasOwnProperty(i)) {
2158                                         tiles[i].style.webkitTransform += ' translate(0,0)';
2159                                 }
2160                         }
2161                 }
2162         },
2163
2164         _initContainer: function () {
2165                 var tilePane = this._map._panes.tilePane;
2166
2167                 if (!this._container || tilePane.empty) {
2168                         this._container = L.DomUtil.create('div', 'leaflet-layer');
2169
2170                         this._updateZIndex();
2171
2172                         tilePane.appendChild(this._container);
2173
2174                         if (this.options.opacity < 1) {
2175                                 this._updateOpacity();
2176                         }
2177                 }
2178         },
2179
2180         _resetCallback: function (e) {
2181                 this._reset(e.hard);
2182         },
2183
2184         _reset: function (clearOldContainer) {
2185                 var key,
2186                         tiles = this._tiles;
2187
2188                 for (key in tiles) {
2189                         if (tiles.hasOwnProperty(key)) {
2190                                 this.fire('tileunload', {tile: tiles[key]});
2191                         }
2192                 }
2193
2194                 this._tiles = {};
2195                 this._tilesToLoad = 0;
2196
2197                 if (this.options.reuseTiles) {
2198                         this._unusedTiles = [];
2199                 }
2200
2201                 if (clearOldContainer && this._container) {
2202                         this._container.innerHTML = "";
2203                 }
2204
2205                 this._initContainer();
2206         },
2207
2208         _update: function (e) {
2209
2210                 if (!this._map) { return; }
2211
2212                 var bounds   = this._map.getPixelBounds(),
2213                     zoom     = this._map.getZoom(),
2214                     tileSize = this.options.tileSize;
2215
2216                 if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
2217                         return;
2218                 }
2219
2220                 var nwTilePoint = new L.Point(
2221                                 Math.floor(bounds.min.x / tileSize),
2222                                 Math.floor(bounds.min.y / tileSize)),
2223                         seTilePoint = new L.Point(
2224                                 Math.floor(bounds.max.x / tileSize),
2225                                 Math.floor(bounds.max.y / tileSize)),
2226                         tileBounds = new L.Bounds(nwTilePoint, seTilePoint);
2227
2228                 this._addTilesFromCenterOut(tileBounds);
2229
2230                 if (this.options.unloadInvisibleTiles || this.options.reuseTiles) {
2231                         this._removeOtherTiles(tileBounds);
2232                 }
2233         },
2234
2235         _addTilesFromCenterOut: function (bounds) {
2236                 var queue = [],
2237                         center = bounds.getCenter();
2238
2239                 var j, i, point;
2240
2241                 for (j = bounds.min.y; j <= bounds.max.y; j++) {
2242                         for (i = bounds.min.x; i <= bounds.max.x; i++) {
2243                                 point = new L.Point(i, j);
2244
2245                                 if (this._tileShouldBeLoaded(point)) {
2246                                         queue.push(point);
2247                                 }
2248                         }
2249                 }
2250
2251                 var tilesToLoad = queue.length;
2252
2253                 if (tilesToLoad === 0) { return; }
2254
2255                 // load tiles in order of their distance to center
2256                 queue.sort(function (a, b) {
2257                         return a.distanceTo(center) - b.distanceTo(center);
2258                 });
2259
2260                 var fragment = document.createDocumentFragment();
2261
2262                 // if its the first batch of tiles to load
2263                 if (!this._tilesToLoad) {
2264                         this.fire('loading');
2265                 }
2266
2267                 this._tilesToLoad += tilesToLoad;
2268
2269                 for (i = 0; i < tilesToLoad; i++) {
2270                         this._addTile(queue[i], fragment);
2271                 }
2272
2273                 this._container.appendChild(fragment);
2274         },
2275
2276         _tileShouldBeLoaded: function (tilePoint) {
2277                 if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) {
2278                         return false; // already loaded
2279                 }
2280
2281                 if (!this.options.continuousWorld) {
2282                         var limit = this._getWrapTileNum();
2283
2284                         if (this.options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit) ||
2285                                                         tilePoint.y < 0 || tilePoint.y >= limit) {
2286                                 return false; // exceeds world bounds
2287                         }
2288                 }
2289
2290                 return true;
2291         },
2292
2293         _removeOtherTiles: function (bounds) {
2294                 var kArr, x, y, key;
2295
2296                 for (key in this._tiles) {
2297                         if (this._tiles.hasOwnProperty(key)) {
2298                                 kArr = key.split(':');
2299                                 x = parseInt(kArr[0], 10);
2300                                 y = parseInt(kArr[1], 10);
2301
2302                                 // remove tile if it's out of bounds
2303                                 if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) {
2304                                         this._removeTile(key);
2305                                 }
2306                         }
2307                 }
2308         },
2309
2310         _removeTile: function (key) {
2311                 var tile = this._tiles[key];
2312
2313                 this.fire("tileunload", {tile: tile, url: tile.src});
2314
2315                 if (this.options.reuseTiles) {
2316                         L.DomUtil.removeClass(tile, 'leaflet-tile-loaded');
2317                         this._unusedTiles.push(tile);
2318                 } else if (tile.parentNode === this._container) {
2319                         this._container.removeChild(tile);
2320                 }
2321
2322                 if (!L.Browser.android) { //For https://github.com/CloudMade/Leaflet/issues/137
2323                         tile.src = L.Util.emptyImageUrl;
2324                 }
2325
2326                 delete this._tiles[key];
2327         },
2328
2329         _addTile: function (tilePoint, container) {
2330                 var tilePos = this._getTilePos(tilePoint);
2331
2332                 // get unused tile - or create a new tile
2333                 var tile = this._getTile();
2334
2335                 // Chrome 20 layouts much faster with top/left (Verify with timeline, frames)
2336                 // android 4 browser has display issues with top/left and requires transform instead
2337                 // android 3 browser not tested
2338                 // android 2 browser requires top/left or tiles disappear on load or first drag (reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866
2339                 // (other browsers don't currently care) - see debug/hacks/jitter.html for an example
2340                 L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23);
2341
2342                 this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
2343
2344                 this._loadTile(tile, tilePoint);
2345
2346                 if (tile.parentNode !== this._container) {
2347                         container.appendChild(tile);
2348                 }
2349         },
2350
2351         _getZoomForUrl: function () {
2352
2353                 var options = this.options,
2354                         zoom = this._map.getZoom();
2355
2356                 if (options.zoomReverse) {
2357                         zoom = options.maxZoom - zoom;
2358                 }
2359
2360                 return zoom + options.zoomOffset;
2361         },
2362
2363         _getTilePos: function (tilePoint) {
2364                 var origin = this._map.getPixelOrigin(),
2365                         tileSize = this.options.tileSize;
2366
2367                 return tilePoint.multiplyBy(tileSize).subtract(origin);
2368         },
2369
2370         // image-specific code (override to implement e.g. Canvas or SVG tile layer)
2371
2372         getTileUrl: function (tilePoint) {
2373                 this._adjustTilePoint(tilePoint);
2374
2375                 return L.Util.template(this._url, L.Util.extend({
2376                         s: this._getSubdomain(tilePoint),
2377                         z: this._getZoomForUrl(),
2378                         x: tilePoint.x,
2379                         y: tilePoint.y
2380                 }, this.options));
2381         },
2382
2383         _getWrapTileNum: function () {
2384                 // TODO refactor, limit is not valid for non-standard projections
2385                 return Math.pow(2, this._getZoomForUrl());
2386         },
2387
2388         _adjustTilePoint: function (tilePoint) {
2389
2390                 var limit = this._getWrapTileNum();
2391
2392                 // wrap tile coordinates
2393                 if (!this.options.continuousWorld && !this.options.noWrap) {
2394                         tilePoint.x = ((tilePoint.x % limit) + limit) % limit;
2395                 }
2396
2397                 if (this.options.tms) {
2398                         tilePoint.y = limit - tilePoint.y - 1;
2399                 }
2400         },
2401
2402         _getSubdomain: function (tilePoint) {
2403                 var index = (tilePoint.x + tilePoint.y) % this.options.subdomains.length;
2404                 return this.options.subdomains[index];
2405         },
2406
2407         _createTileProto: function () {
2408                 var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile');
2409                 img.galleryimg = 'no';
2410
2411                 var tileSize = this.options.tileSize;
2412                 img.style.width = tileSize + 'px';
2413                 img.style.height = tileSize + 'px';
2414         },
2415
2416         _getTile: function () {
2417                 if (this.options.reuseTiles && this._unusedTiles.length > 0) {
2418                         var tile = this._unusedTiles.pop();
2419                         this._resetTile(tile);
2420                         return tile;
2421                 }
2422                 return this._createTile();
2423         },
2424
2425         _resetTile: function (tile) {
2426                 // Override if data stored on a tile needs to be cleaned up before reuse
2427         },
2428
2429         _createTile: function () {
2430                 var tile = this._tileImg.cloneNode(false);
2431                 tile.onselectstart = tile.onmousemove = L.Util.falseFn;
2432                 return tile;
2433         },
2434
2435         _loadTile: function (tile, tilePoint) {
2436                 tile._layer  = this;
2437                 tile.onload  = this._tileOnLoad;
2438                 tile.onerror = this._tileOnError;
2439
2440                 tile.src     = this.getTileUrl(tilePoint);
2441         },
2442
2443     _tileLoaded: function () {
2444         this._tilesToLoad--;
2445         if (!this._tilesToLoad) {
2446             this.fire('load');
2447         }
2448     },
2449
2450         _tileOnLoad: function (e) {
2451                 var layer = this._layer;
2452
2453                 //Only if we are loading an actual image
2454                 if (this.src !== L.Util.emptyImageUrl) {
2455                         L.DomUtil.addClass(this, 'leaflet-tile-loaded');
2456
2457                         layer.fire('tileload', {
2458                                 tile: this,
2459                                 url: this.src
2460                         });
2461                 }
2462
2463                 layer._tileLoaded();
2464         },
2465
2466         _tileOnError: function (e) {
2467                 var layer = this._layer;
2468
2469                 layer.fire('tileerror', {
2470                         tile: this,
2471                         url: this.src
2472                 });
2473
2474                 var newUrl = layer.options.errorTileUrl;
2475                 if (newUrl) {
2476                         this.src = newUrl;
2477                 }
2478
2479         layer._tileLoaded();
2480     }
2481 });
2482
2483 L.tileLayer = function (url, options) {
2484         return new L.TileLayer(url, options);
2485 };
2486
2487
2488 L.TileLayer.WMS = L.TileLayer.extend({
2489
2490         defaultWmsParams: {
2491                 service: 'WMS',
2492                 request: 'GetMap',
2493                 version: '1.1.1',
2494                 layers: '',
2495                 styles: '',
2496                 format: 'image/jpeg',
2497                 transparent: false
2498         },
2499
2500         initialize: function (url, options) { // (String, Object)
2501
2502                 this._url = url;
2503
2504                 var wmsParams = L.Util.extend({}, this.defaultWmsParams);
2505
2506                 if (options.detectRetina && L.Browser.retina) {
2507                         wmsParams.width = wmsParams.height = this.options.tileSize * 2;
2508                 } else {
2509                         wmsParams.width = wmsParams.height = this.options.tileSize;
2510                 }
2511
2512                 for (var i in options) {
2513                         // all keys that are not TileLayer options go to WMS params
2514                         if (!this.options.hasOwnProperty(i)) {
2515                                 wmsParams[i] = options[i];
2516                         }
2517                 }
2518
2519                 this.wmsParams = wmsParams;
2520
2521                 L.Util.setOptions(this, options);
2522         },
2523
2524         onAdd: function (map) {
2525
2526                 var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs';
2527                 this.wmsParams[projectionKey] = map.options.crs.code;
2528
2529                 L.TileLayer.prototype.onAdd.call(this, map);
2530         },
2531
2532         getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String
2533
2534                 var map = this._map,
2535                         crs = map.options.crs,
2536                         tileSize = this.options.tileSize,
2537
2538                         nwPoint = tilePoint.multiplyBy(tileSize),
2539                         sePoint = nwPoint.add(new L.Point(tileSize, tileSize)),
2540
2541                         nw = crs.project(map.unproject(nwPoint, zoom)),
2542                         se = crs.project(map.unproject(sePoint, zoom)),
2543
2544                         bbox = [nw.x, se.y, se.x, nw.y].join(','),
2545
2546                         url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)});
2547
2548                 return url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox;
2549         },
2550
2551         setParams: function (params, noRedraw) {
2552
2553                 L.Util.extend(this.wmsParams, params);
2554
2555                 if (!noRedraw) {
2556                         this.redraw();
2557                 }
2558
2559                 return this;
2560         }
2561 });
2562
2563 L.tileLayer.wms = function (url, options) {
2564         return new L.TileLayer.WMS(url, options);
2565 };
2566
2567
2568 L.TileLayer.Canvas = L.TileLayer.extend({
2569         options: {
2570                 async: false
2571         },
2572
2573         initialize: function (options) {
2574                 L.Util.setOptions(this, options);
2575         },
2576
2577         redraw: function () {
2578                 var i,
2579                         tiles = this._tiles;
2580
2581                 for (i in tiles) {
2582                         if (tiles.hasOwnProperty(i)) {
2583                                 this._redrawTile(tiles[i]);
2584                         }
2585                 }
2586         },
2587
2588         _redrawTile: function (tile) {
2589                 this.drawTile(tile, tile._tilePoint, this._map._zoom);
2590         },
2591
2592         _createTileProto: function () {
2593                 var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');
2594
2595                 var tileSize = this.options.tileSize;
2596                 proto.width = tileSize;
2597                 proto.height = tileSize;
2598         },
2599
2600         _createTile: function () {
2601                 var tile = this._canvasProto.cloneNode(false);
2602                 tile.onselectstart = tile.onmousemove = L.Util.falseFn;
2603                 return tile;
2604         },
2605
2606         _loadTile: function (tile, tilePoint) {
2607                 tile._layer = this;
2608                 tile._tilePoint = tilePoint;
2609                 
2610                 this._redrawTile(tile);
2611
2612                 if (!this.options.async) {
2613                         this.tileDrawn(tile);
2614                 }
2615         },
2616
2617         drawTile: function (tile, tilePoint) {
2618                 // override with rendering code
2619         },
2620
2621         tileDrawn: function (tile) {
2622                 this._tileOnLoad.call(tile);
2623         }
2624 });
2625
2626
2627 L.tileLayer.canvas = function (options) {
2628         return new L.TileLayer.Canvas(options);
2629 };
2630
2631 L.ImageOverlay = L.Class.extend({
2632         includes: L.Mixin.Events,
2633
2634         options: {
2635                 opacity: 1
2636         },
2637
2638         initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
2639                 this._url = url;
2640                 this._bounds = L.latLngBounds(bounds);
2641
2642                 L.Util.setOptions(this, options);
2643         },
2644
2645         onAdd: function (map) {
2646                 this._map = map;
2647
2648                 if (!this._image) {
2649                         this._initImage();
2650                 }
2651
2652                 map._panes.overlayPane.appendChild(this._image);
2653
2654                 map.on('viewreset', this._reset, this);
2655
2656                 if (map.options.zoomAnimation && L.Browser.any3d) {
2657                         map.on('zoomanim', this._animateZoom, this);
2658                 }
2659
2660                 this._reset();
2661         },
2662
2663         onRemove: function (map) {
2664                 map.getPanes().overlayPane.removeChild(this._image);
2665
2666                 map.off('viewreset', this._reset, this);
2667
2668                 if (map.options.zoomAnimation) {
2669                         map.off('zoomanim', this._animateZoom, this);
2670                 }
2671         },
2672
2673         addTo: function (map) {
2674                 map.addLayer(this);
2675                 return this;
2676         },
2677
2678         setOpacity: function (opacity) {
2679                 this.options.opacity = opacity;
2680                 this._updateOpacity();
2681                 return this;
2682         },
2683
2684         // TODO remove bringToFront/bringToBack duplication from TileLayer/Path
2685         bringToFront: function () {
2686                 if (this._image) {
2687                         this._map._panes.overlayPane.appendChild(this._image);
2688                 }
2689                 return this;
2690         },
2691
2692         bringToBack: function () {
2693                 var pane = this._map._panes.overlayPane;
2694                 if (this._image) {
2695                         pane.insertBefore(this._image, pane.firstChild);
2696                 }
2697                 return this;
2698         },
2699
2700         _initImage: function () {
2701                 this._image = L.DomUtil.create('img', 'leaflet-image-layer');
2702
2703                 if (this._map.options.zoomAnimation && L.Browser.any3d) {
2704                         L.DomUtil.addClass(this._image, 'leaflet-zoom-animated');
2705                 } else {
2706                         L.DomUtil.addClass(this._image, 'leaflet-zoom-hide');
2707                 }
2708
2709                 this._updateOpacity();
2710
2711                 //TODO createImage util method to remove duplication
2712                 L.Util.extend(this._image, {
2713                         galleryimg: 'no',
2714                         onselectstart: L.Util.falseFn,
2715                         onmousemove: L.Util.falseFn,
2716                         onload: L.Util.bind(this._onImageLoad, this),
2717                         src: this._url
2718                 });
2719         },
2720
2721         _animateZoom: function (e) {
2722                 var map = this._map,
2723                         image = this._image,
2724                     scale = map.getZoomScale(e.zoom),
2725                     nw = this._bounds.getNorthWest(),
2726                     se = this._bounds.getSouthEast(),
2727
2728                     topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center),
2729                     size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft),
2730                     currentSize = map.latLngToLayerPoint(se)._subtract(map.latLngToLayerPoint(nw)),
2731                     origin = topLeft._add(size._subtract(currentSize)._divideBy(2));
2732
2733                 image.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(origin) + ' scale(' + scale + ') ';
2734         },
2735
2736         _reset: function () {
2737                 var image   = this._image,
2738                     topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
2739                     size    = this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(topLeft);
2740
2741                 L.DomUtil.setPosition(image, topLeft);
2742
2743                 image.style.width  = size.x + 'px';
2744                 image.style.height = size.y + 'px';
2745         },
2746
2747         _onImageLoad: function () {
2748                 this.fire('load');
2749         },
2750
2751         _updateOpacity: function () {
2752                 L.DomUtil.setOpacity(this._image, this.options.opacity);
2753         }
2754 });
2755
2756 L.imageOverlay = function (url, bounds, options) {
2757         return new L.ImageOverlay(url, bounds, options);
2758 };
2759
2760
2761 L.Icon = L.Class.extend({
2762         options: {
2763                 /*
2764                 iconUrl: (String) (required)
2765                 iconSize: (Point) (can be set through CSS)
2766                 iconAnchor: (Point) (centered by default if size is specified, can be set in CSS with negative margins)
2767                 popupAnchor: (Point) (if not specified, popup opens in the anchor point)
2768                 shadowUrl: (Point) (no shadow by default)
2769                 shadowSize: (Point)
2770                 shadowAnchor: (Point)
2771                 */
2772                 className: ''
2773         },
2774
2775         initialize: function (options) {
2776                 L.Util.setOptions(this, options);
2777         },
2778
2779         createIcon: function () {
2780                 return this._createIcon('icon');
2781         },
2782
2783         createShadow: function () {
2784                 return this._createIcon('shadow');
2785         },
2786
2787         _createIcon: function (name) {
2788                 var src = this._getIconUrl(name);
2789
2790                 if (!src) {
2791                         if (name === 'icon') {
2792                                 throw new Error("iconUrl not set in Icon options (see the docs).");
2793                         }
2794                         return null;
2795                 }
2796
2797                 var img = this._createImg(src);
2798                 this._setIconStyles(img, name);
2799
2800                 return img;
2801         },
2802
2803         _setIconStyles: function (img, name) {
2804                 var options = this.options,
2805                         size = L.point(options[name + 'Size']),
2806                         anchor;
2807
2808                 if (name === 'shadow') {
2809                         anchor = L.point(options.shadowAnchor || options.iconAnchor);
2810                 } else {
2811                         anchor = L.point(options.iconAnchor);
2812                 }
2813
2814                 if (!anchor && size) {
2815                         anchor = size.divideBy(2, true);
2816                 }
2817
2818                 img.className = 'leaflet-marker-' + name + ' ' + options.className;
2819
2820                 if (anchor) {
2821                         img.style.marginLeft = (-anchor.x) + 'px';
2822                         img.style.marginTop  = (-anchor.y) + 'px';
2823                 }
2824
2825                 if (size) {
2826                         img.style.width  = size.x + 'px';
2827                         img.style.height = size.y + 'px';
2828                 }
2829         },
2830
2831         _createImg: function (src) {
2832                 var el;
2833
2834                 if (!L.Browser.ie6) {
2835                         el = document.createElement('img');
2836                         el.src = src;
2837                 } else {
2838                         el = document.createElement('div');
2839                         el.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + src + '")';
2840                 }
2841                 return el;
2842         },
2843
2844         _getIconUrl: function (name) {
2845                 return this.options[name + 'Url'];
2846         }
2847 });
2848
2849 L.icon = function (options) {
2850         return new L.Icon(options);
2851 };
2852
2853
2854
2855 L.Icon.Default = L.Icon.extend({
2856
2857         options: {
2858                 iconSize: new L.Point(25, 41),
2859                 iconAnchor: new L.Point(12, 41),
2860                 popupAnchor: new L.Point(1, -34),
2861
2862                 shadowSize: new L.Point(41, 41)
2863         },
2864
2865         _getIconUrl: function (name) {
2866                 var key = name + 'Url';
2867
2868                 if (this.options[key]) {
2869                         return this.options[key];
2870                 }
2871
2872                 var path = L.Icon.Default.imagePath;
2873
2874                 if (!path) {
2875                         throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually.");
2876                 }
2877
2878                 return path + '/marker-' + name + '.png';
2879         }
2880 });
2881
2882 L.Icon.Default.imagePath = (function () {
2883         var scripts = document.getElementsByTagName('script'),
2884             leafletRe = /\/?leaflet[\-\._]?([\w\-\._]*)\.js\??/;
2885
2886         var i, len, src, matches;
2887
2888         for (i = 0, len = scripts.length; i < len; i++) {
2889                 src = scripts[i].src;
2890                 matches = src.match(leafletRe);
2891
2892                 if (matches) {
2893                         return src.split(leafletRe)[0] + '/images';
2894                 }
2895         }
2896 }());
2897
2898
2899 /*
2900  * L.Marker is used to display clickable/draggable icons on the map.
2901  */
2902
2903 L.Marker = L.Class.extend({
2904
2905         includes: L.Mixin.Events,
2906
2907         options: {
2908                 icon: new L.Icon.Default(),
2909                 title: '',
2910                 clickable: true,
2911                 draggable: false,
2912                 zIndexOffset: 0,
2913                 opacity: 1,
2914                 riseOnHover: false,
2915                 riseOffset: 250
2916         },
2917
2918         initialize: function (latlng, options) {
2919                 L.Util.setOptions(this, options);
2920                 this._latlng = L.latLng(latlng);
2921         },
2922
2923         onAdd: function (map) {
2924                 this._map = map;
2925
2926                 map.on('viewreset', this.update, this);
2927
2928                 this._initIcon();
2929                 this.update();
2930
2931                 if (map.options.zoomAnimation && map.options.markerZoomAnimation) {
2932                         map.on('zoomanim', this._animateZoom, this);
2933                 }
2934         },
2935
2936         addTo: function (map) {
2937                 map.addLayer(this);
2938                 return this;
2939         },
2940
2941         onRemove: function (map) {
2942                 this._removeIcon();
2943
2944                 this.fire('remove');
2945
2946                 map.off({
2947                         'viewreset': this.update,
2948                         'zoomanim': this._animateZoom
2949                 }, this);
2950
2951                 this._map = null;
2952         },
2953
2954         getLatLng: function () {
2955                 return this._latlng;
2956         },
2957
2958         setLatLng: function (latlng) {
2959                 this._latlng = L.latLng(latlng);
2960
2961                 this.update();
2962
2963                 this.fire('move', { latlng: this._latlng });
2964         },
2965
2966         setZIndexOffset: function (offset) {
2967                 this.options.zIndexOffset = offset;
2968                 this.update();
2969         },
2970
2971         setIcon: function (icon) {
2972                 if (this._map) {
2973                         this._removeIcon();
2974                 }
2975
2976                 this.options.icon = icon;
2977
2978                 if (this._map) {
2979                         this._initIcon();
2980                         this.update();
2981                 }
2982         },
2983
2984         update: function () {
2985                 if (!this._icon) { return; }
2986
2987                 var pos = this._map.latLngToLayerPoint(this._latlng).round();
2988                 this._setPos(pos);
2989         },
2990
2991         _initIcon: function () {
2992                 var options = this.options,
2993                     map = this._map,
2994                     animation = (map.options.zoomAnimation && map.options.markerZoomAnimation),
2995                     classToAdd = animation ? 'leaflet-zoom-animated' : 'leaflet-zoom-hide',
2996                     needOpacityUpdate = false;
2997
2998                 if (!this._icon) {
2999                         this._icon = options.icon.createIcon();
3000
3001                         if (options.title) {
3002                                 this._icon.title = options.title;
3003                         }
3004
3005                         this._initInteraction();
3006                         needOpacityUpdate = (this.options.opacity < 1);
3007
3008                         L.DomUtil.addClass(this._icon, classToAdd);
3009
3010                         if (options.riseOnHover) {
3011                                 L.DomEvent
3012                                         .on(this._icon, 'mouseover', this._bringToFront, this)
3013                                         .on(this._icon, 'mouseout', this._resetZIndex, this);
3014                         }
3015                 }
3016
3017                 if (!this._shadow) {
3018                         this._shadow = options.icon.createShadow();
3019
3020                         if (this._shadow) {
3021                                 L.DomUtil.addClass(this._shadow, classToAdd);
3022                                 needOpacityUpdate = (this.options.opacity < 1);
3023                         }
3024                 }
3025
3026                 if (needOpacityUpdate) {
3027                         this._updateOpacity();
3028                 }
3029
3030                 var panes = this._map._panes;
3031
3032                 panes.markerPane.appendChild(this._icon);
3033
3034                 if (this._shadow) {
3035                         panes.shadowPane.appendChild(this._shadow);
3036                 }
3037         },
3038
3039         _removeIcon: function () {
3040                 var panes = this._map._panes;
3041
3042                 if (this.options.riseOnHover) {
3043                         L.DomEvent
3044                                 .off(this._icon, 'mouseover', this._bringToFront)
3045                                 .off(this._icon, 'mouseout', this._resetZIndex);
3046                 }
3047
3048                 panes.markerPane.removeChild(this._icon);
3049
3050                 if (this._shadow) {
3051                         panes.shadowPane.removeChild(this._shadow);
3052                 }
3053
3054                 this._icon = this._shadow = null;
3055         },
3056
3057         _setPos: function (pos) {
3058                 L.DomUtil.setPosition(this._icon, pos);
3059
3060                 if (this._shadow) {
3061                         L.DomUtil.setPosition(this._shadow, pos);
3062                 }
3063
3064                 this._zIndex = pos.y + this.options.zIndexOffset;
3065
3066                 this._resetZIndex();
3067         },
3068
3069         _updateZIndex: function (offset) {
3070                 this._icon.style.zIndex = this._zIndex + offset;
3071         },
3072
3073         _animateZoom: function (opt) {
3074                 var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
3075
3076                 this._setPos(pos);
3077         },
3078
3079         _initInteraction: function () {
3080                 if (!this.options.clickable) {
3081                         return;
3082                 }
3083
3084                 var icon = this._icon,
3085                         events = ['dblclick', 'mousedown', 'mouseover', 'mouseout'];
3086
3087                 L.DomUtil.addClass(icon, 'leaflet-clickable');
3088                 L.DomEvent.on(icon, 'click', this._onMouseClick, this);
3089
3090                 for (var i = 0; i < events.length; i++) {
3091                         L.DomEvent.on(icon, events[i], this._fireMouseEvent, this);
3092                 }
3093
3094                 if (L.Handler.MarkerDrag) {
3095                         this.dragging = new L.Handler.MarkerDrag(this);
3096
3097                         if (this.options.draggable) {
3098                                 this.dragging.enable();
3099                         }
3100                 }
3101         },
3102
3103         _onMouseClick: function (e) {
3104                 if (this.hasEventListeners(e.type)) {
3105                         L.DomEvent.stopPropagation(e);
3106                 }
3107                 if (this.dragging && this.dragging.moved()) { return; }
3108                 if (this._map.dragging && this._map.dragging.moved()) { return; }
3109                 this.fire(e.type, {
3110                         originalEvent: e
3111                 });
3112         },
3113
3114         _fireMouseEvent: function (e) {
3115                 this.fire(e.type, {
3116                         originalEvent: e
3117                 });
3118                 if (e.type !== 'mousedown') {
3119                         L.DomEvent.stopPropagation(e);
3120                 }
3121         },
3122
3123         setOpacity: function (opacity) {
3124                 this.options.opacity = opacity;
3125                 if (this._map) {
3126                         this._updateOpacity();
3127                 }
3128         },
3129
3130         _updateOpacity: function () {
3131                 L.DomUtil.setOpacity(this._icon, this.options.opacity);
3132                 if (this._shadow) {
3133                         L.DomUtil.setOpacity(this._shadow, this.options.opacity);
3134                 }
3135         },
3136
3137         _bringToFront: function () {
3138                 this._updateZIndex(this.options.riseOffset);
3139         },
3140
3141         _resetZIndex: function () {
3142                 this._updateZIndex(0);
3143         }
3144 });
3145
3146 L.marker = function (latlng, options) {
3147         return new L.Marker(latlng, options);
3148 };
3149
3150
3151 L.DivIcon = L.Icon.extend({
3152         options: {
3153                 iconSize: new L.Point(12, 12), // also can be set through CSS
3154                 /*
3155                 iconAnchor: (Point)
3156                 popupAnchor: (Point)
3157                 html: (String)
3158                 bgPos: (Point)
3159                 */
3160                 className: 'leaflet-div-icon'
3161         },
3162
3163         createIcon: function () {
3164                 var div = document.createElement('div'),
3165                     options = this.options;
3166
3167                 if (options.html) {
3168                         div.innerHTML = options.html;
3169                 }
3170
3171                 if (options.bgPos) {
3172                         div.style.backgroundPosition =
3173                                         (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px';
3174                 }
3175
3176                 this._setIconStyles(div, 'icon');
3177                 return div;
3178         },
3179
3180         createShadow: function () {
3181                 return null;
3182         }
3183 });
3184
3185 L.divIcon = function (options) {
3186         return new L.DivIcon(options);
3187 };
3188
3189
3190
3191 L.Map.mergeOptions({
3192         closePopupOnClick: true
3193 });
3194
3195 L.Popup = L.Class.extend({
3196         includes: L.Mixin.Events,
3197
3198         options: {
3199                 minWidth: 50,
3200                 maxWidth: 300,
3201                 maxHeight: null,
3202                 autoPan: true,
3203                 closeButton: true,
3204                 offset: new L.Point(0, 6),
3205                 autoPanPadding: new L.Point(5, 5),
3206                 className: ''
3207         },
3208
3209         initialize: function (options, source) {
3210                 L.Util.setOptions(this, options);
3211
3212                 this._source = source;
3213         },
3214
3215         onAdd: function (map) {
3216                 this._map = map;
3217
3218                 if (!this._container) {
3219                         this._initLayout();
3220                 }
3221                 this._updateContent();
3222
3223                 var animFade = map.options.fadeAnimation;
3224
3225                 if (animFade) {
3226                         L.DomUtil.setOpacity(this._container, 0);
3227                 }
3228                 map._panes.popupPane.appendChild(this._container);
3229
3230                 map.on('viewreset', this._updatePosition, this);
3231
3232                 if (L.Browser.any3d) {
3233                         map.on('zoomanim', this._zoomAnimation, this);
3234                 }
3235
3236                 if (map.options.closePopupOnClick) {
3237                         map.on('preclick', this._close, this);
3238                 }
3239
3240                 this._update();
3241
3242                 if (animFade) {
3243                         L.DomUtil.setOpacity(this._container, 1);
3244                 }
3245         },
3246
3247         addTo: function (map) {
3248                 map.addLayer(this);
3249                 return this;
3250         },
3251
3252         openOn: function (map) {
3253                 map.openPopup(this);
3254                 return this;
3255         },
3256
3257         onRemove: function (map) {
3258                 map._panes.popupPane.removeChild(this._container);
3259
3260                 L.Util.falseFn(this._container.offsetWidth); // force reflow
3261
3262                 map.off({
3263                         viewreset: this._updatePosition,
3264                         preclick: this._close,
3265                         zoomanim: this._zoomAnimation
3266                 }, this);
3267
3268                 if (map.options.fadeAnimation) {
3269                         L.DomUtil.setOpacity(this._container, 0);
3270                 }
3271
3272                 this._map = null;
3273         },
3274
3275         setLatLng: function (latlng) {
3276                 this._latlng = L.latLng(latlng);
3277                 this._update();
3278                 return this;
3279         },
3280
3281         setContent: function (content) {
3282                 this._content = content;
3283                 this._update();
3284                 return this;
3285         },
3286
3287         _close: function () {
3288                 var map = this._map;
3289
3290                 if (map) {
3291                         map._popup = null;
3292
3293                         map
3294                                 .removeLayer(this)
3295                                 .fire('popupclose', {popup: this});
3296                 }
3297         },
3298
3299         _initLayout: function () {
3300                 var prefix = 'leaflet-popup',
3301                         container = this._container = L.DomUtil.create('div', prefix + ' ' + this.options.className + ' leaflet-zoom-animated'),
3302                         closeButton;
3303
3304                 if (this.options.closeButton) {
3305                         closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container);
3306                         closeButton.href = '#close';
3307                         closeButton.innerHTML = '&#215;';
3308
3309                         L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this);
3310                 }
3311
3312                 var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container);
3313                 L.DomEvent.disableClickPropagation(wrapper);
3314
3315                 this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
3316                 L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation);
3317
3318                 this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
3319                 this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
3320         },
3321
3322         _update: function () {
3323                 if (!this._map) { return; }
3324
3325                 this._container.style.visibility = 'hidden';
3326
3327                 this._updateContent();
3328                 this._updateLayout();
3329                 this._updatePosition();
3330
3331                 this._container.style.visibility = '';
3332
3333                 this._adjustPan();
3334         },
3335
3336         _updateContent: function () {
3337                 if (!this._content) { return; }
3338
3339                 if (typeof this._content === 'string') {
3340                         this._contentNode.innerHTML = this._content;
3341                 } else {
3342                         while (this._contentNode.hasChildNodes()) {
3343                                 this._contentNode.removeChild(this._contentNode.firstChild);
3344                         }
3345                         this._contentNode.appendChild(this._content);
3346                 }
3347                 this.fire('contentupdate');
3348         },
3349
3350         _updateLayout: function () {
3351                 var container = this._contentNode,
3352                         style = container.style;
3353
3354                 style.width = '';
3355                 style.whiteSpace = 'nowrap';
3356
3357                 var width = container.offsetWidth;
3358                 width = Math.min(width, this.options.maxWidth);
3359                 width = Math.max(width, this.options.minWidth);
3360
3361                 style.width = (width + 1) + 'px';
3362                 style.whiteSpace = '';
3363
3364                 style.height = '';
3365
3366                 var height = container.offsetHeight,
3367                         maxHeight = this.options.maxHeight,
3368                         scrolledClass = 'leaflet-popup-scrolled';
3369
3370                 if (maxHeight && height > maxHeight) {
3371                         style.height = maxHeight + 'px';
3372                         L.DomUtil.addClass(container, scrolledClass);
3373                 } else {
3374                         L.DomUtil.removeClass(container, scrolledClass);
3375                 }
3376
3377                 this._containerWidth = this._container.offsetWidth;
3378         },
3379
3380         _updatePosition: function () {
3381                 if (!this._map) { return; }
3382
3383                 var pos = this._map.latLngToLayerPoint(this._latlng),
3384                         is3d = L.Browser.any3d,
3385                         offset = this.options.offset;
3386
3387                 if (is3d) {
3388                         L.DomUtil.setPosition(this._container, pos);
3389                 }
3390
3391                 this._containerBottom = -offset.y - (is3d ? 0 : pos.y);
3392                 this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (is3d ? 0 : pos.x);
3393
3394                 //Bottom position the popup in case the height of the popup changes (images loading etc)
3395                 this._container.style.bottom = this._containerBottom + 'px';
3396                 this._container.style.left = this._containerLeft + 'px';
3397         },
3398
3399         _zoomAnimation: function (opt) {
3400                 var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
3401
3402                 L.DomUtil.setPosition(this._container, pos);
3403         },
3404
3405         _adjustPan: function () {
3406                 if (!this.options.autoPan) { return; }
3407
3408                 var map = this._map,
3409                         containerHeight = this._container.offsetHeight,
3410                         containerWidth = this._containerWidth,
3411
3412                         layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
3413
3414                 if (L.Browser.any3d) {
3415                         layerPos._add(L.DomUtil.getPosition(this._container));
3416                 }
3417
3418                 var containerPos = map.layerPointToContainerPoint(layerPos),
3419                         padding = this.options.autoPanPadding,
3420                         size = map.getSize(),
3421                         dx = 0,
3422                         dy = 0;
3423
3424                 if (containerPos.x < 0) {
3425                         dx = containerPos.x - padding.x;
3426                 }
3427                 if (containerPos.x + containerWidth > size.x) {
3428                         dx = containerPos.x + containerWidth - size.x + padding.x;
3429                 }
3430                 if (containerPos.y < 0) {
3431                         dy = containerPos.y - padding.y;
3432                 }
3433                 if (containerPos.y + containerHeight > size.y) {
3434                         dy = containerPos.y + containerHeight - size.y + padding.y;
3435                 }
3436
3437                 if (dx || dy) {
3438                         map.panBy(new L.Point(dx, dy));
3439                 }
3440         },
3441
3442         _onCloseButtonClick: function (e) {
3443                 this._close();
3444                 L.DomEvent.stop(e);
3445         }
3446 });
3447
3448 L.popup = function (options, source) {
3449         return new L.Popup(options, source);
3450 };
3451
3452
3453 /*
3454  * Popup extension to L.Marker, adding openPopup & bindPopup methods.
3455  */
3456
3457 L.Marker.include({
3458         openPopup: function () {
3459                 if (this._popup && this._map) {
3460                         this._popup.setLatLng(this._latlng);
3461                         this._map.openPopup(this._popup);
3462                 }
3463
3464                 return this;
3465         },
3466
3467         closePopup: function () {
3468                 if (this._popup) {
3469                         this._popup._close();
3470                 }
3471                 return this;
3472         },
3473
3474         bindPopup: function (content, options) {
3475                 var anchor = L.point(this.options.icon.options.popupAnchor) || new L.Point(0, 0);
3476
3477                 anchor = anchor.add(L.Popup.prototype.options.offset);
3478
3479                 if (options && options.offset) {
3480                         anchor = anchor.add(options.offset);
3481                 }
3482
3483                 options = L.Util.extend({offset: anchor}, options);
3484
3485                 if (!this._popup) {
3486                         this
3487                                 .on('click', this.openPopup, this)
3488                                 .on('remove', this.closePopup, this)
3489                                 .on('move', this._movePopup, this);
3490                 }
3491
3492                 this._popup = new L.Popup(options, this)
3493                         .setContent(content);
3494
3495                 return this;
3496         },
3497
3498         unbindPopup: function () {
3499                 if (this._popup) {
3500                         this._popup = null;
3501                         this
3502                                 .off('click', this.openPopup)
3503                                 .off('remove', this.closePopup)
3504                                 .off('move', this._movePopup);
3505                 }
3506                 return this;
3507         },
3508
3509         _movePopup: function (e) {
3510                 this._popup.setLatLng(e.latlng);
3511         }
3512 });
3513
3514
3515
3516 L.Map.include({
3517         openPopup: function (popup) {
3518                 this.closePopup();
3519
3520                 this._popup = popup;
3521
3522                 return this
3523                         .addLayer(popup)
3524                         .fire('popupopen', {popup: this._popup});
3525         },
3526
3527         closePopup: function () {
3528                 if (this._popup) {
3529                         this._popup._close();
3530                 }
3531                 return this;
3532         }
3533 });
3534
3535 /*
3536  * L.LayerGroup is a class to combine several layers so you can manipulate the group (e.g. add/remove it) as one layer.
3537  */
3538
3539 L.LayerGroup = L.Class.extend({
3540         initialize: function (layers) {
3541                 this._layers = {};
3542
3543                 var i, len;
3544
3545                 if (layers) {
3546                         for (i = 0, len = layers.length; i < len; i++) {
3547                                 this.addLayer(layers[i]);
3548                         }
3549                 }
3550         },
3551
3552         addLayer: function (layer) {
3553                 var id = L.Util.stamp(layer);
3554
3555                 this._layers[id] = layer;
3556
3557                 if (this._map) {
3558                         this._map.addLayer(layer);
3559                 }
3560
3561                 return this;
3562         },
3563
3564         removeLayer: function (layer) {
3565                 var id = L.Util.stamp(layer);
3566
3567                 delete this._layers[id];
3568
3569                 if (this._map) {
3570                         this._map.removeLayer(layer);
3571                 }
3572
3573                 return this;
3574         },
3575
3576         clearLayers: function () {
3577                 this.eachLayer(this.removeLayer, this);
3578                 return this;
3579         },
3580
3581         invoke: function (methodName) {
3582                 var args = Array.prototype.slice.call(arguments, 1),
3583                         i, layer;
3584
3585                 for (i in this._layers) {
3586                         if (this._layers.hasOwnProperty(i)) {
3587                                 layer = this._layers[i];
3588
3589                                 if (layer[methodName]) {
3590                                         layer[methodName].apply(layer, args);
3591                                 }
3592                         }
3593                 }
3594
3595                 return this;
3596         },
3597
3598         onAdd: function (map) {
3599                 this._map = map;
3600                 this.eachLayer(map.addLayer, map);
3601         },
3602
3603         onRemove: function (map) {
3604                 this.eachLayer(map.removeLayer, map);
3605                 this._map = null;
3606         },
3607
3608         addTo: function (map) {
3609                 map.addLayer(this);
3610                 return this;
3611         },
3612
3613         eachLayer: function (method, context) {
3614                 for (var i in this._layers) {
3615                         if (this._layers.hasOwnProperty(i)) {
3616                                 method.call(context, this._layers[i]);
3617                         }
3618                 }
3619         }
3620 });
3621
3622 L.layerGroup = function (layers) {
3623         return new L.LayerGroup(layers);
3624 };
3625
3626
3627 /*
3628  * L.FeatureGroup extends L.LayerGroup by introducing mouse events and bindPopup method shared between a group of layers.
3629  */
3630
3631 L.FeatureGroup = L.LayerGroup.extend({
3632         includes: L.Mixin.Events,
3633
3634         addLayer: function (layer) {
3635                 if (this._layers[L.Util.stamp(layer)]) {
3636                         return this;
3637                 }
3638
3639                 layer.on('click dblclick mouseover mouseout mousemove contextmenu', this._propagateEvent, this);
3640
3641                 L.LayerGroup.prototype.addLayer.call(this, layer);
3642
3643                 if (this._popupContent && layer.bindPopup) {
3644                         layer.bindPopup(this._popupContent);
3645                 }
3646
3647                 return this;
3648         },
3649
3650         removeLayer: function (layer) {
3651                 layer.off('click dblclick mouseover mouseout mousemove contextmenu', this._propagateEvent, this);
3652
3653                 L.LayerGroup.prototype.removeLayer.call(this, layer);
3654
3655                 if (this._popupContent) {
3656                         return this.invoke('unbindPopup');
3657                 } else {
3658                         return this;
3659                 }
3660         },
3661
3662         bindPopup: function (content) {
3663                 this._popupContent = content;
3664                 return this.invoke('bindPopup', content);
3665         },
3666
3667         setStyle: function (style) {
3668                 return this.invoke('setStyle', style);
3669         },
3670
3671         bringToFront: function () {
3672                 return this.invoke('bringToFront');
3673         },
3674
3675         bringToBack: function () {
3676                 return this.invoke('bringToBack');
3677         },
3678
3679         getBounds: function () {
3680                 var bounds = new L.LatLngBounds();
3681                 this.eachLayer(function (layer) {
3682                         bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds());
3683                 }, this);
3684                 return bounds;
3685         },
3686
3687         _propagateEvent: function (e) {
3688                 e.layer  = e.target;
3689                 e.target = this;
3690
3691                 this.fire(e.type, e);
3692         }
3693 });
3694
3695 L.featureGroup = function (layers) {
3696         return new L.FeatureGroup(layers);
3697 };
3698
3699
3700 /*
3701  * L.Path is a base class for rendering vector paths on a map. It's inherited by Polyline, Circle, etc.
3702  */
3703
3704 L.Path = L.Class.extend({
3705         includes: [L.Mixin.Events],
3706
3707         statics: {
3708                 // how much to extend the clip area around the map view
3709                 // (relative to its size, e.g. 0.5 is half the screen in each direction)
3710                 // set in such way that SVG element doesn't exceed 1280px (vector layers flicker on dragend if it is)
3711                 CLIP_PADDING: L.Browser.mobile ?
3712                         Math.max(0, Math.min(0.5,
3713                                 (1280 / Math.max(window.innerWidth, window.innerHeight) - 1) / 2))
3714                         : 0.5
3715         },
3716
3717         options: {
3718                 stroke: true,
3719                 color: '#0033ff',
3720                 dashArray: null,
3721                 weight: 5,
3722                 opacity: 0.5,
3723
3724                 fill: false,
3725                 fillColor: null, //same as color by default
3726                 fillOpacity: 0.2,
3727
3728                 clickable: true
3729         },
3730
3731         initialize: function (options) {
3732                 L.Util.setOptions(this, options);
3733         },
3734
3735         onAdd: function (map) {
3736                 this._map = map;
3737
3738                 if (!this._container) {
3739                         this._initElements();
3740                         this._initEvents();
3741                 }
3742
3743                 this.projectLatlngs();
3744                 this._updatePath();
3745
3746                 if (this._container) {
3747                         this._map._pathRoot.appendChild(this._container);
3748                 }
3749
3750                 map.on({
3751                         'viewreset': this.projectLatlngs,
3752                         'moveend': this._updatePath
3753                 }, this);
3754         },
3755
3756         addTo: function (map) {
3757                 map.addLayer(this);
3758                 return this;
3759         },
3760
3761         onRemove: function (map) {
3762                 map._pathRoot.removeChild(this._container);
3763
3764                 this._map = null;
3765
3766                 if (L.Browser.vml) {
3767                         this._container = null;
3768                         this._stroke = null;
3769                         this._fill = null;
3770                 }
3771
3772                 this.fire('remove');
3773
3774                 map.off({
3775                         'viewreset': this.projectLatlngs,
3776                         'moveend': this._updatePath
3777                 }, this);
3778         },
3779
3780         projectLatlngs: function () {
3781                 // do all projection stuff here
3782         },
3783
3784         setStyle: function (style) {
3785                 L.Util.setOptions(this, style);
3786
3787                 if (this._container) {
3788                         this._updateStyle();
3789                 }
3790
3791                 return this;
3792         },
3793
3794         redraw: function () {
3795                 if (this._map) {
3796                         this.projectLatlngs();
3797                         this._updatePath();
3798                 }
3799                 return this;
3800         }
3801 });
3802
3803 L.Map.include({
3804         _updatePathViewport: function () {
3805                 var p = L.Path.CLIP_PADDING,
3806                         size = this.getSize(),
3807                         panePos = L.DomUtil.getPosition(this._mapPane),
3808                         min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)._round()),
3809                         max = min.add(size.multiplyBy(1 + p * 2)._round());
3810
3811                 this._pathViewport = new L.Bounds(min, max);
3812         }
3813 });
3814
3815
3816 L.Path.SVG_NS = 'http://www.w3.org/2000/svg';
3817
3818 L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect);
3819
3820 L.Path = L.Path.extend({
3821         statics: {
3822                 SVG: L.Browser.svg
3823         },
3824
3825         bringToFront: function () {
3826                 var root = this._map._pathRoot,
3827                         path = this._container;
3828
3829                 if (path && root.lastChild !== path) {
3830                         root.appendChild(path);
3831                 }
3832                 return this;
3833         },
3834
3835         bringToBack: function () {
3836                 var root = this._map._pathRoot,
3837                         path = this._container,
3838                         first = root.firstChild;
3839
3840                 if (path && first !== path) {
3841                         root.insertBefore(path, first);
3842                 }
3843                 return this;
3844         },
3845
3846         getPathString: function () {
3847                 // form path string here
3848         },
3849
3850         _createElement: function (name) {
3851                 return document.createElementNS(L.Path.SVG_NS, name);
3852         },
3853
3854         _initElements: function () {
3855                 this._map._initPathRoot();
3856                 this._initPath();
3857                 this._initStyle();
3858         },
3859
3860         _initPath: function () {
3861                 this._container = this._createElement('g');
3862
3863                 this._path = this._createElement('path');
3864                 this._container.appendChild(this._path);
3865         },
3866
3867         _initStyle: function () {
3868                 if (this.options.stroke) {
3869                         this._path.setAttribute('stroke-linejoin', 'round');
3870                         this._path.setAttribute('stroke-linecap', 'round');
3871                 }
3872                 if (this.options.fill) {
3873                         this._path.setAttribute('fill-rule', 'evenodd');
3874                 }
3875                 this._updateStyle();
3876         },
3877
3878         _updateStyle: function () {
3879                 if (this.options.stroke) {
3880                         this._path.setAttribute('stroke', this.options.color);
3881                         this._path.setAttribute('stroke-opacity', this.options.opacity);
3882                         this._path.setAttribute('stroke-width', this.options.weight);
3883                         if (this.options.dashArray) {
3884                                 this._path.setAttribute('stroke-dasharray', this.options.dashArray);
3885                         } else {
3886                                 this._path.removeAttribute('stroke-dasharray');
3887                         }
3888                 } else {
3889                         this._path.setAttribute('stroke', 'none');
3890                 }
3891                 if (this.options.fill) {
3892                         this._path.setAttribute('fill', this.options.fillColor || this.options.color);
3893                         this._path.setAttribute('fill-opacity', this.options.fillOpacity);
3894                 } else {
3895                         this._path.setAttribute('fill', 'none');
3896                 }
3897         },
3898
3899         _updatePath: function () {
3900                 var str = this.getPathString();
3901                 if (!str) {
3902                         // fix webkit empty string parsing bug
3903                         str = 'M0 0';
3904                 }
3905                 this._path.setAttribute('d', str);
3906         },
3907
3908         // TODO remove duplication with L.Map
3909         _initEvents: function () {
3910                 if (this.options.clickable) {
3911                         if (L.Browser.svg || !L.Browser.vml) {
3912                                 this._path.setAttribute('class', 'leaflet-clickable');
3913                         }
3914
3915                         L.DomEvent.on(this._container, 'click', this._onMouseClick, this);
3916
3917                         var events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'mousemove', 'contextmenu'];
3918                         for (var i = 0; i < events.length; i++) {
3919                                 L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this);
3920                         }
3921                 }
3922         },
3923
3924         _onMouseClick: function (e) {
3925                 if (this._map.dragging && this._map.dragging.moved()) {
3926                         return;
3927                 }
3928
3929                 this._fireMouseEvent(e);
3930         },
3931
3932         _fireMouseEvent: function (e) {
3933                 if (!this.hasEventListeners(e.type)) {
3934                         return;
3935                 }
3936
3937                 var map = this._map,
3938                         containerPoint = map.mouseEventToContainerPoint(e),
3939                         layerPoint = map.containerPointToLayerPoint(containerPoint),
3940                         latlng = map.layerPointToLatLng(layerPoint);
3941
3942                 this.fire(e.type, {
3943                         latlng: latlng,
3944                         layerPoint: layerPoint,
3945                         containerPoint: containerPoint,
3946                         originalEvent: e
3947                 });
3948
3949                 if (e.type === 'contextmenu') {
3950                         L.DomEvent.preventDefault(e);
3951                 }
3952                 L.DomEvent.stopPropagation(e);
3953         }
3954 });
3955
3956 L.Map.include({
3957         _initPathRoot: function () {
3958                 if (!this._pathRoot) {
3959                         this._pathRoot = L.Path.prototype._createElement('svg');
3960                         this._panes.overlayPane.appendChild(this._pathRoot);
3961
3962                         if (this.options.zoomAnimation && L.Browser.any3d) {
3963                                 this._pathRoot.setAttribute('class', ' leaflet-zoom-animated');
3964
3965                                 this.on({
3966                                         'zoomanim': this._animatePathZoom,
3967                                         'zoomend': this._endPathZoom
3968                                 });
3969                         } else {
3970                                 this._pathRoot.setAttribute('class', ' leaflet-zoom-hide');
3971                         }
3972
3973                         this.on('moveend', this._updateSvgViewport);
3974                         this._updateSvgViewport();
3975                 }
3976         },
3977
3978         _animatePathZoom: function (opt) {
3979                 var scale = this.getZoomScale(opt.zoom),
3980                         offset = this._getCenterOffset(opt.center),
3981                         translate = offset.multiplyBy(-scale)._add(this._pathViewport.min);
3982
3983                 this._pathRoot.style[L.DomUtil.TRANSFORM] =
3984                                 L.DomUtil.getTranslateString(translate) + ' scale(' + scale + ') ';
3985
3986                 this._pathZooming = true;
3987         },
3988
3989         _endPathZoom: function () {
3990                 this._pathZooming = false;
3991         },
3992
3993         _updateSvgViewport: function () {
3994                 if (this._pathZooming) {
3995                         // Do not update SVGs while a zoom animation is going on otherwise the animation will break.
3996                         // When the zoom animation ends we will be updated again anyway
3997                         // This fixes the case where you do a momentum move and zoom while the move is still ongoing.
3998                         return;
3999                 }
4000
4001                 this._updatePathViewport();
4002
4003                 var vp = this._pathViewport,
4004                         min = vp.min,
4005                         max = vp.max,
4006                         width = max.x - min.x,
4007                         height = max.y - min.y,
4008                         root = this._pathRoot,
4009                         pane = this._panes.overlayPane;
4010
4011                 // Hack to make flicker on drag end on mobile webkit less irritating
4012                 if (L.Browser.mobileWebkit) {
4013                         pane.removeChild(root);
4014                 }
4015
4016                 L.DomUtil.setPosition(root, min);
4017                 root.setAttribute('width', width);
4018                 root.setAttribute('height', height);
4019                 root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
4020
4021                 if (L.Browser.mobileWebkit) {
4022                         pane.appendChild(root);
4023                 }
4024         }
4025 });
4026
4027
4028 /*
4029  * Popup extension to L.Path (polylines, polygons, circles), adding bindPopup method.
4030  */
4031
4032 L.Path.include({
4033
4034         bindPopup: function (content, options) {
4035
4036                 if (!this._popup || this._popup.options !== options) {
4037                         this._popup = new L.Popup(options, this);
4038                 }
4039
4040                 this._popup.setContent(content);
4041
4042                 if (!this._popupHandlersAdded) {
4043                         this
4044                                 .on('click', this._openPopup, this)
4045                                 .on('remove', this._closePopup, this);
4046                         this._popupHandlersAdded = true;
4047                 }
4048
4049                 return this;
4050         },
4051
4052         unbindPopup: function () {
4053                 if (this._popup) {
4054                         this._popup = null;
4055                         this
4056                                 .off('click', this.openPopup)
4057                                 .off('remove', this.closePopup);
4058                 }
4059                 return this;
4060         },
4061
4062         openPopup: function (latlng) {
4063
4064                 if (this._popup) {
4065                         latlng = latlng || this._latlng ||
4066                                         this._latlngs[Math.floor(this._latlngs.length / 2)];
4067
4068                         this._openPopup({latlng: latlng});
4069                 }
4070
4071                 return this;
4072         },
4073
4074         _openPopup: function (e) {
4075                 this._popup.setLatLng(e.latlng);
4076                 this._map.openPopup(this._popup);
4077         },
4078
4079         _closePopup: function () {
4080                 this._popup._close();
4081         }
4082 });
4083
4084
4085 /*
4086  * Vector rendering for IE6-8 through VML.
4087  * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
4088  */
4089
4090 L.Browser.vml = !L.Browser.svg && (function () {
4091         try {
4092                 var div = document.createElement('div');
4093                 div.innerHTML = '<v:shape adj="1"/>';
4094
4095                 var shape = div.firstChild;
4096                 shape.style.behavior = 'url(#default#VML)';
4097
4098                 return shape && (typeof shape.adj === 'object');
4099         } catch (e) {
4100                 return false;
4101         }
4102 }());
4103
4104 L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({
4105         statics: {
4106                 VML: true,
4107                 CLIP_PADDING: 0.02
4108         },
4109
4110         _createElement: (function () {
4111                 try {
4112                         document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
4113                         return function (name) {
4114                                 return document.createElement('<lvml:' + name + ' class="lvml">');
4115                         };
4116                 } catch (e) {
4117                         return function (name) {
4118                                 return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
4119                         };
4120                 }
4121         }()),
4122
4123         _initPath: function () {
4124                 var container = this._container = this._createElement('shape');
4125                 L.DomUtil.addClass(container, 'leaflet-vml-shape');
4126                 if (this.options.clickable) {
4127                         L.DomUtil.addClass(container, 'leaflet-clickable');
4128                 }
4129                 container.coordsize = '1 1';
4130
4131                 this._path = this._createElement('path');
4132                 container.appendChild(this._path);
4133
4134                 this._map._pathRoot.appendChild(container);
4135         },
4136
4137         _initStyle: function () {
4138                 this._updateStyle();
4139         },
4140
4141         _updateStyle: function () {
4142                 var stroke = this._stroke,
4143                         fill = this._fill,
4144                         options = this.options,
4145                         container = this._container;
4146
4147                 container.stroked = options.stroke;
4148                 container.filled = options.fill;
4149
4150                 if (options.stroke) {
4151                         if (!stroke) {
4152                                 stroke = this._stroke = this._createElement('stroke');
4153                                 stroke.endcap = 'round';
4154                                 container.appendChild(stroke);
4155                         }
4156                         stroke.weight = options.weight + 'px';
4157                         stroke.color = options.color;
4158                         stroke.opacity = options.opacity;
4159                         if (options.dashArray) {
4160                                 stroke.dashStyle = options.dashArray.replace(/ *, */g, ' ');
4161                         } else {
4162                                 stroke.dashStyle = '';
4163                         }
4164                 } else if (stroke) {
4165                         container.removeChild(stroke);
4166                         this._stroke = null;
4167                 }
4168
4169                 if (options.fill) {
4170                         if (!fill) {
4171                                 fill = this._fill = this._createElement('fill');
4172                                 container.appendChild(fill);
4173                         }
4174                         fill.color = options.fillColor || options.color;
4175                         fill.opacity = options.fillOpacity;
4176                 } else if (fill) {
4177                         container.removeChild(fill);
4178                         this._fill = null;
4179                 }
4180         },
4181
4182         _updatePath: function () {
4183                 var style = this._container.style;
4184
4185                 style.display = 'none';
4186                 this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug
4187                 style.display = '';
4188         }
4189 });
4190
4191 L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : {
4192         _initPathRoot: function () {
4193                 if (this._pathRoot) { return; }
4194
4195                 var root = this._pathRoot = document.createElement('div');
4196                 root.className = 'leaflet-vml-container';
4197                 this._panes.overlayPane.appendChild(root);
4198
4199                 this.on('moveend', this._updatePathViewport);
4200                 this._updatePathViewport();
4201         }
4202 });
4203
4204
4205 /*
4206  * Vector rendering for all browsers that support canvas.
4207  */
4208
4209 L.Browser.canvas = (function () {
4210         return !!document.createElement('canvas').getContext;
4211 }());
4212
4213 L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path : L.Path.extend({
4214         statics: {
4215                 //CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value
4216                 CANVAS: true,
4217                 SVG: false
4218         },
4219
4220         redraw: function () {
4221                 if (this._map) {
4222                         this.projectLatlngs();
4223                         this._requestUpdate();
4224                 }
4225                 return this;
4226         },
4227
4228         setStyle: function (style) {
4229                 L.Util.setOptions(this, style);
4230
4231                 if (this._map) {
4232                         this._updateStyle();
4233                         this._requestUpdate();
4234                 }
4235                 return this;
4236         },
4237
4238         onRemove: function (map) {
4239                 map
4240                     .off('viewreset', this.projectLatlngs, this)
4241                     .off('moveend', this._updatePath, this);
4242
4243                 this._requestUpdate();
4244
4245                 this._map = null;
4246         },
4247
4248         _requestUpdate: function () {
4249                 if (this._map && !L.Path._updateRequest) {
4250                         L.Path._updateRequest = L.Util.requestAnimFrame(this._fireMapMoveEnd, this._map);
4251                 }
4252         },
4253
4254         _fireMapMoveEnd: function () {
4255                 L.Path._updateRequest = null;
4256                 this.fire('moveend');
4257         },
4258
4259         _initElements: function () {
4260                 this._map._initPathRoot();
4261                 this._ctx = this._map._canvasCtx;
4262         },
4263
4264         _updateStyle: function () {
4265                 var options = this.options;
4266
4267                 if (options.stroke) {
4268                         this._ctx.lineWidth = options.weight;
4269                         this._ctx.strokeStyle = options.color;
4270                 }
4271                 if (options.fill) {
4272                         this._ctx.fillStyle = options.fillColor || options.color;
4273                 }
4274         },
4275
4276         _drawPath: function () {
4277                 var i, j, len, len2, point, drawMethod;
4278
4279                 this._ctx.beginPath();
4280
4281                 for (i = 0, len = this._parts.length; i < len; i++) {
4282                         for (j = 0, len2 = this._parts[i].length; j < len2; j++) {
4283                                 point = this._parts[i][j];
4284                                 drawMethod = (j === 0 ? 'move' : 'line') + 'To';
4285
4286                                 this._ctx[drawMethod](point.x, point.y);
4287                         }
4288                         // TODO refactor ugly hack
4289                         if (this instanceof L.Polygon) {
4290                                 this._ctx.closePath();
4291                         }
4292                 }
4293         },
4294
4295         _checkIfEmpty: function () {
4296                 return !this._parts.length;
4297         },
4298
4299         _updatePath: function () {
4300                 if (this._checkIfEmpty()) { return; }
4301
4302                 var ctx = this._ctx,
4303                         options = this.options;
4304
4305                 this._drawPath();
4306                 ctx.save();
4307                 this._updateStyle();
4308
4309                 if (options.fill) {
4310                         if (options.fillOpacity < 1) {
4311                                 ctx.globalAlpha = options.fillOpacity;
4312                         }
4313                         ctx.fill();
4314                 }
4315
4316                 if (options.stroke) {
4317                         if (options.opacity < 1) {
4318                                 ctx.globalAlpha = options.opacity;
4319                         }
4320                         ctx.stroke();
4321                 }
4322
4323                 ctx.restore();
4324
4325                 // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
4326         },
4327
4328         _initEvents: function () {
4329                 if (this.options.clickable) {
4330                         // TODO hand cursor
4331                         // TODO mouseover, mouseout, dblclick
4332                         this._map.on('click', this._onClick, this);
4333                 }
4334         },
4335
4336         _onClick: function (e) {
4337                 if (this._containsPoint(e.layerPoint)) {
4338                         this.fire('click', e);
4339                 }
4340         }
4341 });
4342
4343 L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : {
4344         _initPathRoot: function () {
4345                 var root = this._pathRoot,
4346                         ctx;
4347
4348                 if (!root) {
4349                         root = this._pathRoot = document.createElement("canvas");
4350                         root.style.position = 'absolute';
4351                         ctx = this._canvasCtx = root.getContext('2d');
4352
4353                         ctx.lineCap = "round";
4354                         ctx.lineJoin = "round";
4355
4356                         this._panes.overlayPane.appendChild(root);
4357
4358                         if (this.options.zoomAnimation) {
4359                                 this._pathRoot.className = 'leaflet-zoom-animated';
4360                                 this.on('zoomanim', this._animatePathZoom);
4361                                 this.on('zoomend', this._endPathZoom);
4362                         }
4363                         this.on('moveend', this._updateCanvasViewport);
4364                         this._updateCanvasViewport();
4365                 }
4366         },
4367
4368         _updateCanvasViewport: function () {
4369                 if (this._pathZooming) {
4370                         //Don't redraw while zooming. See _updateSvgViewport for more details
4371                         return;
4372                 }
4373                 this._updatePathViewport();
4374
4375                 var vp = this._pathViewport,
4376                         min = vp.min,
4377                         size = vp.max.subtract(min),
4378                         root = this._pathRoot;
4379
4380                 //TODO check if this works properly on mobile webkit
4381                 L.DomUtil.setPosition(root, min);
4382                 root.width = size.x;
4383                 root.height = size.y;
4384                 root.getContext('2d').translate(-min.x, -min.y);
4385         }
4386 });
4387
4388
4389 /*
4390  * L.LineUtil contains different utility functions for line segments
4391  * and polylines (clipping, simplification, distances, etc.)
4392  */
4393
4394 L.LineUtil = {
4395
4396         // Simplify polyline with vertex reduction and Douglas-Peucker simplification.
4397         // Improves rendering performance dramatically by lessening the number of points to draw.
4398
4399         simplify: function (/*Point[]*/ points, /*Number*/ tolerance) {
4400                 if (!tolerance || !points.length) {
4401                         return points.slice();
4402                 }
4403
4404                 var sqTolerance = tolerance * tolerance;
4405
4406                 // stage 1: vertex reduction
4407                 points = this._reducePoints(points, sqTolerance);
4408
4409                 // stage 2: Douglas-Peucker simplification
4410                 points = this._simplifyDP(points, sqTolerance);
4411
4412                 return points;
4413         },
4414
4415         // distance from a point to a segment between two points
4416         pointToSegmentDistance:  function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
4417                 return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true));
4418         },
4419
4420         closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
4421                 return this._sqClosestPointOnSegment(p, p1, p2);
4422         },
4423
4424         // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm
4425         _simplifyDP: function (points, sqTolerance) {
4426
4427                 var len = points.length,
4428                         ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array,
4429                         markers = new ArrayConstructor(len);
4430
4431                 markers[0] = markers[len - 1] = 1;
4432
4433                 this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
4434
4435                 var i,
4436                         newPoints = [];
4437
4438                 for (i = 0; i < len; i++) {
4439                         if (markers[i]) {
4440                                 newPoints.push(points[i]);
4441                         }
4442                 }
4443
4444                 return newPoints;
4445         },
4446
4447         _simplifyDPStep: function (points, markers, sqTolerance, first, last) {
4448
4449                 var maxSqDist = 0,
4450                         index, i, sqDist;
4451
4452                 for (i = first + 1; i <= last - 1; i++) {
4453                         sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true);
4454
4455                         if (sqDist > maxSqDist) {
4456                                 index = i;
4457                                 maxSqDist = sqDist;
4458                         }
4459                 }
4460
4461                 if (maxSqDist > sqTolerance) {
4462                         markers[index] = 1;
4463
4464                         this._simplifyDPStep(points, markers, sqTolerance, first, index);
4465                         this._simplifyDPStep(points, markers, sqTolerance, index, last);
4466                 }
4467         },
4468
4469         // reduce points that are too close to each other to a single point
4470         _reducePoints: function (points, sqTolerance) {
4471                 var reducedPoints = [points[0]];
4472
4473                 for (var i = 1, prev = 0, len = points.length; i < len; i++) {
4474                         if (this._sqDist(points[i], points[prev]) > sqTolerance) {
4475                                 reducedPoints.push(points[i]);
4476                                 prev = i;
4477                         }
4478                 }
4479                 if (prev < len - 1) {
4480                         reducedPoints.push(points[len - 1]);
4481                 }
4482                 return reducedPoints;
4483         },
4484
4485         /*jshint bitwise:false */ // temporarily allow bitwise oprations
4486
4487         // Cohen-Sutherland line clipping algorithm.
4488         // Used to avoid rendering parts of a polyline that are not currently visible.
4489
4490         clipSegment: function (a, b, bounds, useLastCode) {
4491                 var min = bounds.min,
4492                         max = bounds.max;
4493
4494                 var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds),
4495                         codeB = this._getBitCode(b, bounds);
4496
4497                 // save 2nd code to avoid calculating it on the next segment
4498                 this._lastCode = codeB;
4499
4500                 while (true) {
4501                         // if a,b is inside the clip window (trivial accept)
4502                         if (!(codeA | codeB)) {
4503                                 return [a, b];
4504                         // if a,b is outside the clip window (trivial reject)
4505                         } else if (codeA & codeB) {
4506                                 return false;
4507                         // other cases
4508                         } else {
4509                                 var codeOut = codeA || codeB,
4510                                         p = this._getEdgeIntersection(a, b, codeOut, bounds),
4511                                         newCode = this._getBitCode(p, bounds);
4512
4513                                 if (codeOut === codeA) {
4514                                         a = p;
4515                                         codeA = newCode;
4516                                 } else {
4517                                         b = p;
4518                                         codeB = newCode;
4519                                 }
4520                         }
4521                 }
4522         },
4523
4524         _getEdgeIntersection: function (a, b, code, bounds) {
4525                 var dx = b.x - a.x,
4526                         dy = b.y - a.y,
4527                         min = bounds.min,
4528                         max = bounds.max;
4529
4530                 if (code & 8) { // top
4531                         return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y);
4532                 } else if (code & 4) { // bottom
4533                         return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y);
4534                 } else if (code & 2) { // right
4535                         return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx);
4536                 } else if (code & 1) { // left
4537                         return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx);
4538                 }
4539         },
4540
4541         _getBitCode: function (/*Point*/ p, bounds) {
4542                 var code = 0;
4543
4544                 if (p.x < bounds.min.x) { // left
4545                         code |= 1;
4546                 } else if (p.x > bounds.max.x) { // right
4547                         code |= 2;
4548                 }
4549                 if (p.y < bounds.min.y) { // bottom
4550                         code |= 4;
4551                 } else if (p.y > bounds.max.y) { // top
4552                         code |= 8;
4553                 }
4554
4555                 return code;
4556         },
4557
4558         /*jshint bitwise:true */
4559
4560         // square distance (to avoid unnecessary Math.sqrt calls)
4561         _sqDist: function (p1, p2) {
4562                 var dx = p2.x - p1.x,
4563                         dy = p2.y - p1.y;
4564                 return dx * dx + dy * dy;
4565         },
4566
4567         // return closest point on segment or distance to that point
4568         _sqClosestPointOnSegment: function (p, p1, p2, sqDist) {
4569                 var x = p1.x,
4570                         y = p1.y,
4571                         dx = p2.x - x,
4572                         dy = p2.y - y,
4573                         dot = dx * dx + dy * dy,
4574                         t;
4575
4576                 if (dot > 0) {
4577                         t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
4578
4579                         if (t > 1) {
4580                                 x = p2.x;
4581                                 y = p2.y;
4582                         } else if (t > 0) {
4583                                 x += dx * t;
4584                                 y += dy * t;
4585                         }
4586                 }
4587
4588                 dx = p.x - x;
4589                 dy = p.y - y;
4590
4591                 return sqDist ? dx * dx + dy * dy : new L.Point(x, y);
4592         }
4593 };
4594
4595
4596 L.Polyline = L.Path.extend({
4597         initialize: function (latlngs, options) {
4598                 L.Path.prototype.initialize.call(this, options);
4599
4600                 this._latlngs = this._convertLatLngs(latlngs);
4601
4602                 // TODO refactor: move to Polyline.Edit.js
4603                 if (L.Handler.PolyEdit) {
4604                         this.editing = new L.Handler.PolyEdit(this);
4605
4606                         if (this.options.editable) {
4607                                 this.editing.enable();
4608                         }
4609                 }
4610         },
4611
4612         options: {
4613                 // how much to simplify the polyline on each zoom level
4614                 // more = better performance and smoother look, less = more accurate
4615                 smoothFactor: 1.0,
4616                 noClip: false
4617         },
4618
4619         projectLatlngs: function () {
4620                 this._originalPoints = [];
4621
4622                 for (var i = 0, len = this._latlngs.length; i < len; i++) {
4623                         this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]);
4624                 }
4625         },
4626
4627         getPathString: function () {
4628                 for (var i = 0, len = this._parts.length, str = ''; i < len; i++) {
4629                         str += this._getPathPartStr(this._parts[i]);
4630                 }
4631                 return str;
4632         },
4633
4634         getLatLngs: function () {
4635                 return this._latlngs;
4636         },
4637
4638         setLatLngs: function (latlngs) {
4639                 this._latlngs = this._convertLatLngs(latlngs);
4640                 return this.redraw();
4641         },
4642
4643         addLatLng: function (latlng) {
4644                 this._latlngs.push(L.latLng(latlng));
4645                 return this.redraw();
4646         },
4647
4648         spliceLatLngs: function (index, howMany) {
4649                 var removed = [].splice.apply(this._latlngs, arguments);
4650                 this._convertLatLngs(this._latlngs);
4651                 this.redraw();
4652                 return removed;
4653         },
4654
4655         closestLayerPoint: function (p) {
4656                 var minDistance = Infinity, parts = this._parts, p1, p2, minPoint = null;
4657
4658                 for (var j = 0, jLen = parts.length; j < jLen; j++) {
4659                         var points = parts[j];
4660                         for (var i = 1, len = points.length; i < len; i++) {
4661                                 p1 = points[i - 1];
4662                                 p2 = points[i];
4663                                 var sqDist = L.LineUtil._sqClosestPointOnSegment(p, p1, p2, true);
4664                                 if (sqDist < minDistance) {
4665                                         minDistance = sqDist;
4666                                         minPoint = L.LineUtil._sqClosestPointOnSegment(p, p1, p2);
4667                                 }
4668                         }
4669                 }
4670                 if (minPoint) {
4671                         minPoint.distance = Math.sqrt(minDistance);
4672                 }
4673                 return minPoint;
4674         },
4675
4676         getBounds: function () {
4677                 var b = new L.LatLngBounds();
4678                 var latLngs = this.getLatLngs();
4679                 for (var i = 0, len = latLngs.length; i < len; i++) {
4680                         b.extend(latLngs[i]);
4681                 }
4682                 return b;
4683         },
4684
4685         // TODO refactor: move to Polyline.Edit.js
4686         onAdd: function (map) {
4687                 L.Path.prototype.onAdd.call(this, map);
4688
4689                 if (this.editing && this.editing.enabled()) {
4690                         this.editing.addHooks();
4691                 }
4692         },
4693
4694         onRemove: function (map) {
4695                 if (this.editing && this.editing.enabled()) {
4696                         this.editing.removeHooks();
4697                 }
4698
4699                 L.Path.prototype.onRemove.call(this, map);
4700         },
4701
4702         _convertLatLngs: function (latlngs) {
4703                 var i, len;
4704                 for (i = 0, len = latlngs.length; i < len; i++) {
4705                         if (latlngs[i] instanceof Array && typeof latlngs[i][0] !== 'number') {
4706                                 return;
4707                         }
4708                         latlngs[i] = L.latLng(latlngs[i]);
4709                 }
4710                 return latlngs;
4711         },
4712
4713         _initEvents: function () {
4714                 L.Path.prototype._initEvents.call(this);
4715         },
4716
4717         _getPathPartStr: function (points) {
4718                 var round = L.Path.VML;
4719
4720                 for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) {
4721                         p = points[j];
4722                         if (round) {
4723                                 p._round();
4724                         }
4725                         str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
4726                 }
4727                 return str;
4728         },
4729
4730         _clipPoints: function () {
4731                 var points = this._originalPoints,
4732                         len = points.length,
4733                         i, k, segment;
4734
4735                 if (this.options.noClip) {
4736                         this._parts = [points];
4737                         return;
4738                 }
4739
4740                 this._parts = [];
4741
4742                 var parts = this._parts,
4743                         vp = this._map._pathViewport,
4744                         lu = L.LineUtil;
4745
4746                 for (i = 0, k = 0; i < len - 1; i++) {
4747                         segment = lu.clipSegment(points[i], points[i + 1], vp, i);
4748                         if (!segment) {
4749                                 continue;
4750                         }
4751
4752                         parts[k] = parts[k] || [];
4753                         parts[k].push(segment[0]);
4754
4755                         // if segment goes out of screen, or it's the last one, it's the end of the line part
4756                         if ((segment[1] !== points[i + 1]) || (i === len - 2)) {
4757                                 parts[k].push(segment[1]);
4758                                 k++;
4759                         }
4760                 }
4761         },
4762
4763         // simplify each clipped part of the polyline
4764         _simplifyPoints: function () {
4765                 var parts = this._parts,
4766                         lu = L.LineUtil;
4767
4768                 for (var i = 0, len = parts.length; i < len; i++) {
4769                         parts[i] = lu.simplify(parts[i], this.options.smoothFactor);
4770                 }
4771         },
4772
4773         _updatePath: function () {
4774                 if (!this._map) { return; }
4775
4776                 this._clipPoints();
4777                 this._simplifyPoints();
4778
4779                 L.Path.prototype._updatePath.call(this);
4780         }
4781 });
4782
4783 L.polyline = function (latlngs, options) {
4784         return new L.Polyline(latlngs, options);
4785 };
4786
4787
4788 /*
4789  * L.PolyUtil contains utilify functions for polygons (clipping, etc.).
4790  */
4791
4792 /*jshint bitwise:false */ // allow bitwise oprations here
4793
4794 L.PolyUtil = {};
4795
4796 /*
4797  * Sutherland-Hodgeman polygon clipping algorithm.
4798  * Used to avoid rendering parts of a polygon that are not currently visible.
4799  */
4800 L.PolyUtil.clipPolygon = function (points, bounds) {
4801         var min = bounds.min,
4802                 max = bounds.max,
4803                 clippedPoints,
4804                 edges = [1, 4, 2, 8],
4805                 i, j, k,
4806                 a, b,
4807                 len, edge, p,
4808                 lu = L.LineUtil;
4809
4810         for (i = 0, len = points.length; i < len; i++) {
4811                 points[i]._code = lu._getBitCode(points[i], bounds);
4812         }
4813
4814         // for each edge (left, bottom, right, top)
4815         for (k = 0; k < 4; k++) {
4816                 edge = edges[k];
4817                 clippedPoints = [];
4818
4819                 for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
4820                         a = points[i];
4821                         b = points[j];
4822
4823                         // if a is inside the clip window
4824                         if (!(a._code & edge)) {
4825                                 // if b is outside the clip window (a->b goes out of screen)
4826                                 if (b._code & edge) {
4827                                         p = lu._getEdgeIntersection(b, a, edge, bounds);
4828                                         p._code = lu._getBitCode(p, bounds);
4829                                         clippedPoints.push(p);
4830                                 }
4831                                 clippedPoints.push(a);
4832
4833                         // else if b is inside the clip window (a->b enters the screen)
4834                         } else if (!(b._code & edge)) {
4835                                 p = lu._getEdgeIntersection(b, a, edge, bounds);
4836                                 p._code = lu._getBitCode(p, bounds);
4837                                 clippedPoints.push(p);
4838                         }
4839                 }
4840                 points = clippedPoints;
4841         }
4842
4843         return points;
4844 };
4845
4846 /*jshint bitwise:true */
4847
4848
4849 /*
4850  * L.Polygon is used to display polygons on a map.
4851  */
4852
4853 L.Polygon = L.Polyline.extend({
4854         options: {
4855                 fill: true
4856         },
4857
4858         initialize: function (latlngs, options) {
4859                 L.Polyline.prototype.initialize.call(this, latlngs, options);
4860
4861                 if (latlngs && (latlngs[0] instanceof Array) && (typeof latlngs[0][0] !== 'number')) {
4862                         this._latlngs = this._convertLatLngs(latlngs[0]);
4863                         this._holes = latlngs.slice(1);
4864                 }
4865         },
4866
4867         projectLatlngs: function () {
4868                 L.Polyline.prototype.projectLatlngs.call(this);
4869
4870                 // project polygon holes points
4871                 // TODO move this logic to Polyline to get rid of duplication
4872                 this._holePoints = [];
4873
4874                 if (!this._holes) {
4875                         return;
4876                 }
4877
4878                 for (var i = 0, len = this._holes.length, hole; i < len; i++) {
4879                         this._holePoints[i] = [];
4880
4881                         for (var j = 0, len2 = this._holes[i].length; j < len2; j++) {
4882                                 this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]);
4883                         }
4884                 }
4885         },
4886
4887         _clipPoints: function () {
4888                 var points = this._originalPoints,
4889                         newParts = [];
4890
4891                 this._parts = [points].concat(this._holePoints);
4892
4893                 if (this.options.noClip) {
4894                         return;
4895                 }
4896
4897                 for (var i = 0, len = this._parts.length; i < len; i++) {
4898                         var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport);
4899                         if (!clipped.length) {
4900                                 continue;
4901                         }
4902                         newParts.push(clipped);
4903                 }
4904
4905                 this._parts = newParts;
4906         },
4907
4908         _getPathPartStr: function (points) {
4909                 var str = L.Polyline.prototype._getPathPartStr.call(this, points);
4910                 return str + (L.Browser.svg ? 'z' : 'x');
4911         }
4912 });
4913
4914 L.polygon = function (latlngs, options) {
4915         return new L.Polygon(latlngs, options);
4916 };
4917
4918
4919 /*
4920  * Contains L.MultiPolyline and L.MultiPolygon layers.
4921  */
4922
4923 (function () {
4924         function createMulti(Klass) {
4925                 return L.FeatureGroup.extend({
4926                         initialize: function (latlngs, options) {
4927                                 this._layers = {};
4928                                 this._options = options;
4929                                 this.setLatLngs(latlngs);
4930                         },
4931
4932                         setLatLngs: function (latlngs) {
4933                                 var i = 0, len = latlngs.length;
4934
4935                                 this.eachLayer(function (layer) {
4936                                         if (i < len) {
4937                                                 layer.setLatLngs(latlngs[i++]);
4938                                         } else {
4939                                                 this.removeLayer(layer);
4940                                         }
4941                                 }, this);
4942
4943                                 while (i < len) {
4944                                         this.addLayer(new Klass(latlngs[i++], this._options));
4945                                 }
4946
4947                                 return this;
4948                         }
4949                 });
4950         }
4951
4952         L.MultiPolyline = createMulti(L.Polyline);
4953         L.MultiPolygon = createMulti(L.Polygon);
4954
4955         L.multiPolyline = function (latlngs, options) {
4956                 return new L.MultiPolyline(latlngs, options);
4957         };
4958
4959         L.multiPolygon = function (latlngs, options) {
4960                 return new L.MultiPolygon(latlngs, options);
4961         };
4962 }());
4963
4964
4965 /*
4966  * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds
4967  */
4968
4969 L.Rectangle = L.Polygon.extend({
4970         initialize: function (latLngBounds, options) {
4971                 L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
4972         },
4973
4974         setBounds: function (latLngBounds) {
4975                 this.setLatLngs(this._boundsToLatLngs(latLngBounds));
4976         },
4977
4978         _boundsToLatLngs: function (latLngBounds) {
4979                 latLngBounds = L.latLngBounds(latLngBounds);
4980             return [
4981                 latLngBounds.getSouthWest(),
4982                 latLngBounds.getNorthWest(),
4983                 latLngBounds.getNorthEast(),
4984                 latLngBounds.getSouthEast(),
4985                 latLngBounds.getSouthWest()
4986             ];
4987         }
4988 });
4989
4990 L.rectangle = function (latLngBounds, options) {
4991         return new L.Rectangle(latLngBounds, options);
4992 };
4993
4994
4995 /*
4996  * L.Circle is a circle overlay (with a certain radius in meters).
4997  */
4998
4999 L.Circle = L.Path.extend({
5000         initialize: function (latlng, radius, options) {
5001                 L.Path.prototype.initialize.call(this, options);
5002
5003                 this._latlng = L.latLng(latlng);
5004                 this._mRadius = radius;
5005         },
5006
5007         options: {
5008                 fill: true
5009         },
5010
5011         setLatLng: function (latlng) {
5012                 this._latlng = L.latLng(latlng);
5013                 return this.redraw();
5014         },
5015
5016         setRadius: function (radius) {
5017                 this._mRadius = radius;
5018                 return this.redraw();
5019         },
5020
5021         projectLatlngs: function () {
5022                 var lngRadius = this._getLngRadius(),
5023                         latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngRadius, true),
5024                         point2 = this._map.latLngToLayerPoint(latlng2);
5025
5026                 this._point = this._map.latLngToLayerPoint(this._latlng);
5027                 this._radius = Math.max(Math.round(this._point.x - point2.x), 1);
5028         },
5029
5030         getBounds: function () {
5031                 var lngRadius = this._getLngRadius(),
5032                         latRadius = (this._mRadius / 40075017) * 360,
5033                         latlng = this._latlng,
5034                         sw = new L.LatLng(latlng.lat - latRadius, latlng.lng - lngRadius),
5035                         ne = new L.LatLng(latlng.lat + latRadius, latlng.lng + lngRadius);
5036
5037                 return new L.LatLngBounds(sw, ne);
5038         },
5039
5040         getLatLng: function () {
5041                 return this._latlng;
5042         },
5043
5044         getPathString: function () {
5045                 var p = this._point,
5046                         r = this._radius;
5047
5048                 if (this._checkIfEmpty()) {
5049                         return '';
5050                 }
5051
5052                 if (L.Browser.svg) {
5053                         return "M" + p.x + "," + (p.y - r) +
5054                                         "A" + r + "," + r + ",0,1,1," +
5055                                         (p.x - 0.1) + "," + (p.y - r) + " z";
5056                 } else {
5057                         p._round();
5058                         r = Math.round(r);
5059                         return "AL " + p.x + "," + p.y + " " + r + "," + r + " 0," + (65535 * 360);
5060                 }
5061         },
5062
5063         getRadius: function () {
5064                 return this._mRadius;
5065         },
5066
5067         // TODO Earth hardcoded, move into projection code!
5068
5069         _getLatRadius: function () {
5070                 return (this._mRadius / 40075017) * 360;
5071         },
5072
5073         _getLngRadius: function () {
5074                 return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
5075         },
5076
5077         _checkIfEmpty: function () {
5078                 if (!this._map) {
5079                         return false;
5080                 }
5081                 var vp = this._map._pathViewport,
5082                         r = this._radius,
5083                         p = this._point;
5084
5085                 return p.x - r > vp.max.x || p.y - r > vp.max.y ||
5086                         p.x + r < vp.min.x || p.y + r < vp.min.y;
5087         }
5088 });
5089
5090 L.circle = function (latlng, radius, options) {
5091         return new L.Circle(latlng, radius, options);
5092 };
5093
5094
5095 /*
5096  * L.CircleMarker is a circle overlay with a permanent pixel radius.
5097  */
5098
5099 L.CircleMarker = L.Circle.extend({
5100         options: {
5101                 radius: 10,
5102                 weight: 2
5103         },
5104
5105         initialize: function (latlng, options) {
5106                 L.Circle.prototype.initialize.call(this, latlng, null, options);
5107                 this._radius = this.options.radius;
5108         },
5109
5110         projectLatlngs: function () {
5111                 this._point = this._map.latLngToLayerPoint(this._latlng);
5112         },
5113
5114         setRadius: function (radius) {
5115                 this._radius = radius;
5116                 return this.redraw();
5117         }
5118 });
5119
5120 L.circleMarker = function (latlng, options) {
5121         return new L.CircleMarker(latlng, options);
5122 };
5123
5124
5125
5126 L.Polyline.include(!L.Path.CANVAS ? {} : {
5127         _containsPoint: function (p, closed) {
5128                 var i, j, k, len, len2, dist, part,
5129                         w = this.options.weight / 2;
5130
5131                 if (L.Browser.touch) {
5132                         w += 10; // polyline click tolerance on touch devices
5133                 }
5134
5135                 for (i = 0, len = this._parts.length; i < len; i++) {
5136                         part = this._parts[i];
5137                         for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
5138                                 if (!closed && (j === 0)) {
5139                                         continue;
5140                                 }
5141
5142                                 dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]);
5143
5144                                 if (dist <= w) {
5145                                         return true;
5146                                 }
5147                         }
5148                 }
5149                 return false;
5150         }
5151 });
5152
5153
5154
5155 L.Polygon.include(!L.Path.CANVAS ? {} : {
5156         _containsPoint: function (p) {
5157                 var inside = false,
5158                         part, p1, p2,
5159                         i, j, k,
5160                         len, len2;
5161
5162                 // TODO optimization: check if within bounds first
5163
5164                 if (L.Polyline.prototype._containsPoint.call(this, p, true)) {
5165                         // click on polygon border
5166                         return true;
5167                 }
5168
5169                 // ray casting algorithm for detecting if point is in polygon
5170
5171                 for (i = 0, len = this._parts.length; i < len; i++) {
5172                         part = this._parts[i];
5173
5174                         for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
5175                                 p1 = part[j];
5176                                 p2 = part[k];
5177
5178                                 if (((p1.y > p.y) !== (p2.y > p.y)) &&
5179                                                 (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
5180                                         inside = !inside;
5181                                 }
5182                         }
5183                 }
5184
5185                 return inside;
5186         }
5187 });
5188
5189
5190 /*
5191  * Circle canvas specific drawing parts.
5192  */
5193
5194 L.Circle.include(!L.Path.CANVAS ? {} : {
5195         _drawPath: function () {
5196                 var p = this._point;
5197                 this._ctx.beginPath();
5198                 this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false);
5199         },
5200
5201         _containsPoint: function (p) {
5202                 var center = this._point,
5203                         w2 = this.options.stroke ? this.options.weight / 2 : 0;
5204
5205                 return (p.distanceTo(center) <= this._radius + w2);
5206         }
5207 });
5208
5209
5210 L.GeoJSON = L.FeatureGroup.extend({
5211         initialize: function (geojson, options) {
5212                 L.Util.setOptions(this, options);
5213
5214                 this._layers = {};
5215
5216                 if (geojson) {
5217                         this.addData(geojson);
5218                 }
5219         },
5220
5221         addData: function (geojson) {
5222                 var features = geojson instanceof Array ? geojson : geojson.features,
5223                     i, len;
5224
5225                 if (features) {
5226                         for (i = 0, len = features.length; i < len; i++) {
5227                                 this.addData(features[i]);
5228                         }
5229                         return this;
5230                 }
5231
5232                 var options = this.options;
5233
5234                 if (options.filter && !options.filter(geojson)) { return; }
5235
5236                 var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer);
5237                 layer.feature = geojson;
5238
5239                 this.resetStyle(layer);
5240
5241                 if (options.onEachFeature) {
5242                         options.onEachFeature(geojson, layer);
5243                 }
5244
5245                 return this.addLayer(layer);
5246         },
5247
5248         resetStyle: function (layer) {
5249                 var style = this.options.style;
5250                 if (style) {
5251                         this._setLayerStyle(layer, style);
5252                 }
5253         },
5254
5255         setStyle: function (style) {
5256                 this.eachLayer(function (layer) {
5257                         this._setLayerStyle(layer, style);
5258                 }, this);
5259         },
5260
5261         _setLayerStyle: function (layer, style) {
5262                 if (typeof style === 'function') {
5263                         style = style(layer.feature);
5264                 }
5265                 if (layer.setStyle) {
5266                         layer.setStyle(style);
5267                 }
5268         }
5269 });
5270
5271 L.Util.extend(L.GeoJSON, {
5272         geometryToLayer: function (geojson, pointToLayer) {
5273                 var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
5274                     coords = geometry.coordinates,
5275                     layers = [],
5276                     latlng, latlngs, i, len, layer;
5277
5278                 switch (geometry.type) {
5279                 case 'Point':
5280                         latlng = this.coordsToLatLng(coords);
5281                         return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng);
5282
5283                 case 'MultiPoint':
5284                         for (i = 0, len = coords.length; i < len; i++) {
5285                                 latlng = this.coordsToLatLng(coords[i]);
5286                                 layer = pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng);
5287                                 layers.push(layer);
5288                         }
5289                         return new L.FeatureGroup(layers);
5290
5291                 case 'LineString':
5292                         latlngs = this.coordsToLatLngs(coords);
5293                         return new L.Polyline(latlngs);
5294
5295                 case 'Polygon':
5296                         latlngs = this.coordsToLatLngs(coords, 1);
5297                         return new L.Polygon(latlngs);
5298
5299                 case 'MultiLineString':
5300                         latlngs = this.coordsToLatLngs(coords, 1);
5301                         return new L.MultiPolyline(latlngs);
5302
5303                 case "MultiPolygon":
5304                         latlngs = this.coordsToLatLngs(coords, 2);
5305                         return new L.MultiPolygon(latlngs);
5306
5307                 case "GeometryCollection":
5308                         for (i = 0, len = geometry.geometries.length; i < len; i++) {
5309                                 layer = this.geometryToLayer(geometry.geometries[i], pointToLayer);
5310                                 layers.push(layer);
5311                         }
5312                         return new L.FeatureGroup(layers);
5313
5314                 default:
5315                         throw new Error('Invalid GeoJSON object.');
5316                 }
5317         },
5318
5319         coordsToLatLng: function (coords, reverse) { // (Array, Boolean) -> LatLng
5320                 var lat = parseFloat(coords[reverse ? 0 : 1]),
5321                     lng = parseFloat(coords[reverse ? 1 : 0]);
5322
5323                 return new L.LatLng(lat, lng, true);
5324         },
5325
5326         coordsToLatLngs: function (coords, levelsDeep, reverse) { // (Array, Number, Boolean) -> Array
5327                 var latlng,
5328                     latlngs = [],
5329                     i, len;
5330
5331                 for (i = 0, len = coords.length; i < len; i++) {
5332                         latlng = levelsDeep ?
5333                                         this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) :
5334                                         this.coordsToLatLng(coords[i], reverse);
5335
5336                         latlngs.push(latlng);
5337                 }
5338
5339                 return latlngs;
5340         }
5341 });
5342
5343 L.geoJson = function (geojson, options) {
5344         return new L.GeoJSON(geojson, options);
5345 };
5346
5347
5348 /*
5349  * L.DomEvent contains functions for working with DOM events.
5350  */
5351
5352 L.DomEvent = {
5353         /* inpired by John Resig, Dean Edwards and YUI addEvent implementations */
5354         addListener: function (obj, type, fn, context) { // (HTMLElement, String, Function[, Object])
5355
5356                 var id = L.Util.stamp(fn),
5357                         key = '_leaflet_' + type + id,
5358                         handler, originalHandler, newType;
5359
5360                 if (obj[key]) { return this; }
5361
5362                 handler = function (e) {
5363                         return fn.call(context || obj, e || L.DomEvent._getEvent());
5364                 };
5365
5366                 if (L.Browser.msTouch && type.indexOf('touch') === 0) {
5367                         return this.addMsTouchListener(obj, type, handler, id);
5368                 } else if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
5369                         return this.addDoubleTapListener(obj, handler, id);
5370
5371                 } else if ('addEventListener' in obj) {
5372                         
5373                         if (type === 'mousewheel') {
5374                                 obj.addEventListener('DOMMouseScroll', handler, false);
5375                                 obj.addEventListener(type, handler, false);
5376
5377                         } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
5378
5379                                 originalHandler = handler;
5380                                 newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
5381
5382                                 handler = function (e) {
5383                                         if (!L.DomEvent._checkMouse(obj, e)) { return; }
5384                                         return originalHandler(e);
5385                                 };
5386
5387                                 obj.addEventListener(newType, handler, false);
5388
5389                         } else {
5390                                 obj.addEventListener(type, handler, false);
5391                         }
5392
5393                 } else if ('attachEvent' in obj) {
5394                         obj.attachEvent("on" + type, handler);
5395                 }
5396
5397                 obj[key] = handler;
5398
5399                 return this;
5400         },
5401
5402         removeListener: function (obj, type, fn) {  // (HTMLElement, String, Function)
5403
5404                 var id = L.Util.stamp(fn),
5405                         key = '_leaflet_' + type + id,
5406                         handler = obj[key];
5407
5408                 if (!handler) { return; }
5409
5410                 if (L.Browser.msTouch && type.indexOf('touch') === 0) {
5411                         this.removeMsTouchListener(obj, type, id);
5412                 } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
5413                         this.removeDoubleTapListener(obj, id);
5414
5415                 } else if ('removeEventListener' in obj) {
5416
5417                         if (type === 'mousewheel') {
5418                                 obj.removeEventListener('DOMMouseScroll', handler, false);
5419                                 obj.removeEventListener(type, handler, false);
5420
5421                         } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
5422                                 obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false);
5423                         } else {
5424                                 obj.removeEventListener(type, handler, false);
5425                         }
5426                 } else if ('detachEvent' in obj) {
5427                         obj.detachEvent("on" + type, handler);
5428                 }
5429
5430                 obj[key] = null;
5431
5432                 return this;
5433         },
5434
5435         stopPropagation: function (e) {
5436
5437                 if (e.stopPropagation) {
5438                         e.stopPropagation();
5439                 } else {
5440                         e.cancelBubble = true;
5441                 }
5442                 return this;
5443         },
5444
5445         disableClickPropagation: function (el) {
5446
5447                 var stop = L.DomEvent.stopPropagation;
5448                 
5449                 return L.DomEvent
5450                         .addListener(el, L.Draggable.START, stop)
5451                         .addListener(el, 'click', stop)
5452                         .addListener(el, 'dblclick', stop);
5453         },
5454
5455         preventDefault: function (e) {
5456
5457                 if (e.preventDefault) {
5458                         e.preventDefault();
5459                 } else {
5460                         e.returnValue = false;
5461                 }
5462                 return this;
5463         },
5464
5465         stop: function (e) {
5466                 return L.DomEvent.preventDefault(e).stopPropagation(e);
5467         },
5468
5469         getMousePosition: function (e, container) {
5470
5471                 var body = document.body,
5472                         docEl = document.documentElement,
5473                         x = e.pageX ? e.pageX : e.clientX + body.scrollLeft + docEl.scrollLeft,
5474                         y = e.pageY ? e.pageY : e.clientY + body.scrollTop + docEl.scrollTop,
5475                         pos = new L.Point(x, y);
5476
5477                 return (container ? pos._subtract(L.DomUtil.getViewportOffset(container)) : pos);
5478         },
5479
5480         getWheelDelta: function (e) {
5481
5482                 var delta = 0;
5483
5484                 if (e.wheelDelta) {
5485                         delta = e.wheelDelta / 120;
5486                 }
5487                 if (e.detail) {
5488                         delta = -e.detail / 3;
5489                 }
5490                 return delta;
5491         },
5492
5493         // check if element really left/entered the event target (for mouseenter/mouseleave)
5494         _checkMouse: function (el, e) {
5495
5496                 var related = e.relatedTarget;
5497
5498                 if (!related) { return true; }
5499
5500                 try {
5501                         while (related && (related !== el)) {
5502                                 related = related.parentNode;
5503                         }
5504                 } catch (err) {
5505                         return false;
5506                 }
5507                 return (related !== el);
5508         },
5509
5510         /*jshint noarg:false */
5511         _getEvent: function () { // evil magic for IE
5512
5513                 var e = window.event;
5514                 if (!e) {
5515                         var caller = arguments.callee.caller;
5516                         while (caller) {
5517                                 e = caller['arguments'][0];
5518                                 if (e && window.Event === e.constructor) {
5519                                         break;
5520                                 }
5521                                 caller = caller.caller;
5522                         }
5523                 }
5524                 return e;
5525         }
5526         /*jshint noarg:false */
5527 };
5528
5529 L.DomEvent.on = L.DomEvent.addListener;
5530 L.DomEvent.off = L.DomEvent.removeListener;
5531
5532 /*
5533  * L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too.
5534  */
5535
5536 L.Draggable = L.Class.extend({
5537         includes: L.Mixin.Events,
5538
5539         statics: {
5540                 START: L.Browser.touch ? 'touchstart' : 'mousedown',
5541                 END: L.Browser.touch ? 'touchend' : 'mouseup',
5542                 MOVE: L.Browser.touch ? 'touchmove' : 'mousemove',
5543                 TAP_TOLERANCE: 15
5544         },
5545
5546         initialize: function (element, dragStartTarget) {
5547                 this._element = element;
5548                 this._dragStartTarget = dragStartTarget || element;
5549         },
5550
5551         enable: function () {
5552                 if (this._enabled) {
5553                         return;
5554                 }
5555                 L.DomEvent.on(this._dragStartTarget, L.Draggable.START, this._onDown, this);
5556                 this._enabled = true;
5557         },
5558
5559         disable: function () {
5560                 if (!this._enabled) {
5561                         return;
5562                 }
5563                 L.DomEvent.off(this._dragStartTarget, L.Draggable.START, this._onDown);
5564                 this._enabled = false;
5565                 this._moved = false;
5566         },
5567
5568         _onDown: function (e) {
5569                 if ((!L.Browser.touch && e.shiftKey) ||
5570                         ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; }
5571
5572                 L.DomEvent.preventDefault(e);
5573
5574                 if (L.Draggable._disabled) { return; }
5575
5576                 this._simulateClick = true;
5577
5578                 if (e.touches && e.touches.length > 1) {
5579                         this._simulateClick = false;
5580                         return;
5581                 }
5582
5583                 var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
5584                         el = first.target;
5585
5586                 if (L.Browser.touch && el.tagName.toLowerCase() === 'a') {
5587                         L.DomUtil.addClass(el, 'leaflet-active');
5588                 }
5589
5590                 this._moved = false;
5591                 if (this._moving) {
5592                         return;
5593                 }
5594
5595                 this._startPoint = new L.Point(first.clientX, first.clientY);
5596                 this._startPos = this._newPos = L.DomUtil.getPosition(this._element);
5597
5598                 L.DomEvent.on(document, L.Draggable.MOVE, this._onMove, this);
5599                 L.DomEvent.on(document, L.Draggable.END, this._onUp, this);
5600         },
5601
5602         _onMove: function (e) {
5603                 if (e.touches && e.touches.length > 1) { return; }
5604
5605                 var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
5606                         newPoint = new L.Point(first.clientX, first.clientY),
5607                         diffVec = newPoint.subtract(this._startPoint);
5608
5609                 if (!diffVec.x && !diffVec.y) { return; }
5610
5611                 L.DomEvent.preventDefault(e);
5612
5613                 if (!this._moved) {
5614                         this.fire('dragstart');
5615                         this._moved = true;
5616
5617                         this._startPos = L.DomUtil.getPosition(this._element).subtract(diffVec);
5618
5619                         if (!L.Browser.touch) {
5620                                 L.DomUtil.disableTextSelection();
5621                                 this._setMovingCursor();
5622                         }
5623                 }
5624
5625                 this._newPos = this._startPos.add(diffVec);
5626                 this._moving = true;
5627
5628                 L.Util.cancelAnimFrame(this._animRequest);
5629                 this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
5630         },
5631
5632         _updatePosition: function () {
5633                 this.fire('predrag');
5634                 L.DomUtil.setPosition(this._element, this._newPos);
5635                 this.fire('drag');
5636         },
5637
5638         _onUp: function (e) {
5639                 var simulateClickTouch;
5640                 if (this._simulateClick && e.changedTouches) {
5641                         var first = e.changedTouches[0],
5642                                 el = first.target,
5643                                 dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0;
5644
5645                         if (el.tagName.toLowerCase() === 'a') {
5646                                 L.DomUtil.removeClass(el, 'leaflet-active');
5647                         }
5648
5649                         if (dist < L.Draggable.TAP_TOLERANCE) {
5650                                 simulateClickTouch = first;
5651                         }
5652                 }
5653
5654                 if (!L.Browser.touch) {
5655                         L.DomUtil.enableTextSelection();
5656                         this._restoreCursor();
5657                 }
5658
5659                 L.DomEvent.off(document, L.Draggable.MOVE, this._onMove);
5660                 L.DomEvent.off(document, L.Draggable.END, this._onUp);
5661
5662                 if (this._moved) {
5663                         // ensure drag is not fired after dragend
5664                         L.Util.cancelAnimFrame(this._animRequest);
5665
5666                         this.fire('dragend');
5667                 }
5668                 this._moving = false;
5669
5670                 if (simulateClickTouch) {
5671                         this._moved = false;
5672                         this._simulateEvent('click', simulateClickTouch);
5673                 }
5674         },
5675
5676         _setMovingCursor: function () {
5677                 L.DomUtil.addClass(document.body, 'leaflet-dragging');
5678         },
5679
5680         _restoreCursor: function () {
5681                 L.DomUtil.removeClass(document.body, 'leaflet-dragging');
5682         },
5683
5684         _simulateEvent: function (type, e) {
5685                 var simulatedEvent = document.createEvent('MouseEvents');
5686
5687                 simulatedEvent.initMouseEvent(
5688                                 type, true, true, window, 1,
5689                                 e.screenX, e.screenY,
5690                                 e.clientX, e.clientY,
5691                                 false, false, false, false, 0, null);
5692
5693                 e.target.dispatchEvent(simulatedEvent);
5694         }
5695 });
5696
5697
5698 /*
5699  * L.Handler classes are used internally to inject interaction features to classes like Map and Marker.
5700  */
5701
5702 L.Handler = L.Class.extend({
5703         initialize: function (map) {
5704                 this._map = map;
5705         },
5706
5707         enable: function () {
5708                 if (this._enabled) { return; }
5709
5710                 this._enabled = true;
5711                 this.addHooks();
5712         },
5713
5714         disable: function () {
5715                 if (!this._enabled) { return; }
5716
5717                 this._enabled = false;
5718                 this.removeHooks();
5719         },
5720
5721         enabled: function () {
5722                 return !!this._enabled;
5723         }
5724 });
5725
5726
5727 /*
5728  * L.Handler.MapDrag is used internally by L.Map to make the map draggable.
5729  */
5730
5731 L.Map.mergeOptions({
5732         dragging: true,
5733
5734         inertia: !L.Browser.android23,
5735         inertiaDeceleration: 3400, // px/s^2
5736         inertiaMaxSpeed: 6000, // px/s
5737         inertiaThreshold: L.Browser.touch ? 32 : 18, // ms
5738
5739         // TODO refactor, move to CRS
5740         worldCopyJump: true
5741 });
5742
5743 L.Map.Drag = L.Handler.extend({
5744         addHooks: function () {
5745                 if (!this._draggable) {
5746                         this._draggable = new L.Draggable(this._map._mapPane, this._map._container);
5747
5748                         this._draggable.on({
5749                                 'dragstart': this._onDragStart,
5750                                 'drag': this._onDrag,
5751                                 'dragend': this._onDragEnd
5752                         }, this);
5753
5754                         var options = this._map.options;
5755
5756                         if (options.worldCopyJump) {
5757                                 this._draggable.on('predrag', this._onPreDrag, this);
5758                                 this._map.on('viewreset', this._onViewReset, this);
5759                         }
5760                 }
5761                 this._draggable.enable();
5762         },
5763
5764         removeHooks: function () {
5765                 this._draggable.disable();
5766         },
5767
5768         moved: function () {
5769                 return this._draggable && this._draggable._moved;
5770         },
5771
5772         _onDragStart: function () {
5773                 var map = this._map;
5774
5775                 if (map._panAnim) {
5776                         map._panAnim.stop();
5777                 }
5778
5779                 map
5780                         .fire('movestart')
5781                         .fire('dragstart');
5782
5783                 if (map.options.inertia) {
5784                         this._positions = [];
5785                         this._times = [];
5786                 }
5787         },
5788
5789         _onDrag: function () {
5790                 if (this._map.options.inertia) {
5791                         var time = this._lastTime = +new Date(),
5792                             pos = this._lastPos = this._draggable._newPos;
5793
5794                         this._positions.push(pos);
5795                         this._times.push(time);
5796
5797                         if (time - this._times[0] > 200) {
5798                                 this._positions.shift();
5799                                 this._times.shift();
5800                         }
5801                 }
5802
5803                 this._map
5804                         .fire('move')
5805                         .fire('drag');
5806         },
5807
5808         _onViewReset: function () {
5809                 var pxCenter = this._map.getSize()._divideBy(2),
5810                         pxWorldCenter = this._map.latLngToLayerPoint(new L.LatLng(0, 0));
5811
5812                 this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
5813                 this._worldWidth = this._map.project(new L.LatLng(0, 180)).x;
5814         },
5815
5816         _onPreDrag: function () {
5817                 // TODO refactor to be able to adjust map pane position after zoom
5818                 var map = this._map,
5819                         worldWidth = this._worldWidth,
5820                         halfWidth = Math.round(worldWidth / 2),
5821                         dx = this._initialWorldOffset,
5822                         x = this._draggable._newPos.x,
5823                         newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
5824                         newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
5825                         newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
5826
5827                 this._draggable._newPos.x = newX;
5828         },
5829
5830         _onDragEnd: function () {
5831                 var map = this._map,
5832                         options = map.options,
5833                         delay = +new Date() - this._lastTime,
5834
5835                         noInertia = !options.inertia ||
5836                                         delay > options.inertiaThreshold ||
5837                                         this._positions[0] === undefined;
5838
5839                 if (noInertia) {
5840                         map.fire('moveend');
5841
5842                 } else {
5843
5844                         var direction = this._lastPos.subtract(this._positions[0]),
5845                                 duration = (this._lastTime + delay - this._times[0]) / 1000,
5846
5847                                 speedVector = direction.multiplyBy(0.58 / duration),
5848                                 speed = speedVector.distanceTo(new L.Point(0, 0)),
5849
5850                                 limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
5851                                 limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
5852
5853                                 decelerationDuration = limitedSpeed / options.inertiaDeceleration,
5854                                 offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
5855
5856                         L.Util.requestAnimFrame(L.Util.bind(function () {
5857                                 this._map.panBy(offset, decelerationDuration);
5858                         }, this));
5859                 }
5860
5861                 map.fire('dragend');
5862
5863                 if (options.maxBounds) {
5864                         // TODO predrag validation instead of animation
5865                         L.Util.requestAnimFrame(this._panInsideMaxBounds, map, true, map._container);
5866                 }
5867         },
5868
5869         _panInsideMaxBounds: function () {
5870                 this.panInsideBounds(this.options.maxBounds);
5871         }
5872 });
5873
5874 L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);
5875
5876
5877 /*
5878  * L.Handler.DoubleClickZoom is used internally by L.Map to add double-click zooming.
5879  */
5880
5881 L.Map.mergeOptions({
5882         doubleClickZoom: true
5883 });
5884
5885 L.Map.DoubleClickZoom = L.Handler.extend({
5886         addHooks: function () {
5887                 this._map.on('dblclick', this._onDoubleClick);
5888         },
5889
5890         removeHooks: function () {
5891                 this._map.off('dblclick', this._onDoubleClick);
5892         },
5893
5894         _onDoubleClick: function (e) {
5895                 this.setView(e.latlng, this._zoom + 1);
5896         }
5897 });
5898
5899 L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom);
5900
5901 /*
5902  * L.Handler.ScrollWheelZoom is used internally by L.Map to enable mouse scroll wheel zooming on the map.
5903  */
5904
5905 L.Map.mergeOptions({
5906         scrollWheelZoom: !L.Browser.touch || L.Browser.msTouch
5907 });
5908
5909 L.Map.ScrollWheelZoom = L.Handler.extend({
5910         addHooks: function () {
5911                 L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this);
5912                 this._delta = 0;
5913         },
5914
5915         removeHooks: function () {
5916                 L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll);
5917         },
5918
5919         _onWheelScroll: function (e) {
5920                 var delta = L.DomEvent.getWheelDelta(e);
5921
5922                 this._delta += delta;
5923                 this._lastMousePos = this._map.mouseEventToContainerPoint(e);
5924
5925                 if (!this._startTime) {
5926                         this._startTime = +new Date();
5927                 }
5928
5929                 var left = Math.max(40 - (+new Date() - this._startTime), 0);
5930
5931                 clearTimeout(this._timer);
5932                 this._timer = setTimeout(L.Util.bind(this._performZoom, this), left);
5933
5934                 L.DomEvent.preventDefault(e);
5935                 L.DomEvent.stopPropagation(e);
5936         },
5937
5938         _performZoom: function () {
5939                 var map = this._map,
5940                         delta = Math.round(this._delta),
5941                         zoom = map.getZoom();
5942
5943                 delta = Math.max(Math.min(delta, 4), -4);
5944                 delta = map._limitZoom(zoom + delta) - zoom;
5945
5946                 this._delta = 0;
5947
5948                 this._startTime = null;
5949
5950                 if (!delta) { return; }
5951
5952                 var newZoom = zoom + delta,
5953                         newCenter = this._getCenterForScrollWheelZoom(newZoom);
5954
5955                 map.setView(newCenter, newZoom);
5956         },
5957
5958         _getCenterForScrollWheelZoom: function (newZoom) {
5959                 var map = this._map,
5960                         scale = map.getZoomScale(newZoom),
5961                         viewHalf = map.getSize()._divideBy(2),
5962                         centerOffset = this._lastMousePos._subtract(viewHalf)._multiplyBy(1 - 1 / scale),
5963                         newCenterPoint = map._getTopLeftPoint()._add(viewHalf)._add(centerOffset);
5964
5965                 return map.unproject(newCenterPoint);
5966         }
5967 });
5968
5969 L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);
5970
5971
5972 L.Util.extend(L.DomEvent, {
5973
5974         _touchstart: L.Browser.msTouch ? 'MSPointerDown' : 'touchstart',
5975         _touchend: L.Browser.msTouch ? 'MSPointerUp' : 'touchend',
5976
5977         // inspired by Zepto touch code by Thomas Fuchs
5978         addDoubleTapListener: function (obj, handler, id) {
5979                 var last,
5980                         doubleTap = false,
5981                         delay = 250,
5982                         touch,
5983                         pre = '_leaflet_',
5984                         touchstart = this._touchstart,
5985                         touchend = this._touchend,
5986                         trackedTouches = [];
5987
5988                 function onTouchStart(e) {
5989                         var count;
5990                         if (L.Browser.msTouch) {
5991                                 trackedTouches.push(e.pointerId);
5992                                 count = trackedTouches.length;
5993                         } else {
5994                                 count = e.touches.length;
5995                         }
5996                         if (count > 1) {
5997                                 return;
5998                         }
5999
6000                         var now = Date.now(),
6001                                 delta = now - (last || now);
6002
6003                         touch = e.touches ? e.touches[0] : e;
6004                         doubleTap = (delta > 0 && delta <= delay);
6005                         last = now;
6006                 }
6007                 function onTouchEnd(e) {
6008                         if (L.Browser.msTouch) {
6009                                 var idx = trackedTouches.indexOf(e.pointerId);
6010                                 if (idx === -1) {
6011                                         return;
6012                                 }
6013                                 trackedTouches.splice(idx, 1);
6014                         }
6015
6016                         if (doubleTap) {
6017                                 if (L.Browser.msTouch) {
6018                                         //Work around .type being readonly with MSPointer* events
6019                                         var newTouch = { },
6020                                                 prop;
6021                                         for (var i in touch) {
6022                                                 if (true) { //Make JSHint happy, we want to copy all properties
6023                                                         prop = touch[i];
6024                                                         if (typeof prop === 'function') {
6025                                                                 newTouch[i] = prop.bind(touch);
6026                                                         } else {
6027                                                                 newTouch[i] = prop;
6028                                                         }
6029                                                 }
6030                                         }
6031                                         touch = newTouch;
6032                                 }
6033                                 touch.type = 'dblclick';
6034                                 handler(touch);
6035                                 last = null;
6036                         }
6037                 }
6038                 obj[pre + touchstart + id] = onTouchStart;
6039                 obj[pre + touchend + id] = onTouchEnd;
6040
6041                 //On msTouch we need to listen on the document otherwise a drag starting on the map and moving off screen will not come through to us
6042                 // so we will lose track of how many touches are ongoing
6043                 var endElement = L.Browser.msTouch ? document.documentElement : obj;
6044
6045                 obj.addEventListener(touchstart, onTouchStart, false);
6046                 endElement.addEventListener(touchend, onTouchEnd, false);
6047                 if (L.Browser.msTouch) {
6048                         endElement.addEventListener('MSPointerCancel', onTouchEnd, false);
6049                 }
6050                 return this;
6051         },
6052
6053         removeDoubleTapListener: function (obj, id) {
6054                 var pre = '_leaflet_';
6055                 obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false);
6056                 (L.Browser.msTouch ? document.documentElement : obj).removeEventListener(this._touchend, obj[pre + this._touchend + id], false);
6057                 if (L.Browser.msTouch) {
6058                         document.documentElement.removeEventListener('MSPointerCancel', obj[pre + this._touchend + id], false);
6059                 }
6060                 return this;
6061         }
6062 });
6063
6064
6065 L.Util.extend(L.DomEvent, {
6066
6067         _msTouches: [],
6068         _msDocumentListener: false,
6069
6070         // Provides a touch events wrapper for msPointer events.
6071         // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019
6072
6073         addMsTouchListener: function (obj, type, handler, id) {
6074
6075                 switch (type) {
6076                 case 'touchstart':
6077                         return this.addMsTouchListenerStart(obj, type, handler, id);
6078                 case 'touchend':
6079                         return this.addMsTouchListenerEnd(obj, type, handler, id);
6080                 case 'touchmove':
6081                         return this.addMsTouchListenerMove(obj, type, handler, id);
6082                 default:
6083                         throw 'Unknown touch event type';
6084                 }
6085         },
6086
6087         addMsTouchListenerStart: function (obj, type, handler, id) {
6088                 var pre = '_leaflet_',
6089                         touches = this._msTouches;
6090
6091                 var cb = function (e) {
6092
6093                         var alreadyInArray = false;
6094                         for (var i = 0; i < touches.length; i++) {
6095                                 if (touches[i].pointerId === e.pointerId) {
6096                                         alreadyInArray = true;
6097                                         break;
6098                                 }
6099                         }
6100                         if (!alreadyInArray) {
6101                                 touches.push(e);
6102                         }
6103
6104                         e.touches = touches.slice();
6105                         e.changedTouches = [e];
6106
6107                         handler(e);
6108                 };
6109
6110                 obj[pre + 'touchstart' + id] = cb;
6111                 obj.addEventListener('MSPointerDown', cb, false);
6112
6113                 //Need to also listen for end events to keep the _msTouches list accurate
6114                 //this needs to be on the body and never go away
6115                 if (!this._msDocumentListener) {
6116                         var internalCb = function (e) {
6117                                 for (var i = 0; i < touches.length; i++) {
6118                                         if (touches[i].pointerId === e.pointerId) {
6119                                                 touches.splice(i, 1);
6120                                                 break;
6121                                         }
6122                                 }
6123                         };
6124                         //We listen on the documentElement as any drags that end by moving the touch off the screen get fired there
6125                         document.documentElement.addEventListener('MSPointerUp', internalCb, false);
6126                         document.documentElement.addEventListener('MSPointerCancel', internalCb, false);
6127
6128                         this._msDocumentListener = true;
6129                 }
6130
6131                 return this;
6132         },
6133
6134         addMsTouchListenerMove: function (obj, type, handler, id) {
6135                 var pre = '_leaflet_';
6136
6137                 var touches = this._msTouches;
6138                 var cb = function (e) {
6139
6140                         //Don't fire touch moves when mouse isn't down
6141                         if (e.pointerType === e.MSPOINTER_TYPE_MOUSE && e.buttons === 0) {
6142                                 return;
6143                         }
6144
6145                         for (var i = 0; i < touches.length; i++) {
6146                                 if (touches[i].pointerId === e.pointerId) {
6147                                         touches[i] = e;
6148                                         break;
6149                                 }
6150                         }
6151
6152                         e.touches = touches.slice();
6153                         e.changedTouches = [e];
6154
6155                         handler(e);
6156                 };
6157
6158                 obj[pre + 'touchmove' + id] = cb;
6159                 obj.addEventListener('MSPointerMove', cb, false);
6160
6161                 return this;
6162         },
6163
6164         addMsTouchListenerEnd: function (obj, type, handler, id) {
6165                 var pre = '_leaflet_',
6166                         touches = this._msTouches;
6167
6168                 var cb = function (e) {
6169                         for (var i = 0; i < touches.length; i++) {
6170                                 if (touches[i].pointerId === e.pointerId) {
6171                                         touches.splice(i, 1);
6172                                         break;
6173                                 }
6174                         }
6175
6176                         e.touches = touches.slice();
6177                         e.changedTouches = [e];
6178
6179                         handler(e);
6180                 };
6181
6182                 obj[pre + 'touchend' + id] = cb;
6183                 obj.addEventListener('MSPointerUp', cb, false);
6184                 obj.addEventListener('MSPointerCancel', cb, false);
6185
6186                 return this;
6187         },
6188
6189         removeMsTouchListener: function (obj, type, id) {
6190                 var pre = '_leaflet_',
6191                     cb = obj[pre + type + id];
6192
6193                 switch (type) {
6194                 case 'touchstart':
6195                         obj.removeEventListener('MSPointerDown', cb, false);
6196                         break;
6197                 case 'touchmove':
6198                         obj.removeEventListener('MSPointerMove', cb, false);
6199                         break;
6200                 case 'touchend':
6201                         obj.removeEventListener('MSPointerUp', cb, false);
6202                         obj.removeEventListener('MSPointerCancel', cb, false);
6203                         break;
6204                 }
6205
6206                 return this;
6207         }
6208 });
6209
6210
6211 /*
6212  * L.Handler.TouchZoom is used internally by L.Map to add touch-zooming on Webkit-powered mobile browsers.
6213  */
6214
6215 L.Map.mergeOptions({
6216         touchZoom: L.Browser.touch && !L.Browser.android23
6217 });
6218
6219 L.Map.TouchZoom = L.Handler.extend({
6220         addHooks: function () {
6221                 L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
6222         },
6223
6224         removeHooks: function () {
6225                 L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
6226         },
6227
6228         _onTouchStart: function (e) {
6229                 var map = this._map;
6230
6231                 if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
6232
6233                 var p1 = map.mouseEventToLayerPoint(e.touches[0]),
6234                         p2 = map.mouseEventToLayerPoint(e.touches[1]),
6235                         viewCenter = map._getCenterLayerPoint();
6236
6237                 this._startCenter = p1.add(p2)._divideBy(2);
6238                 this._startDist = p1.distanceTo(p2);
6239
6240                 this._moved = false;
6241                 this._zooming = true;
6242
6243                 this._centerOffset = viewCenter.subtract(this._startCenter);
6244
6245                 if (map._panAnim) {
6246                         map._panAnim.stop();
6247                 }
6248
6249                 L.DomEvent
6250                         .on(document, 'touchmove', this._onTouchMove, this)
6251                         .on(document, 'touchend', this._onTouchEnd, this);
6252
6253                 L.DomEvent.preventDefault(e);
6254         },
6255
6256         _onTouchMove: function (e) {
6257                 if (!e.touches || e.touches.length !== 2) { return; }
6258
6259                 var map = this._map;
6260
6261                 var p1 = map.mouseEventToLayerPoint(e.touches[0]),
6262                         p2 = map.mouseEventToLayerPoint(e.touches[1]);
6263
6264                 this._scale = p1.distanceTo(p2) / this._startDist;
6265                 this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter);
6266
6267                 if (this._scale === 1) { return; }
6268
6269                 if (!this._moved) {
6270                         L.DomUtil.addClass(map._mapPane, 'leaflet-zoom-anim leaflet-touching');
6271
6272                         map
6273                                 .fire('movestart')
6274                                 .fire('zoomstart')
6275                                 ._prepareTileBg();
6276
6277                         this._moved = true;
6278                 }
6279
6280                 L.Util.cancelAnimFrame(this._animRequest);
6281                 this._animRequest = L.Util.requestAnimFrame(this._updateOnMove, this, true, this._map._container);
6282
6283                 L.DomEvent.preventDefault(e);
6284         },
6285
6286         _updateOnMove: function () {
6287                 var map = this._map,
6288                         origin = this._getScaleOrigin(),
6289                         center = map.layerPointToLatLng(origin);
6290
6291                 map.fire('zoomanim', {
6292                         center: center,
6293                         zoom: map.getScaleZoom(this._scale)
6294                 });
6295
6296                 // Used 2 translates instead of transform-origin because of a very strange bug -
6297                 // it didn't count the origin on the first touch-zoom but worked correctly afterwards
6298
6299                 map._tileBg.style[L.DomUtil.TRANSFORM] =
6300                         L.DomUtil.getTranslateString(this._delta) + ' ' +
6301                         L.DomUtil.getScaleString(this._scale, this._startCenter);
6302         },
6303
6304         _onTouchEnd: function (e) {
6305                 if (!this._moved || !this._zooming) { return; }
6306
6307                 var map = this._map;
6308
6309                 this._zooming = false;
6310                 L.DomUtil.removeClass(map._mapPane, 'leaflet-touching');
6311
6312                 L.DomEvent
6313                         .off(document, 'touchmove', this._onTouchMove)
6314                         .off(document, 'touchend', this._onTouchEnd);
6315
6316                 var origin = this._getScaleOrigin(),
6317                         center = map.layerPointToLatLng(origin),
6318
6319                         oldZoom = map.getZoom(),
6320                         floatZoomDelta = map.getScaleZoom(this._scale) - oldZoom,
6321                         roundZoomDelta = (floatZoomDelta > 0 ? Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
6322                         zoom = map._limitZoom(oldZoom + roundZoomDelta);
6323
6324                 map.fire('zoomanim', {
6325                         center: center,
6326                         zoom: zoom
6327                 });
6328
6329                 map._runAnimation(center, zoom, map.getZoomScale(zoom) / this._scale, origin, true);
6330         },
6331
6332         _getScaleOrigin: function () {
6333                 var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale);
6334                 return this._startCenter.add(centerOffset);
6335         }
6336 });
6337
6338 L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
6339
6340
6341 /*
6342  * L.Handler.ShiftDragZoom is used internally by L.Map to add shift-drag zoom (zoom to a selected bounding box).
6343  */
6344
6345 L.Map.mergeOptions({
6346         boxZoom: true
6347 });
6348
6349 L.Map.BoxZoom = L.Handler.extend({
6350         initialize: function (map) {
6351                 this._map = map;
6352                 this._container = map._container;
6353                 this._pane = map._panes.overlayPane;
6354         },
6355
6356         addHooks: function () {
6357                 L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
6358         },
6359
6360         removeHooks: function () {
6361                 L.DomEvent.off(this._container, 'mousedown', this._onMouseDown);
6362         },
6363
6364         _onMouseDown: function (e) {
6365                 if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
6366
6367                 L.DomUtil.disableTextSelection();
6368
6369                 this._startLayerPoint = this._map.mouseEventToLayerPoint(e);
6370
6371                 this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane);
6372                 L.DomUtil.setPosition(this._box, this._startLayerPoint);
6373
6374                 //TODO refactor: move cursor to styles
6375                 this._container.style.cursor = 'crosshair';
6376
6377                 L.DomEvent
6378                         .on(document, 'mousemove', this._onMouseMove, this)
6379                         .on(document, 'mouseup', this._onMouseUp, this)
6380                         .preventDefault(e);
6381                         
6382                 this._map.fire("boxzoomstart");
6383         },
6384
6385         _onMouseMove: function (e) {
6386                 var startPoint = this._startLayerPoint,
6387                         box = this._box,
6388
6389                         layerPoint = this._map.mouseEventToLayerPoint(e),
6390                         offset = layerPoint.subtract(startPoint),
6391
6392                         newPos = new L.Point(
6393                                 Math.min(layerPoint.x, startPoint.x),
6394                                 Math.min(layerPoint.y, startPoint.y));
6395
6396                 L.DomUtil.setPosition(box, newPos);
6397
6398                 // TODO refactor: remove hardcoded 4 pixels
6399                 box.style.width  = (Math.max(0, Math.abs(offset.x) - 4)) + 'px';
6400                 box.style.height = (Math.max(0, Math.abs(offset.y) - 4)) + 'px';
6401         },
6402
6403         _onMouseUp: function (e) {
6404                 this._pane.removeChild(this._box);
6405                 this._container.style.cursor = '';
6406
6407                 L.DomUtil.enableTextSelection();
6408
6409                 L.DomEvent
6410                         .off(document, 'mousemove', this._onMouseMove)
6411                         .off(document, 'mouseup', this._onMouseUp);
6412
6413                 var map = this._map,
6414                         layerPoint = map.mouseEventToLayerPoint(e);
6415
6416                 var bounds = new L.LatLngBounds(
6417                                 map.layerPointToLatLng(this._startLayerPoint),
6418                                 map.layerPointToLatLng(layerPoint));
6419
6420                 map.fitBounds(bounds);
6421                 
6422                 map.fire("boxzoomend", {
6423                         boxZoomBounds: bounds
6424                 });
6425         }
6426 });
6427
6428 L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom);
6429
6430
6431 L.Map.mergeOptions({
6432         keyboard: true,
6433         keyboardPanOffset: 80,
6434         keyboardZoomOffset: 1
6435 });
6436
6437 L.Map.Keyboard = L.Handler.extend({
6438
6439         // list of e.keyCode values for particular actions
6440         keyCodes: {
6441                 left:    [37],
6442                 right:   [39],
6443                 down:    [40],
6444                 up:      [38],
6445                 zoomIn:  [187, 107, 61],
6446                 zoomOut: [189, 109]
6447         },
6448
6449         initialize: function (map) {
6450                 this._map = map;
6451
6452                 this._setPanOffset(map.options.keyboardPanOffset);
6453                 this._setZoomOffset(map.options.keyboardZoomOffset);
6454         },
6455
6456         addHooks: function () {
6457                 var container = this._map._container;
6458
6459                 // make the container focusable by tabbing
6460                 if (container.tabIndex === -1) {
6461                         container.tabIndex = "0";
6462                 }
6463
6464                 L.DomEvent
6465                         .addListener(container, 'focus', this._onFocus, this)
6466                         .addListener(container, 'blur', this._onBlur, this)
6467                         .addListener(container, 'mousedown', this._onMouseDown, this);
6468
6469                 this._map
6470                         .on('focus', this._addHooks, this)
6471                         .on('blur', this._removeHooks, this);
6472         },
6473
6474         removeHooks: function () {
6475                 this._removeHooks();
6476
6477                 var container = this._map._container;
6478                 L.DomEvent
6479                         .removeListener(container, 'focus', this._onFocus, this)
6480                         .removeListener(container, 'blur', this._onBlur, this)
6481                         .removeListener(container, 'mousedown', this._onMouseDown, this);
6482
6483                 this._map
6484                         .off('focus', this._addHooks, this)
6485                         .off('blur', this._removeHooks, this);
6486         },
6487
6488         _onMouseDown: function () {
6489                 if (!this._focused) {
6490                         this._map._container.focus();
6491                 }
6492         },
6493
6494         _onFocus: function () {
6495                 this._focused = true;
6496                 this._map.fire('focus');
6497         },
6498
6499         _onBlur: function () {
6500                 this._focused = false;
6501                 this._map.fire('blur');
6502         },
6503
6504         _setPanOffset: function (pan) {
6505                 var keys = this._panKeys = {},
6506                     codes = this.keyCodes,
6507                     i, len;
6508
6509                 for (i = 0, len = codes.left.length; i < len; i++) {
6510                         keys[codes.left[i]] = [-1 * pan, 0];
6511                 }
6512                 for (i = 0, len = codes.right.length; i < len; i++) {
6513                         keys[codes.right[i]] = [pan, 0];
6514                 }
6515                 for (i = 0, len = codes.down.length; i < len; i++) {
6516                         keys[codes.down[i]] = [0, pan];
6517                 }
6518                 for (i = 0, len = codes.up.length; i < len; i++) {
6519                         keys[codes.up[i]] = [0, -1 * pan];
6520                 }
6521         },
6522
6523         _setZoomOffset: function (zoom) {
6524                 var keys = this._zoomKeys = {},
6525                         codes = this.keyCodes,
6526                     i, len;
6527
6528                 for (i = 0, len = codes.zoomIn.length; i < len; i++) {
6529                         keys[codes.zoomIn[i]] = zoom;
6530                 }
6531                 for (i = 0, len = codes.zoomOut.length; i < len; i++) {
6532                         keys[codes.zoomOut[i]] = -zoom;
6533                 }
6534         },
6535
6536         _addHooks: function () {
6537                 L.DomEvent.addListener(document, 'keydown', this._onKeyDown, this);
6538         },
6539
6540         _removeHooks: function () {
6541                 L.DomEvent.removeListener(document, 'keydown', this._onKeyDown, this);
6542         },
6543
6544         _onKeyDown: function (e) {
6545                 var key = e.keyCode;
6546
6547                 if (this._panKeys.hasOwnProperty(key)) {
6548                         this._map.panBy(this._panKeys[key]);
6549
6550                 } else if (this._zoomKeys.hasOwnProperty(key)) {
6551                         this._map.setZoom(this._map.getZoom() + this._zoomKeys[key]);
6552
6553                 } else {
6554                         return;
6555                 }
6556
6557                 L.DomEvent.stop(e);
6558         }
6559 });
6560
6561 L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard);
6562
6563
6564 /*
6565  * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
6566  */
6567
6568 L.Handler.MarkerDrag = L.Handler.extend({
6569         initialize: function (marker) {
6570                 this._marker = marker;
6571         },
6572
6573         addHooks: function () {
6574                 var icon = this._marker._icon;
6575                 if (!this._draggable) {
6576                         this._draggable = new L.Draggable(icon, icon)
6577                                 .on('dragstart', this._onDragStart, this)
6578                                 .on('drag', this._onDrag, this)
6579                                 .on('dragend', this._onDragEnd, this);
6580                 }
6581                 this._draggable.enable();
6582         },
6583
6584         removeHooks: function () {
6585                 this._draggable.disable();
6586         },
6587
6588         moved: function () {
6589                 return this._draggable && this._draggable._moved;
6590         },
6591
6592         _onDragStart: function (e) {
6593                 this._marker
6594                         .closePopup()
6595                         .fire('movestart')
6596                         .fire('dragstart');
6597         },
6598
6599         _onDrag: function (e) {
6600                 var marker = this._marker,
6601                         shadow = marker._shadow,
6602                         iconPos = L.DomUtil.getPosition(marker._icon),
6603                         latlng = marker._map.layerPointToLatLng(iconPos);
6604
6605                 // update shadow position
6606                 if (shadow) {
6607                         L.DomUtil.setPosition(shadow, iconPos);
6608                 }
6609
6610                 marker._latlng = latlng;
6611
6612                 marker
6613                         .fire('move', { latlng: latlng })
6614                         .fire('drag');
6615         },
6616
6617         _onDragEnd: function () {
6618                 this._marker
6619                         .fire('moveend')
6620                         .fire('dragend');
6621         }
6622 });
6623
6624
6625 L.Handler.PolyEdit = L.Handler.extend({
6626         options: {
6627                 icon: new L.DivIcon({
6628                         iconSize: new L.Point(8, 8),
6629                         className: 'leaflet-div-icon leaflet-editing-icon'
6630                 })
6631         },
6632
6633         initialize: function (poly, options) {
6634                 this._poly = poly;
6635                 L.Util.setOptions(this, options);
6636         },
6637
6638         addHooks: function () {
6639                 if (this._poly._map) {
6640                         if (!this._markerGroup) {
6641                                 this._initMarkers();
6642                         }
6643                         this._poly._map.addLayer(this._markerGroup);
6644                 }
6645         },
6646
6647         removeHooks: function () {
6648                 if (this._poly._map) {
6649                         this._poly._map.removeLayer(this._markerGroup);
6650                         delete this._markerGroup;
6651                         delete this._markers;
6652                 }
6653         },
6654
6655         updateMarkers: function () {
6656                 this._markerGroup.clearLayers();
6657                 this._initMarkers();
6658         },
6659
6660         _initMarkers: function () {
6661                 if (!this._markerGroup) {
6662                         this._markerGroup = new L.LayerGroup();
6663                 }
6664                 this._markers = [];
6665
6666                 var latlngs = this._poly._latlngs,
6667                     i, j, len, marker;
6668
6669                 // TODO refactor holes implementation in Polygon to support it here
6670
6671                 for (i = 0, len = latlngs.length; i < len; i++) {
6672
6673                         marker = this._createMarker(latlngs[i], i);
6674                         marker.on('click', this._onMarkerClick, this);
6675                         this._markers.push(marker);
6676                 }
6677
6678                 var markerLeft, markerRight;
6679
6680                 for (i = 0, j = len - 1; i < len; j = i++) {
6681                         if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) {
6682                                 continue;
6683                         }
6684
6685                         markerLeft = this._markers[j];
6686                         markerRight = this._markers[i];
6687
6688                         this._createMiddleMarker(markerLeft, markerRight);
6689                         this._updatePrevNext(markerLeft, markerRight);
6690                 }
6691         },
6692
6693         _createMarker: function (latlng, index) {
6694                 var marker = new L.Marker(latlng, {
6695                         draggable: true,
6696                         icon: this.options.icon
6697                 });
6698
6699                 marker._origLatLng = latlng;
6700                 marker._index = index;
6701
6702                 marker.on('drag', this._onMarkerDrag, this);
6703                 marker.on('dragend', this._fireEdit, this);
6704
6705                 this._markerGroup.addLayer(marker);
6706
6707                 return marker;
6708         },
6709
6710         _fireEdit: function () {
6711                 this._poly.fire('edit');
6712         },
6713
6714         _onMarkerDrag: function (e) {
6715                 var marker = e.target;
6716
6717                 L.Util.extend(marker._origLatLng, marker._latlng);
6718
6719                 if (marker._middleLeft) {
6720                         marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker));
6721                 }
6722                 if (marker._middleRight) {
6723                         marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next));
6724                 }
6725
6726                 this._poly.redraw();
6727         },
6728
6729         _onMarkerClick: function (e) {
6730                 // 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
6731                 if (this._poly._latlngs.length < 3) {
6732                         return;
6733                 }
6734
6735                 var marker = e.target,
6736                     i = marker._index;
6737
6738                 // Check existence of previous and next markers since they wouldn't exist for edge points on the polyline
6739                 if (marker._prev && marker._next) {
6740                         this._createMiddleMarker(marker._prev, marker._next);
6741                 } else if (!marker._prev) {
6742                         marker._next._middleLeft = null;
6743                 } else if (!marker._next) {
6744                         marker._prev._middleRight = null;
6745                 }
6746                 this._updatePrevNext(marker._prev, marker._next);
6747
6748                 // The marker itself is guaranteed to exist and present in the layer, since we managed to click on it
6749                 this._markerGroup.removeLayer(marker);
6750                 // Check for the existence of middle left or middle right
6751                 if (marker._middleLeft) {
6752                         this._markerGroup.removeLayer(marker._middleLeft);
6753                 }
6754                 if (marker._middleRight) {
6755                         this._markerGroup.removeLayer(marker._middleRight);
6756                 }
6757                 this._markers.splice(i, 1);
6758                 this._poly.spliceLatLngs(i, 1);
6759                 this._updateIndexes(i, -1);
6760                 this._poly.fire('edit');
6761         },
6762
6763         _updateIndexes: function (index, delta) {
6764                 this._markerGroup.eachLayer(function (marker) {
6765                         if (marker._index > index) {
6766                                 marker._index += delta;
6767                         }
6768                 });
6769         },
6770
6771         _createMiddleMarker: function (marker1, marker2) {
6772                 var latlng = this._getMiddleLatLng(marker1, marker2),
6773                         marker = this._createMarker(latlng),
6774                         onClick,
6775                         onDragStart,
6776                         onDragEnd;
6777
6778                 marker.setOpacity(0.6);
6779
6780                 marker1._middleRight = marker2._middleLeft = marker;
6781
6782                 onDragStart = function () {
6783                         var i = marker2._index;
6784
6785                         marker._index = i;
6786
6787                         marker
6788                                 .off('click', onClick)
6789                                 .on('click', this._onMarkerClick, this);
6790
6791                         latlng.lat = marker.getLatLng().lat;
6792                         latlng.lng = marker.getLatLng().lng;
6793                         this._poly.spliceLatLngs(i, 0, latlng);
6794                         this._markers.splice(i, 0, marker);
6795
6796                         marker.setOpacity(1);
6797
6798                         this._updateIndexes(i, 1);
6799                         marker2._index++;
6800                         this._updatePrevNext(marker1, marker);
6801                         this._updatePrevNext(marker, marker2);
6802                 };
6803
6804                 onDragEnd = function () {
6805                         marker.off('dragstart', onDragStart, this);
6806                         marker.off('dragend', onDragEnd, this);
6807
6808                         this._createMiddleMarker(marker1, marker);
6809                         this._createMiddleMarker(marker, marker2);
6810                 };
6811
6812                 onClick = function () {
6813                         onDragStart.call(this);
6814                         onDragEnd.call(this);
6815                         this._poly.fire('edit');
6816                 };
6817
6818                 marker
6819                         .on('click', onClick, this)
6820                         .on('dragstart', onDragStart, this)
6821                         .on('dragend', onDragEnd, this);
6822
6823                 this._markerGroup.addLayer(marker);
6824         },
6825
6826         _updatePrevNext: function (marker1, marker2) {
6827                 if (marker1) {
6828                         marker1._next = marker2;
6829                 }
6830                 if (marker2) {
6831                         marker2._prev = marker1;
6832                 }
6833         },
6834
6835         _getMiddleLatLng: function (marker1, marker2) {
6836                 var map = this._poly._map,
6837                     p1 = map.latLngToLayerPoint(marker1.getLatLng()),
6838                     p2 = map.latLngToLayerPoint(marker2.getLatLng());
6839
6840                 return map.layerPointToLatLng(p1._add(p2)._divideBy(2));
6841         }
6842 });
6843
6844
6845
6846 L.Control = L.Class.extend({
6847         options: {
6848                 position: 'topright'
6849         },
6850
6851         initialize: function (options) {
6852                 L.Util.setOptions(this, options);
6853         },
6854
6855         getPosition: function () {
6856                 return this.options.position;
6857         },
6858
6859         setPosition: function (position) {
6860                 var map = this._map;
6861
6862                 if (map) {
6863                         map.removeControl(this);
6864                 }
6865
6866                 this.options.position = position;
6867
6868                 if (map) {
6869                         map.addControl(this);
6870                 }
6871
6872                 return this;
6873         },
6874
6875         addTo: function (map) {
6876                 this._map = map;
6877
6878                 var container = this._container = this.onAdd(map),
6879                     pos = this.getPosition(),
6880                         corner = map._controlCorners[pos];
6881
6882                 L.DomUtil.addClass(container, 'leaflet-control');
6883
6884                 if (pos.indexOf('bottom') !== -1) {
6885                         corner.insertBefore(container, corner.firstChild);
6886                 } else {
6887                         corner.appendChild(container);
6888                 }
6889
6890                 return this;
6891         },
6892
6893         removeFrom: function (map) {
6894                 var pos = this.getPosition(),
6895                         corner = map._controlCorners[pos];
6896
6897                 corner.removeChild(this._container);
6898                 this._map = null;
6899
6900                 if (this.onRemove) {
6901                         this.onRemove(map);
6902                 }
6903
6904                 return this;
6905         }
6906 });
6907
6908 L.control = function (options) {
6909         return new L.Control(options);
6910 };
6911
6912
6913 L.Map.include({
6914         addControl: function (control) {
6915                 control.addTo(this);
6916                 return this;
6917         },
6918
6919         removeControl: function (control) {
6920                 control.removeFrom(this);
6921                 return this;
6922         },
6923
6924         _initControlPos: function () {
6925                 var corners = this._controlCorners = {},
6926                     l = 'leaflet-',
6927                     container = this._controlContainer =
6928                                 L.DomUtil.create('div', l + 'control-container', this._container);
6929
6930                 function createCorner(vSide, hSide) {
6931                         var className = l + vSide + ' ' + l + hSide;
6932
6933                         corners[vSide + hSide] =
6934                                         L.DomUtil.create('div', className, container);
6935                 }
6936
6937                 createCorner('top', 'left');
6938                 createCorner('top', 'right');
6939                 createCorner('bottom', 'left');
6940                 createCorner('bottom', 'right');
6941         }
6942 });
6943
6944
6945 L.Control.Zoom = L.Control.extend({
6946         options: {
6947                 position: 'topleft'
6948         },
6949
6950         onAdd: function (map) {
6951                 var className = 'leaflet-control-zoom',
6952                     container = L.DomUtil.create('div', className);
6953
6954                 this._map = map;
6955
6956                 this._createButton('Zoom in', className + '-in', container, this._zoomIn, this);
6957                 this._createButton('Zoom out', className + '-out', container, this._zoomOut, this);
6958
6959                 return container;
6960         },
6961
6962         _zoomIn: function (e) {
6963                 this._map.zoomIn(e.shiftKey ? 3 : 1);
6964         },
6965
6966         _zoomOut: function (e) {
6967                 this._map.zoomOut(e.shiftKey ? 3 : 1);
6968         },
6969
6970         _createButton: function (title, className, container, fn, context) {
6971                 var link = L.DomUtil.create('a', className, container);
6972                 link.href = '#';
6973                 link.title = title;
6974
6975                 L.DomEvent
6976                         .on(link, 'click', L.DomEvent.stopPropagation)
6977                         .on(link, 'mousedown', L.DomEvent.stopPropagation)
6978                         .on(link, 'dblclick', L.DomEvent.stopPropagation)
6979                         .on(link, 'click', L.DomEvent.preventDefault)
6980                         .on(link, 'click', fn, context);
6981
6982                 return link;
6983         }
6984 });
6985
6986 L.Map.mergeOptions({
6987         zoomControl: true
6988 });
6989
6990 L.Map.addInitHook(function () {
6991         if (this.options.zoomControl) {
6992                 this.zoomControl = new L.Control.Zoom();
6993                 this.addControl(this.zoomControl);
6994         }
6995 });
6996
6997 L.control.zoom = function (options) {
6998         return new L.Control.Zoom(options);
6999 };
7000
7001
7002
7003 L.Control.Attribution = L.Control.extend({
7004         options: {
7005                 position: 'bottomright',
7006                 prefix: 'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>'
7007         },
7008
7009         initialize: function (options) {
7010                 L.Util.setOptions(this, options);
7011
7012                 this._attributions = {};
7013         },
7014
7015         onAdd: function (map) {
7016                 this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
7017                 L.DomEvent.disableClickPropagation(this._container);
7018
7019                 map
7020                         .on('layeradd', this._onLayerAdd, this)
7021                         .on('layerremove', this._onLayerRemove, this);
7022
7023                 this._update();
7024
7025                 return this._container;
7026         },
7027
7028         onRemove: function (map) {
7029                 map
7030                         .off('layeradd', this._onLayerAdd)
7031                         .off('layerremove', this._onLayerRemove);
7032
7033         },
7034
7035         setPrefix: function (prefix) {
7036                 this.options.prefix = prefix;
7037                 this._update();
7038                 return this;
7039         },
7040
7041         addAttribution: function (text) {
7042                 if (!text) { return; }
7043
7044                 if (!this._attributions[text]) {
7045                         this._attributions[text] = 0;
7046                 }
7047                 this._attributions[text]++;
7048
7049                 this._update();
7050
7051                 return this;
7052         },
7053
7054         removeAttribution: function (text) {
7055                 if (!text) { return; }
7056
7057                 this._attributions[text]--;
7058                 this._update();
7059
7060                 return this;
7061         },
7062
7063         _update: function () {
7064                 if (!this._map) { return; }
7065
7066                 var attribs = [];
7067
7068                 for (var i in this._attributions) {
7069                         if (this._attributions.hasOwnProperty(i) && this._attributions[i]) {
7070                                 attribs.push(i);
7071                         }
7072                 }
7073
7074                 var prefixAndAttribs = [];
7075
7076                 if (this.options.prefix) {
7077                         prefixAndAttribs.push(this.options.prefix);
7078                 }
7079                 if (attribs.length) {
7080                         prefixAndAttribs.push(attribs.join(', '));
7081                 }
7082
7083                 this._container.innerHTML = prefixAndAttribs.join(' &#8212; ');
7084         },
7085
7086         _onLayerAdd: function (e) {
7087                 if (e.layer.getAttribution) {
7088                         this.addAttribution(e.layer.getAttribution());
7089                 }
7090         },
7091
7092         _onLayerRemove: function (e) {
7093                 if (e.layer.getAttribution) {
7094                         this.removeAttribution(e.layer.getAttribution());
7095                 }
7096         }
7097 });
7098
7099 L.Map.mergeOptions({
7100         attributionControl: true
7101 });
7102
7103 L.Map.addInitHook(function () {
7104         if (this.options.attributionControl) {
7105                 this.attributionControl = (new L.Control.Attribution()).addTo(this);
7106         }
7107 });
7108
7109 L.control.attribution = function (options) {
7110         return new L.Control.Attribution(options);
7111 };
7112
7113
7114 L.Control.Scale = L.Control.extend({
7115         options: {
7116                 position: 'bottomleft',
7117                 maxWidth: 100,
7118                 metric: true,
7119                 imperial: true,
7120                 updateWhenIdle: false
7121         },
7122
7123         onAdd: function (map) {
7124                 this._map = map;
7125
7126                 var className = 'leaflet-control-scale',
7127                     container = L.DomUtil.create('div', className),
7128                     options = this.options;
7129
7130                 this._addScales(options, className, container);
7131
7132                 map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
7133                 map.whenReady(this._update, this);
7134
7135                 return container;
7136         },
7137
7138         onRemove: function (map) {
7139                 map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
7140         },
7141
7142         _addScales: function (options, className, container) {
7143                 if (options.metric) {
7144                         this._mScale = L.DomUtil.create('div', className + '-line', container);
7145                 }
7146                 if (options.imperial) {
7147                         this._iScale = L.DomUtil.create('div', className + '-line', container);
7148                 }
7149         },
7150
7151         _update: function () {
7152                 var bounds = this._map.getBounds(),
7153                     centerLat = bounds.getCenter().lat,
7154                     halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180),
7155                     dist = halfWorldMeters * (bounds.getNorthEast().lng - bounds.getSouthWest().lng) / 180,
7156
7157                     size = this._map.getSize(),
7158                     options = this.options,
7159                     maxMeters = 0;
7160
7161                 if (size.x > 0) {
7162                         maxMeters = dist * (options.maxWidth / size.x);
7163                 }
7164
7165                 this._updateScales(options, maxMeters);
7166         },
7167
7168         _updateScales: function (options, maxMeters) {
7169                 if (options.metric && maxMeters) {
7170                         this._updateMetric(maxMeters);
7171                 }
7172
7173                 if (options.imperial && maxMeters) {
7174                         this._updateImperial(maxMeters);
7175                 }
7176         },
7177
7178         _updateMetric: function (maxMeters) {
7179                 var meters = this._getRoundNum(maxMeters);
7180
7181                 this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px';
7182                 this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
7183         },
7184
7185         _updateImperial: function (maxMeters) {
7186                 var maxFeet = maxMeters * 3.2808399,
7187                         scale = this._iScale,
7188                         maxMiles, miles, feet;
7189
7190                 if (maxFeet > 5280) {
7191                         maxMiles = maxFeet / 5280;
7192                         miles = this._getRoundNum(maxMiles);
7193
7194                         scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
7195                         scale.innerHTML = miles + ' mi';
7196
7197                 } else {
7198                         feet = this._getRoundNum(maxFeet);
7199
7200                         scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
7201                         scale.innerHTML = feet + ' ft';
7202                 }
7203         },
7204
7205         _getScaleWidth: function (ratio) {
7206                 return Math.round(this.options.maxWidth * ratio) - 10;
7207         },
7208
7209         _getRoundNum: function (num) {
7210                 var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
7211                     d = num / pow10;
7212
7213                 d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;
7214
7215                 return pow10 * d;
7216         }
7217 });
7218
7219 L.control.scale = function (options) {
7220         return new L.Control.Scale(options);
7221 };
7222
7223
7224 L.Control.Layers = L.Control.extend({
7225         options: {
7226                 collapsed: true,
7227                 position: 'topright',
7228                 autoZIndex: true
7229         },
7230
7231         initialize: function (baseLayers, overlays, options) {
7232                 L.Util.setOptions(this, options);
7233
7234                 this._layers = {};
7235                 this._lastZIndex = 0;
7236
7237                 for (var i in baseLayers) {
7238                         if (baseLayers.hasOwnProperty(i)) {
7239                                 this._addLayer(baseLayers[i], i);
7240                         }
7241                 }
7242
7243                 for (i in overlays) {
7244                         if (overlays.hasOwnProperty(i)) {
7245                                 this._addLayer(overlays[i], i, true);
7246                         }
7247                 }
7248         },
7249
7250         onAdd: function (map) {
7251                 this._initLayout();
7252                 this._update();
7253
7254                 return this._container;
7255         },
7256
7257         addBaseLayer: function (layer, name) {
7258                 this._addLayer(layer, name);
7259                 this._update();
7260                 return this;
7261         },
7262
7263         addOverlay: function (layer, name) {
7264                 this._addLayer(layer, name, true);
7265                 this._update();
7266                 return this;
7267         },
7268
7269         removeLayer: function (layer) {
7270                 var id = L.Util.stamp(layer);
7271                 delete this._layers[id];
7272                 this._update();
7273                 return this;
7274         },
7275
7276         _initLayout: function () {
7277                 var className = 'leaflet-control-layers',
7278                     container = this._container = L.DomUtil.create('div', className);
7279
7280                 if (!L.Browser.touch) {
7281                         L.DomEvent.disableClickPropagation(container);
7282                 } else {
7283                         L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
7284                 }
7285
7286                 var form = this._form = L.DomUtil.create('form', className + '-list');
7287
7288                 if (this.options.collapsed) {
7289                         L.DomEvent
7290                                 .on(container, 'mouseover', this._expand, this)
7291                                 .on(container, 'mouseout', this._collapse, this);
7292
7293                         var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
7294                         link.href = '#';
7295                         link.title = 'Layers';
7296
7297                         if (L.Browser.touch) {
7298                                 L.DomEvent
7299                                         .on(link, 'click', L.DomEvent.stopPropagation)
7300                                         .on(link, 'click', L.DomEvent.preventDefault)
7301                                         .on(link, 'click', this._expand, this);
7302                         }
7303                         else {
7304                                 L.DomEvent.on(link, 'focus', this._expand, this);
7305                         }
7306
7307                         this._map.on('movestart', this._collapse, this);
7308                         // TODO keyboard accessibility
7309                 } else {
7310                         this._expand();
7311                 }
7312
7313                 this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
7314                 this._separator = L.DomUtil.create('div', className + '-separator', form);
7315                 this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
7316
7317                 container.appendChild(form);
7318         },
7319
7320         _addLayer: function (layer, name, overlay) {
7321                 var id = L.Util.stamp(layer);
7322
7323                 this._layers[id] = {
7324                         layer: layer,
7325                         name: name,
7326                         overlay: overlay
7327                 };
7328
7329                 if (this.options.autoZIndex && layer.setZIndex) {
7330                         this._lastZIndex++;
7331                         layer.setZIndex(this._lastZIndex);
7332                 }
7333         },
7334
7335         _update: function () {
7336                 if (!this._container) {
7337                         return;
7338                 }
7339
7340                 this._baseLayersList.innerHTML = '';
7341                 this._overlaysList.innerHTML = '';
7342
7343                 var baseLayersPresent = false,
7344                         overlaysPresent = false;
7345
7346                 for (var i in this._layers) {
7347                         if (this._layers.hasOwnProperty(i)) {
7348                                 var obj = this._layers[i];
7349                                 this._addItem(obj);
7350                                 overlaysPresent = overlaysPresent || obj.overlay;
7351                                 baseLayersPresent = baseLayersPresent || !obj.overlay;
7352                         }
7353                 }
7354
7355                 this._separator.style.display = (overlaysPresent && baseLayersPresent ? '' : 'none');
7356         },
7357
7358         // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
7359         _createRadioElement: function (name, checked) {
7360
7361                 var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + name + '"';
7362                 if (checked) {
7363                         radioHtml += ' checked="checked"';
7364                 }
7365                 radioHtml += '/>';
7366
7367                 var radioFragment = document.createElement('div');
7368                 radioFragment.innerHTML = radioHtml;
7369
7370                 return radioFragment.firstChild;
7371         },
7372
7373         _addItem: function (obj) {
7374                 var label = document.createElement('label'),
7375                     input,
7376                     checked = this._map.hasLayer(obj.layer);
7377
7378                 if (obj.overlay) {
7379                         input = document.createElement('input');
7380                         input.type = 'checkbox';
7381                         input.className = 'leaflet-control-layers-selector';
7382                         input.defaultChecked = checked;
7383                 } else {
7384                         input = this._createRadioElement('leaflet-base-layers', checked);
7385                 }
7386
7387                 input.layerId = L.Util.stamp(obj.layer);
7388
7389                 L.DomEvent.on(input, 'click', this._onInputClick, this);
7390
7391                 var name = document.createElement('span');
7392                 name.innerHTML = ' ' + obj.name;
7393
7394                 label.appendChild(input);
7395                 label.appendChild(name);
7396
7397                 var container = obj.overlay ? this._overlaysList : this._baseLayersList;
7398                 container.appendChild(label);
7399         },
7400
7401         _onInputClick: function () {
7402                 var i, input, obj,
7403                         inputs = this._form.getElementsByTagName('input'),
7404                         inputsLen = inputs.length,
7405                         baseLayer;
7406
7407                 for (i = 0; i < inputsLen; i++) {
7408                         input = inputs[i];
7409                         obj = this._layers[input.layerId];
7410
7411                         if (input.checked && !this._map.hasLayer(obj.layer)) {
7412                                 this._map.addLayer(obj.layer);
7413                                 if (!obj.overlay) {
7414                                         baseLayer = obj.layer;
7415                                 }
7416                         } else if (!input.checked && this._map.hasLayer(obj.layer)) {
7417                                 this._map.removeLayer(obj.layer);
7418                         }
7419                 }
7420
7421                 if (baseLayer) {
7422                         this._map.fire('baselayerchange', {layer: baseLayer});
7423                 }
7424         },
7425
7426         _expand: function () {
7427                 L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
7428         },
7429
7430         _collapse: function () {
7431                 this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
7432         }
7433 });
7434
7435 L.control.layers = function (baseLayers, overlays, options) {
7436         return new L.Control.Layers(baseLayers, overlays, options);
7437 };
7438
7439
7440 /*
7441  * L.PosAnimation is used by Leaflet internally for pan animations.
7442  */
7443
7444 L.PosAnimation = L.Class.extend({
7445         includes: L.Mixin.Events,
7446
7447         run: function (el, newPos, duration, easing) { // (HTMLElement, Point[, Number, String])
7448                 this.stop();
7449
7450                 this._el = el;
7451                 this._inProgress = true;
7452
7453                 this.fire('start');
7454
7455                 el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) + 's ' + (easing || 'ease-out');
7456
7457                 L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
7458                 L.DomUtil.setPosition(el, newPos);
7459
7460                 // toggle reflow, Chrome flickers for some reason if you don't do this
7461                 L.Util.falseFn(el.offsetWidth);
7462
7463                 // there's no native way to track value updates of tranisitioned properties, so we imitate this
7464                 this._stepTimer = setInterval(L.Util.bind(this.fire, this, 'step'), 50);
7465         },
7466
7467         stop: function () {
7468                 if (!this._inProgress) { return; }
7469
7470                 // if we just removed the transition property, the element would jump to its final position,
7471                 // so we need to make it stay at the current position
7472
7473                 L.DomUtil.setPosition(this._el, this._getPos());
7474                 this._onTransitionEnd();
7475         },
7476
7477         // you can't easily get intermediate values of properties animated with CSS3 Transitions,
7478         // we need to parse computed style (in case of transform it returns matrix string)
7479
7480         _transformRe: /(-?[\d\.]+), (-?[\d\.]+)\)/,
7481
7482         _getPos: function () {
7483                 var left, top, matches,
7484                         el = this._el,
7485                         style = window.getComputedStyle(el);
7486
7487                 if (L.Browser.any3d) {
7488                         matches = style[L.DomUtil.TRANSFORM].match(this._transformRe);
7489                         left = parseFloat(matches[1]);
7490                         top  = parseFloat(matches[2]);
7491                 } else {
7492                         left = parseFloat(style.left);
7493                         top  = parseFloat(style.top);
7494                 }
7495
7496                 return new L.Point(left, top, true);
7497         },
7498
7499         _onTransitionEnd: function () {
7500                 L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
7501
7502                 if (!this._inProgress) { return; }
7503                 this._inProgress = false;
7504
7505                 this._el.style[L.DomUtil.TRANSITION] = '';
7506
7507                 clearInterval(this._stepTimer);
7508
7509                 this.fire('step').fire('end');
7510         }
7511
7512 });
7513
7514
7515
7516 L.Map.include({
7517
7518         setView: function (center, zoom, forceReset) {
7519                 zoom = this._limitZoom(zoom);
7520
7521                 var zoomChanged = (this._zoom !== zoom);
7522
7523                 if (this._loaded && !forceReset && this._layers) {
7524
7525                         if (this._panAnim) {
7526                                 this._panAnim.stop();
7527                         }
7528
7529                         var done = (zoomChanged ?
7530                                         this._zoomToIfClose && this._zoomToIfClose(center, zoom) :
7531                                         this._panByIfClose(center));
7532
7533                         // exit if animated pan or zoom started
7534                         if (done) {
7535                                 clearTimeout(this._sizeTimer);
7536                                 return this;
7537                         }
7538                 }
7539
7540                 // reset the map view
7541                 this._resetView(center, zoom);
7542
7543                 return this;
7544         },
7545
7546         panBy: function (offset, duration) {
7547                 offset = L.point(offset);
7548
7549                 if (!(offset.x || offset.y)) {
7550                         return this;
7551                 }
7552
7553                 if (!this._panAnim) {
7554                         this._panAnim = new L.PosAnimation();
7555
7556                         this._panAnim.on({
7557                                 'step': this._onPanTransitionStep,
7558                                 'end': this._onPanTransitionEnd
7559                         }, this);
7560                 }
7561
7562                 this.fire('movestart');
7563
7564                 L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
7565
7566                 var newPos = L.DomUtil.getPosition(this._mapPane).subtract(offset);
7567                 this._panAnim.run(this._mapPane, newPos, duration || 0.25);
7568
7569                 return this;
7570         },
7571
7572         _onPanTransitionStep: function () {
7573                 this.fire('move');
7574         },
7575
7576         _onPanTransitionEnd: function () {
7577                 L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
7578                 this.fire('moveend');
7579         },
7580
7581         _panByIfClose: function (center) {
7582                 // difference between the new and current centers in pixels
7583                 var offset = this._getCenterOffset(center)._floor();
7584
7585                 if (this._offsetIsWithinView(offset)) {
7586                         this.panBy(offset);
7587                         return true;
7588                 }
7589                 return false;
7590         },
7591
7592         _offsetIsWithinView: function (offset, multiplyFactor) {
7593                 var m = multiplyFactor || 1,
7594                         size = this.getSize();
7595
7596                 return (Math.abs(offset.x) <= size.x * m) &&
7597                                 (Math.abs(offset.y) <= size.y * m);
7598         }
7599 });
7600
7601
7602 /*
7603  * L.PosAnimation fallback implementation that powers Leaflet pan animations
7604  * in browsers that don't support CSS3 Transitions.
7605  */
7606
7607 L.PosAnimation = L.DomUtil.TRANSITION ? L.PosAnimation : L.PosAnimation.extend({
7608
7609         run: function (el, newPos, duration, easing) { // (HTMLElement, Point[, Number, String])
7610                 this.stop();
7611
7612                 this._el = el;
7613                 this._inProgress = true;
7614                 this._duration = duration || 0.25;
7615                 this._ease = this._easings[easing || 'ease-out'];
7616
7617                 this._startPos = L.DomUtil.getPosition(el);
7618                 this._offset = newPos.subtract(this._startPos);
7619                 this._startTime = +new Date();
7620
7621                 this.fire('start');
7622
7623                 this._animate();
7624         },
7625
7626         stop: function () {
7627                 if (!this._inProgress) { return; }
7628
7629                 this._step();
7630                 this._complete();
7631         },
7632
7633         _animate: function () {
7634                 // animation loop
7635                 this._animId = L.Util.requestAnimFrame(this._animate, this);
7636                 this._step();
7637         },
7638
7639         _step: function () {
7640                 var elapsed = (+new Date()) - this._startTime,
7641                         duration = this._duration * 1000;
7642
7643                 if (elapsed < duration) {
7644                         this._runFrame(this._ease(elapsed / duration));
7645                 } else {
7646                         this._runFrame(1);
7647                         this._complete();
7648                 }
7649         },
7650
7651         _runFrame: function (progress) {
7652                 var pos = this._startPos.add(this._offset.multiplyBy(progress));
7653                 L.DomUtil.setPosition(this._el, pos);
7654
7655                 this.fire('step');
7656         },
7657
7658         _complete: function () {
7659                 L.Util.cancelAnimFrame(this._animId);
7660
7661                 this._inProgress = false;
7662                 this.fire('end');
7663         },
7664
7665         // easing functions, they map time progress to movement progress
7666         _easings: {
7667                 'ease-out': function (t) { return t * (2 - t); },
7668                 'linear':   function (t) { return t; }
7669         }
7670
7671 });
7672
7673
7674 L.Map.mergeOptions({
7675         zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android23 && !L.Browser.mobileOpera
7676 });
7677
7678 if (L.DomUtil.TRANSITION) {
7679         L.Map.addInitHook(function () {
7680                 L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
7681         });
7682 }
7683
7684 L.Map.include(!L.DomUtil.TRANSITION ? {} : {
7685
7686         _zoomToIfClose: function (center, zoom) {
7687
7688                 if (this._animatingZoom) { return true; }
7689
7690                 if (!this.options.zoomAnimation) { return false; }
7691
7692                 var scale = this.getZoomScale(zoom),
7693                         offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);
7694
7695                 // if offset does not exceed half of the view
7696                 if (!this._offsetIsWithinView(offset, 1)) { return false; }
7697
7698                 L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
7699
7700                 this
7701                         .fire('movestart')
7702                         .fire('zoomstart');
7703
7704                 this.fire('zoomanim', {
7705                         center: center,
7706                         zoom: zoom
7707                 });
7708
7709                 var origin = this._getCenterLayerPoint().add(offset);
7710
7711                 this._prepareTileBg();
7712                 this._runAnimation(center, zoom, scale, origin);
7713
7714                 return true;
7715         },
7716
7717         _catchTransitionEnd: function (e) {
7718                 if (this._animatingZoom) {
7719                         this._onZoomTransitionEnd();
7720                 }
7721         },
7722
7723         _runAnimation: function (center, zoom, scale, origin, backwardsTransform) {
7724                 this._animateToCenter = center;
7725                 this._animateToZoom = zoom;
7726                 this._animatingZoom = true;
7727
7728                 if (L.Draggable) {
7729                         L.Draggable._disabled = true;
7730                 }
7731
7732                 var transform = L.DomUtil.TRANSFORM,
7733                         tileBg = this._tileBg;
7734
7735                 clearTimeout(this._clearTileBgTimer);
7736
7737                 L.Util.falseFn(tileBg.offsetWidth); //hack to make sure transform is updated before running animation
7738
7739                 var scaleStr = L.DomUtil.getScaleString(scale, origin),
7740                         oldTransform = tileBg.style[transform];
7741
7742                 tileBg.style[transform] = backwardsTransform ?
7743                         oldTransform + ' ' + scaleStr :
7744                         scaleStr + ' ' + oldTransform;
7745         },
7746
7747         _prepareTileBg: function () {
7748                 var tilePane = this._tilePane,
7749                         tileBg = this._tileBg;
7750
7751                 // If foreground layer doesn't have many tiles but bg layer does, keep the existing bg layer and just zoom it some more
7752                 if (tileBg &&
7753                                 this._getLoadedTilesPercentage(tileBg) > 0.5 &&
7754                                 this._getLoadedTilesPercentage(tilePane) < 0.5) {
7755
7756                         tilePane.style.visibility = 'hidden';
7757                         tilePane.empty = true;
7758                         this._stopLoadingImages(tilePane);
7759                         return;
7760                 }
7761
7762                 if (!tileBg) {
7763                         tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
7764                         tileBg.style.zIndex = 1;
7765                 }
7766
7767                 // prepare the background pane to become the main tile pane
7768                 tileBg.style[L.DomUtil.TRANSFORM] = '';
7769                 tileBg.style.visibility = 'hidden';
7770
7771                 // tells tile layers to reinitialize their containers
7772                 tileBg.empty = true; //new FG
7773                 tilePane.empty = false; //new BG
7774
7775                 //Switch out the current layer to be the new bg layer (And vice-versa)
7776                 this._tilePane = this._panes.tilePane = tileBg;
7777                 var newTileBg = this._tileBg = tilePane;
7778
7779                 L.DomUtil.addClass(newTileBg, 'leaflet-zoom-animated');
7780
7781                 this._stopLoadingImages(newTileBg);
7782         },
7783
7784         _getLoadedTilesPercentage: function (container) {
7785                 var tiles = container.getElementsByTagName('img'),
7786                         i, len, count = 0;
7787
7788                 for (i = 0, len = tiles.length; i < len; i++) {
7789                         if (tiles[i].complete) {
7790                                 count++;
7791                         }
7792                 }
7793                 return count / len;
7794         },
7795
7796         // stops loading all tiles in the background layer
7797         _stopLoadingImages: function (container) {
7798                 var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
7799                         i, len, tile;
7800
7801                 for (i = 0, len = tiles.length; i < len; i++) {
7802                         tile = tiles[i];
7803
7804                         if (!tile.complete) {
7805                                 tile.onload = L.Util.falseFn;
7806                                 tile.onerror = L.Util.falseFn;
7807                                 tile.src = L.Util.emptyImageUrl;
7808
7809                                 tile.parentNode.removeChild(tile);
7810                         }
7811                 }
7812         },
7813
7814         _onZoomTransitionEnd: function () {
7815                 this._restoreTileFront();
7816                 L.Util.falseFn(this._tileBg.offsetWidth); // force reflow
7817                 this._resetView(this._animateToCenter, this._animateToZoom, true, true);
7818
7819                 L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
7820                 this._animatingZoom = false;
7821
7822                 if (L.Draggable) {
7823                         L.Draggable._disabled = false;
7824                 }
7825         },
7826
7827         _restoreTileFront: function () {
7828                 this._tilePane.innerHTML = '';
7829                 this._tilePane.style.visibility = '';
7830                 this._tilePane.style.zIndex = 2;
7831                 this._tileBg.style.zIndex = 1;
7832         },
7833
7834         _clearTileBg: function () {
7835                 if (!this._animatingZoom && !this.touchZoom._zooming) {
7836                         this._tileBg.innerHTML = '';
7837                 }
7838         }
7839 });
7840
7841
7842 /*
7843  * Provides L.Map with convenient shortcuts for W3C geolocation.
7844  */
7845
7846 L.Map.include({
7847         _defaultLocateOptions: {
7848                 watch: false,
7849                 setView: false,
7850                 maxZoom: Infinity,
7851                 timeout: 10000,
7852                 maximumAge: 0,
7853                 enableHighAccuracy: false
7854         },
7855
7856         locate: function (/*Object*/ options) {
7857
7858                 options = this._locationOptions = L.Util.extend(this._defaultLocateOptions, options);
7859
7860                 if (!navigator.geolocation) {
7861                         this._handleGeolocationError({
7862                                 code: 0,
7863                                 message: "Geolocation not supported."
7864                         });
7865                         return this;
7866                 }
7867
7868                 var onResponse = L.Util.bind(this._handleGeolocationResponse, this),
7869                         onError = L.Util.bind(this._handleGeolocationError, this);
7870
7871                 if (options.watch) {
7872                         this._locationWatchId = navigator.geolocation.watchPosition(onResponse, onError, options);
7873                 } else {
7874                         navigator.geolocation.getCurrentPosition(onResponse, onError, options);
7875                 }
7876                 return this;
7877         },
7878
7879         stopLocate: function () {
7880                 if (navigator.geolocation) {
7881                         navigator.geolocation.clearWatch(this._locationWatchId);
7882                 }
7883                 return this;
7884         },
7885
7886         _handleGeolocationError: function (error) {
7887                 var c = error.code,
7888                         message = error.message ||
7889                                 (c === 1 ? "permission denied" :
7890                                 (c === 2 ? "position unavailable" : "timeout"));
7891
7892                 if (this._locationOptions.setView && !this._loaded) {
7893                         this.fitWorld();
7894                 }
7895
7896                 this.fire('locationerror', {
7897                         code: c,
7898                         message: "Geolocation error: " + message + "."
7899                 });
7900         },
7901
7902         _handleGeolocationResponse: function (pos) {
7903                 var latAccuracy = 180 * pos.coords.accuracy / 4e7,
7904                         lngAccuracy = latAccuracy * 2,
7905
7906                         lat = pos.coords.latitude,
7907                         lng = pos.coords.longitude,
7908                         latlng = new L.LatLng(lat, lng),
7909
7910                         sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy),
7911                         ne = new L.LatLng(lat + latAccuracy, lng + lngAccuracy),
7912                         bounds = new L.LatLngBounds(sw, ne),
7913
7914                         options = this._locationOptions;
7915
7916                 if (options.setView) {
7917                         var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
7918                         this.setView(latlng, zoom);
7919                 }
7920
7921                 this.fire('locationfound', {
7922                         latlng: latlng,
7923                         bounds: bounds,
7924                         accuracy: pos.coords.accuracy
7925                 });
7926         }
7927 });
7928
7929
7930
7931
7932 }(this));