UNPKG

448 kBJavaScriptView Raw
1/* @preserve
2 * Leaflet 1.9.3, a JS library for interactive maps. https://leafletjs.com
3 * (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade
4 */
5
6(function (global, factory) {
7 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
8 typeof define === 'function' && define.amd ? define(['exports'], factory) :
9 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.leaflet = {}));
10})(this, (function (exports) { 'use strict';
11
12 var version = "1.9.3";
13
14 /*
15 * @namespace Util
16 *
17 * Various utility functions, used by Leaflet internally.
18 */
19
20 // @function extend(dest: Object, src?: Object): Object
21 // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.
22 function extend(dest) {
23 var i, j, len, src;
24
25 for (j = 1, len = arguments.length; j < len; j++) {
26 src = arguments[j];
27 for (i in src) {
28 dest[i] = src[i];
29 }
30 }
31 return dest;
32 }
33
34 // @function create(proto: Object, properties?: Object): Object
35 // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)
36 var create$2 = Object.create || (function () {
37 function F() {}
38 return function (proto) {
39 F.prototype = proto;
40 return new F();
41 };
42 })();
43
44 // @function bind(fn: Function, …): Function
45 // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).
46 // Has a `L.bind()` shortcut.
47 function bind(fn, obj) {
48 var slice = Array.prototype.slice;
49
50 if (fn.bind) {
51 return fn.bind.apply(fn, slice.call(arguments, 1));
52 }
53
54 var args = slice.call(arguments, 2);
55
56 return function () {
57 return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);
58 };
59 }
60
61 // @property lastId: Number
62 // Last unique ID used by [`stamp()`](#util-stamp)
63 var lastId = 0;
64
65 // @function stamp(obj: Object): Number
66 // Returns the unique ID of an object, assigning it one if it doesn't have it.
67 function stamp(obj) {
68 if (!('_leaflet_id' in obj)) {
69 obj['_leaflet_id'] = ++lastId;
70 }
71 return obj._leaflet_id;
72 }
73
74 // @function throttle(fn: Function, time: Number, context: Object): Function
75 // Returns a function which executes function `fn` with the given scope `context`
76 // (so that the `this` keyword refers to `context` inside `fn`'s code). The function
77 // `fn` will be called no more than one time per given amount of `time`. The arguments
78 // received by the bound function will be any arguments passed when binding the
79 // function, followed by any arguments passed when invoking the bound function.
80 // Has an `L.throttle` shortcut.
81 function throttle(fn, time, context) {
82 var lock, args, wrapperFn, later;
83
84 later = function () {
85 // reset lock and call if queued
86 lock = false;
87 if (args) {
88 wrapperFn.apply(context, args);
89 args = false;
90 }
91 };
92
93 wrapperFn = function () {
94 if (lock) {
95 // called too soon, queue to call later
96 args = arguments;
97
98 } else {
99 // call and lock until later
100 fn.apply(context, arguments);
101 setTimeout(later, time);
102 lock = true;
103 }
104 };
105
106 return wrapperFn;
107 }
108
109 // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number
110 // Returns the number `num` modulo `range` in such a way so it lies within
111 // `range[0]` and `range[1]`. The returned value will be always smaller than
112 // `range[1]` unless `includeMax` is set to `true`.
113 function wrapNum(x, range, includeMax) {
114 var max = range[1],
115 min = range[0],
116 d = max - min;
117 return x === max && includeMax ? x : ((x - min) % d + d) % d + min;
118 }
119
120 // @function falseFn(): Function
121 // Returns a function which always returns `false`.
122 function falseFn() { return false; }
123
124 // @function formatNum(num: Number, precision?: Number|false): Number
125 // Returns the number `num` rounded with specified `precision`.
126 // The default `precision` value is 6 decimal places.
127 // `false` can be passed to skip any processing (can be useful to avoid round-off errors).
128 function formatNum(num, precision) {
129 if (precision === false) { return num; }
130 var pow = Math.pow(10, precision === undefined ? 6 : precision);
131 return Math.round(num * pow) / pow;
132 }
133
134 // @function trim(str: String): String
135 // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)
136 function trim(str) {
137 return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
138 }
139
140 // @function splitWords(str: String): String[]
141 // Trims and splits the string on whitespace and returns the array of parts.
142 function splitWords(str) {
143 return trim(str).split(/\s+/);
144 }
145
146 // @function setOptions(obj: Object, options: Object): Object
147 // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.
148 function setOptions(obj, options) {
149 if (!Object.prototype.hasOwnProperty.call(obj, 'options')) {
150 obj.options = obj.options ? create$2(obj.options) : {};
151 }
152 for (var i in options) {
153 obj.options[i] = options[i];
154 }
155 return obj.options;
156 }
157
158 // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String
159 // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}`
160 // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will
161 // be appended at the end. If `uppercase` is `true`, the parameter names will
162 // be uppercased (e.g. `'?A=foo&B=bar'`)
163 function getParamString(obj, existingUrl, uppercase) {
164 var params = [];
165 for (var i in obj) {
166 params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
167 }
168 return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
169 }
170
171 var templateRe = /\{ *([\w_ -]+) *\}/g;
172
173 // @function template(str: String, data: Object): String
174 // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`
175 // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string
176 // `('Hello foo, bar')`. You can also specify functions instead of strings for
177 // data values — they will be evaluated passing `data` as an argument.
178 function template(str, data) {
179 return str.replace(templateRe, function (str, key) {
180 var value = data[key];
181
182 if (value === undefined) {
183 throw new Error('No value provided for variable ' + str);
184
185 } else if (typeof value === 'function') {
186 value = value(data);
187 }
188 return value;
189 });
190 }
191
192 // @function isArray(obj): Boolean
193 // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
194 var isArray = Array.isArray || function (obj) {
195 return (Object.prototype.toString.call(obj) === '[object Array]');
196 };
197
198 // @function indexOf(array: Array, el: Object): Number
199 // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)
200 function indexOf(array, el) {
201 for (var i = 0; i < array.length; i++) {
202 if (array[i] === el) { return i; }
203 }
204 return -1;
205 }
206
207 // @property emptyImageUrl: String
208 // Data URI string containing a base64-encoded empty GIF image.
209 // Used as a hack to free memory from unused images on WebKit-powered
210 // mobile devices (by setting image `src` to this string).
211 var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
212
213 // inspired by https://paulirish.com/2011/requestanimationframe-for-smart-animating/
214
215 function getPrefixed(name) {
216 return window['webkit' + name] || window['moz' + name] || window['ms' + name];
217 }
218
219 var lastTime = 0;
220
221 // fallback for IE 7-8
222 function timeoutDefer(fn) {
223 var time = +new Date(),
224 timeToCall = Math.max(0, 16 - (time - lastTime));
225
226 lastTime = time + timeToCall;
227 return window.setTimeout(fn, timeToCall);
228 }
229
230 var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
231 var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||
232 getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };
233
234 // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number
235 // Schedules `fn` to be executed when the browser repaints. `fn` is bound to
236 // `context` if given. When `immediate` is set, `fn` is called immediately if
237 // the browser doesn't have native support for
238 // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),
239 // otherwise it's delayed. Returns a request ID that can be used to cancel the request.
240 function requestAnimFrame(fn, context, immediate) {
241 if (immediate && requestFn === timeoutDefer) {
242 fn.call(context);
243 } else {
244 return requestFn.call(window, bind(fn, context));
245 }
246 }
247
248 // @function cancelAnimFrame(id: Number): undefined
249 // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).
250 function cancelAnimFrame(id) {
251 if (id) {
252 cancelFn.call(window, id);
253 }
254 }
255
256 var Util = {
257 __proto__: null,
258 extend: extend,
259 create: create$2,
260 bind: bind,
261 get lastId () { return lastId; },
262 stamp: stamp,
263 throttle: throttle,
264 wrapNum: wrapNum,
265 falseFn: falseFn,
266 formatNum: formatNum,
267 trim: trim,
268 splitWords: splitWords,
269 setOptions: setOptions,
270 getParamString: getParamString,
271 template: template,
272 isArray: isArray,
273 indexOf: indexOf,
274 emptyImageUrl: emptyImageUrl,
275 requestFn: requestFn,
276 cancelFn: cancelFn,
277 requestAnimFrame: requestAnimFrame,
278 cancelAnimFrame: cancelAnimFrame
279 };
280
281 // @class Class
282 // @aka L.Class
283
284 // @section
285 // @uninheritable
286
287 // Thanks to John Resig and Dean Edwards for inspiration!
288
289 function Class() {}
290
291 Class.extend = function (props) {
292
293 // @function extend(props: Object): Function
294 // [Extends the current class](#class-inheritance) given the properties to be included.
295 // Returns a Javascript function that is a class constructor (to be called with `new`).
296 var NewClass = function () {
297
298 setOptions(this);
299
300 // call the constructor
301 if (this.initialize) {
302 this.initialize.apply(this, arguments);
303 }
304
305 // call all constructor hooks
306 this.callInitHooks();
307 };
308
309 var parentProto = NewClass.__super__ = this.prototype;
310
311 var proto = create$2(parentProto);
312 proto.constructor = NewClass;
313
314 NewClass.prototype = proto;
315
316 // inherit parent's statics
317 for (var i in this) {
318 if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') {
319 NewClass[i] = this[i];
320 }
321 }
322
323 // mix static properties into the class
324 if (props.statics) {
325 extend(NewClass, props.statics);
326 }
327
328 // mix includes into the prototype
329 if (props.includes) {
330 checkDeprecatedMixinEvents(props.includes);
331 extend.apply(null, [proto].concat(props.includes));
332 }
333
334 // mix given properties into the prototype
335 extend(proto, props);
336 delete proto.statics;
337 delete proto.includes;
338
339 // merge options
340 if (proto.options) {
341 proto.options = parentProto.options ? create$2(parentProto.options) : {};
342 extend(proto.options, props.options);
343 }
344
345 proto._initHooks = [];
346
347 // add method for calling all hooks
348 proto.callInitHooks = function () {
349
350 if (this._initHooksCalled) { return; }
351
352 if (parentProto.callInitHooks) {
353 parentProto.callInitHooks.call(this);
354 }
355
356 this._initHooksCalled = true;
357
358 for (var i = 0, len = proto._initHooks.length; i < len; i++) {
359 proto._initHooks[i].call(this);
360 }
361 };
362
363 return NewClass;
364 };
365
366
367 // @function include(properties: Object): this
368 // [Includes a mixin](#class-includes) into the current class.
369 Class.include = function (props) {
370 var parentOptions = this.prototype.options;
371 extend(this.prototype, props);
372 if (props.options) {
373 this.prototype.options = parentOptions;
374 this.mergeOptions(props.options);
375 }
376 return this;
377 };
378
379 // @function mergeOptions(options: Object): this
380 // [Merges `options`](#class-options) into the defaults of the class.
381 Class.mergeOptions = function (options) {
382 extend(this.prototype.options, options);
383 return this;
384 };
385
386 // @function addInitHook(fn: Function): this
387 // Adds a [constructor hook](#class-constructor-hooks) to the class.
388 Class.addInitHook = function (fn) { // (Function) || (String, args...)
389 var args = Array.prototype.slice.call(arguments, 1);
390
391 var init = typeof fn === 'function' ? fn : function () {
392 this[fn].apply(this, args);
393 };
394
395 this.prototype._initHooks = this.prototype._initHooks || [];
396 this.prototype._initHooks.push(init);
397 return this;
398 };
399
400 function checkDeprecatedMixinEvents(includes) {
401 /* global L: true */
402 if (typeof L === 'undefined' || !L || !L.Mixin) { return; }
403
404 includes = isArray(includes) ? includes : [includes];
405
406 for (var i = 0; i < includes.length; i++) {
407 if (includes[i] === L.Mixin.Events) {
408 console.warn('Deprecated include of L.Mixin.Events: ' +
409 'this property will be removed in future releases, ' +
410 'please inherit from L.Evented instead.', new Error().stack);
411 }
412 }
413 }
414
415 /*
416 * @class Evented
417 * @aka L.Evented
418 * @inherits Class
419 *
420 * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event).
421 *
422 * @example
423 *
424 * ```js
425 * map.on('click', function(e) {
426 * alert(e.latlng);
427 * } );
428 * ```
429 *
430 * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
431 *
432 * ```js
433 * function onClick(e) { ... }
434 *
435 * map.on('click', onClick);
436 * map.off('click', onClick);
437 * ```
438 */
439
440 var Events = {
441 /* @method on(type: String, fn: Function, context?: Object): this
442 * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`).
443 *
444 * @alternative
445 * @method on(eventMap: Object): this
446 * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
447 */
448 on: function (types, fn, context) {
449
450 // types can be a map of types/handlers
451 if (typeof types === 'object') {
452 for (var type in types) {
453 // we don't process space-separated events here for performance;
454 // it's a hot path since Layer uses the on(obj) syntax
455 this._on(type, types[type], fn);
456 }
457
458 } else {
459 // types can be a string of space-separated words
460 types = splitWords(types);
461
462 for (var i = 0, len = types.length; i < len; i++) {
463 this._on(types[i], fn, context);
464 }
465 }
466
467 return this;
468 },
469
470 /* @method off(type: String, fn?: Function, context?: Object): this
471 * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener.
472 *
473 * @alternative
474 * @method off(eventMap: Object): this
475 * Removes a set of type/listener pairs.
476 *
477 * @alternative
478 * @method off: this
479 * Removes all listeners to all events on the object. This includes implicitly attached events.
480 */
481 off: function (types, fn, context) {
482
483 if (!arguments.length) {
484 // clear all listeners if called without arguments
485 delete this._events;
486
487 } else if (typeof types === 'object') {
488 for (var type in types) {
489 this._off(type, types[type], fn);
490 }
491
492 } else {
493 types = splitWords(types);
494
495 var removeAll = arguments.length === 1;
496 for (var i = 0, len = types.length; i < len; i++) {
497 if (removeAll) {
498 this._off(types[i]);
499 } else {
500 this._off(types[i], fn, context);
501 }
502 }
503 }
504
505 return this;
506 },
507
508 // attach listener (without syntactic sugar now)
509 _on: function (type, fn, context, _once) {
510 if (typeof fn !== 'function') {
511 console.warn('wrong listener type: ' + typeof fn);
512 return;
513 }
514
515 // check if fn already there
516 if (this._listens(type, fn, context) !== false) {
517 return;
518 }
519
520 if (context === this) {
521 // Less memory footprint.
522 context = undefined;
523 }
524
525 var newListener = {fn: fn, ctx: context};
526 if (_once) {
527 newListener.once = true;
528 }
529
530 this._events = this._events || {};
531 this._events[type] = this._events[type] || [];
532 this._events[type].push(newListener);
533 },
534
535 _off: function (type, fn, context) {
536 var listeners,
537 i,
538 len;
539
540 if (!this._events) {
541 return;
542 }
543
544 listeners = this._events[type];
545 if (!listeners) {
546 return;
547 }
548
549 if (arguments.length === 1) { // remove all
550 if (this._firingCount) {
551 // Set all removed listeners to noop
552 // so they are not called if remove happens in fire
553 for (i = 0, len = listeners.length; i < len; i++) {
554 listeners[i].fn = falseFn;
555 }
556 }
557 // clear all listeners for a type if function isn't specified
558 delete this._events[type];
559 return;
560 }
561
562 if (typeof fn !== 'function') {
563 console.warn('wrong listener type: ' + typeof fn);
564 return;
565 }
566
567 // find fn and remove it
568 var index = this._listens(type, fn, context);
569 if (index !== false) {
570 var listener = listeners[index];
571 if (this._firingCount) {
572 // set the removed listener to noop so that's not called if remove happens in fire
573 listener.fn = falseFn;
574
575 /* copy array in case events are being fired */
576 this._events[type] = listeners = listeners.slice();
577 }
578 listeners.splice(index, 1);
579 }
580 },
581
582 // @method fire(type: String, data?: Object, propagate?: Boolean): this
583 // Fires an event of the specified type. You can optionally provide a data
584 // object — the first argument of the listener function will contain its
585 // properties. The event can optionally be propagated to event parents.
586 fire: function (type, data, propagate) {
587 if (!this.listens(type, propagate)) { return this; }
588
589 var event = extend({}, data, {
590 type: type,
591 target: this,
592 sourceTarget: data && data.sourceTarget || this
593 });
594
595 if (this._events) {
596 var listeners = this._events[type];
597 if (listeners) {
598 this._firingCount = (this._firingCount + 1) || 1;
599 for (var i = 0, len = listeners.length; i < len; i++) {
600 var l = listeners[i];
601 // off overwrites l.fn, so we need to copy fn to a var
602 var fn = l.fn;
603 if (l.once) {
604 this.off(type, fn, l.ctx);
605 }
606 fn.call(l.ctx || this, event);
607 }
608
609 this._firingCount--;
610 }
611 }
612
613 if (propagate) {
614 // propagate the event to parents (set with addEventParent)
615 this._propagateEvent(event);
616 }
617
618 return this;
619 },
620
621 // @method listens(type: String, propagate?: Boolean): Boolean
622 // @method listens(type: String, fn: Function, context?: Object, propagate?: Boolean): Boolean
623 // Returns `true` if a particular event type has any listeners attached to it.
624 // The verification can optionally be propagated, it will return `true` if parents have the listener attached to it.
625 listens: function (type, fn, context, propagate) {
626 if (typeof type !== 'string') {
627 console.warn('"string" type argument expected');
628 }
629
630 // we don't overwrite the input `fn` value, because we need to use it for propagation
631 var _fn = fn;
632 if (typeof fn !== 'function') {
633 propagate = !!fn;
634 _fn = undefined;
635 context = undefined;
636 }
637
638 var listeners = this._events && this._events[type];
639 if (listeners && listeners.length) {
640 if (this._listens(type, _fn, context) !== false) {
641 return true;
642 }
643 }
644
645 if (propagate) {
646 // also check parents for listeners if event propagates
647 for (var id in this._eventParents) {
648 if (this._eventParents[id].listens(type, fn, context, propagate)) { return true; }
649 }
650 }
651 return false;
652 },
653
654 // returns the index (number) or false
655 _listens: function (type, fn, context) {
656 if (!this._events) {
657 return false;
658 }
659
660 var listeners = this._events[type] || [];
661 if (!fn) {
662 return !!listeners.length;
663 }
664
665 if (context === this) {
666 // Less memory footprint.
667 context = undefined;
668 }
669
670 for (var i = 0, len = listeners.length; i < len; i++) {
671 if (listeners[i].fn === fn && listeners[i].ctx === context) {
672 return i;
673 }
674 }
675 return false;
676
677 },
678
679 // @method once(…): this
680 // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.
681 once: function (types, fn, context) {
682
683 // types can be a map of types/handlers
684 if (typeof types === 'object') {
685 for (var type in types) {
686 // we don't process space-separated events here for performance;
687 // it's a hot path since Layer uses the on(obj) syntax
688 this._on(type, types[type], fn, true);
689 }
690
691 } else {
692 // types can be a string of space-separated words
693 types = splitWords(types);
694
695 for (var i = 0, len = types.length; i < len; i++) {
696 this._on(types[i], fn, context, true);
697 }
698 }
699
700 return this;
701 },
702
703 // @method addEventParent(obj: Evented): this
704 // Adds an event parent - an `Evented` that will receive propagated events
705 addEventParent: function (obj) {
706 this._eventParents = this._eventParents || {};
707 this._eventParents[stamp(obj)] = obj;
708 return this;
709 },
710
711 // @method removeEventParent(obj: Evented): this
712 // Removes an event parent, so it will stop receiving propagated events
713 removeEventParent: function (obj) {
714 if (this._eventParents) {
715 delete this._eventParents[stamp(obj)];
716 }
717 return this;
718 },
719
720 _propagateEvent: function (e) {
721 for (var id in this._eventParents) {
722 this._eventParents[id].fire(e.type, extend({
723 layer: e.target,
724 propagatedFrom: e.target
725 }, e), true);
726 }
727 }
728 };
729
730 // aliases; we should ditch those eventually
731
732 // @method addEventListener(…): this
733 // Alias to [`on(…)`](#evented-on)
734 Events.addEventListener = Events.on;
735
736 // @method removeEventListener(…): this
737 // Alias to [`off(…)`](#evented-off)
738
739 // @method clearAllEventListeners(…): this
740 // Alias to [`off()`](#evented-off)
741 Events.removeEventListener = Events.clearAllEventListeners = Events.off;
742
743 // @method addOneTimeEventListener(…): this
744 // Alias to [`once(…)`](#evented-once)
745 Events.addOneTimeEventListener = Events.once;
746
747 // @method fireEvent(…): this
748 // Alias to [`fire(…)`](#evented-fire)
749 Events.fireEvent = Events.fire;
750
751 // @method hasEventListeners(…): Boolean
752 // Alias to [`listens(…)`](#evented-listens)
753 Events.hasEventListeners = Events.listens;
754
755 var Evented = Class.extend(Events);
756
757 /*
758 * @class Point
759 * @aka L.Point
760 *
761 * Represents a point with `x` and `y` coordinates in pixels.
762 *
763 * @example
764 *
765 * ```js
766 * var point = L.point(200, 300);
767 * ```
768 *
769 * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:
770 *
771 * ```js
772 * map.panBy([200, 300]);
773 * map.panBy(L.point(200, 300));
774 * ```
775 *
776 * Note that `Point` does not inherit from Leaflet's `Class` object,
777 * which means new classes can't inherit from it, and new methods
778 * can't be added to it with the `include` function.
779 */
780
781 function Point(x, y, round) {
782 // @property x: Number; The `x` coordinate of the point
783 this.x = (round ? Math.round(x) : x);
784 // @property y: Number; The `y` coordinate of the point
785 this.y = (round ? Math.round(y) : y);
786 }
787
788 var trunc = Math.trunc || function (v) {
789 return v > 0 ? Math.floor(v) : Math.ceil(v);
790 };
791
792 Point.prototype = {
793
794 // @method clone(): Point
795 // Returns a copy of the current point.
796 clone: function () {
797 return new Point(this.x, this.y);
798 },
799
800 // @method add(otherPoint: Point): Point
801 // Returns the result of addition of the current and the given points.
802 add: function (point) {
803 // non-destructive, returns a new point
804 return this.clone()._add(toPoint(point));
805 },
806
807 _add: function (point) {
808 // destructive, used directly for performance in situations where it's safe to modify existing point
809 this.x += point.x;
810 this.y += point.y;
811 return this;
812 },
813
814 // @method subtract(otherPoint: Point): Point
815 // Returns the result of subtraction of the given point from the current.
816 subtract: function (point) {
817 return this.clone()._subtract(toPoint(point));
818 },
819
820 _subtract: function (point) {
821 this.x -= point.x;
822 this.y -= point.y;
823 return this;
824 },
825
826 // @method divideBy(num: Number): Point
827 // Returns the result of division of the current point by the given number.
828 divideBy: function (num) {
829 return this.clone()._divideBy(num);
830 },
831
832 _divideBy: function (num) {
833 this.x /= num;
834 this.y /= num;
835 return this;
836 },
837
838 // @method multiplyBy(num: Number): Point
839 // Returns the result of multiplication of the current point by the given number.
840 multiplyBy: function (num) {
841 return this.clone()._multiplyBy(num);
842 },
843
844 _multiplyBy: function (num) {
845 this.x *= num;
846 this.y *= num;
847 return this;
848 },
849
850 // @method scaleBy(scale: Point): Point
851 // Multiply each coordinate of the current point by each coordinate of
852 // `scale`. In linear algebra terms, multiply the point by the
853 // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
854 // defined by `scale`.
855 scaleBy: function (point) {
856 return new Point(this.x * point.x, this.y * point.y);
857 },
858
859 // @method unscaleBy(scale: Point): Point
860 // Inverse of `scaleBy`. Divide each coordinate of the current point by
861 // each coordinate of `scale`.
862 unscaleBy: function (point) {
863 return new Point(this.x / point.x, this.y / point.y);
864 },
865
866 // @method round(): Point
867 // Returns a copy of the current point with rounded coordinates.
868 round: function () {
869 return this.clone()._round();
870 },
871
872 _round: function () {
873 this.x = Math.round(this.x);
874 this.y = Math.round(this.y);
875 return this;
876 },
877
878 // @method floor(): Point
879 // Returns a copy of the current point with floored coordinates (rounded down).
880 floor: function () {
881 return this.clone()._floor();
882 },
883
884 _floor: function () {
885 this.x = Math.floor(this.x);
886 this.y = Math.floor(this.y);
887 return this;
888 },
889
890 // @method ceil(): Point
891 // Returns a copy of the current point with ceiled coordinates (rounded up).
892 ceil: function () {
893 return this.clone()._ceil();
894 },
895
896 _ceil: function () {
897 this.x = Math.ceil(this.x);
898 this.y = Math.ceil(this.y);
899 return this;
900 },
901
902 // @method trunc(): Point
903 // Returns a copy of the current point with truncated coordinates (rounded towards zero).
904 trunc: function () {
905 return this.clone()._trunc();
906 },
907
908 _trunc: function () {
909 this.x = trunc(this.x);
910 this.y = trunc(this.y);
911 return this;
912 },
913
914 // @method distanceTo(otherPoint: Point): Number
915 // Returns the cartesian distance between the current and the given points.
916 distanceTo: function (point) {
917 point = toPoint(point);
918
919 var x = point.x - this.x,
920 y = point.y - this.y;
921
922 return Math.sqrt(x * x + y * y);
923 },
924
925 // @method equals(otherPoint: Point): Boolean
926 // Returns `true` if the given point has the same coordinates.
927 equals: function (point) {
928 point = toPoint(point);
929
930 return point.x === this.x &&
931 point.y === this.y;
932 },
933
934 // @method contains(otherPoint: Point): Boolean
935 // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
936 contains: function (point) {
937 point = toPoint(point);
938
939 return Math.abs(point.x) <= Math.abs(this.x) &&
940 Math.abs(point.y) <= Math.abs(this.y);
941 },
942
943 // @method toString(): String
944 // Returns a string representation of the point for debugging purposes.
945 toString: function () {
946 return 'Point(' +
947 formatNum(this.x) + ', ' +
948 formatNum(this.y) + ')';
949 }
950 };
951
952 // @factory L.point(x: Number, y: Number, round?: Boolean)
953 // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.
954
955 // @alternative
956 // @factory L.point(coords: Number[])
957 // Expects an array of the form `[x, y]` instead.
958
959 // @alternative
960 // @factory L.point(coords: Object)
961 // Expects a plain object of the form `{x: Number, y: Number}` instead.
962 function toPoint(x, y, round) {
963 if (x instanceof Point) {
964 return x;
965 }
966 if (isArray(x)) {
967 return new Point(x[0], x[1]);
968 }
969 if (x === undefined || x === null) {
970 return x;
971 }
972 if (typeof x === 'object' && 'x' in x && 'y' in x) {
973 return new Point(x.x, x.y);
974 }
975 return new Point(x, y, round);
976 }
977
978 /*
979 * @class Bounds
980 * @aka L.Bounds
981 *
982 * Represents a rectangular area in pixel coordinates.
983 *
984 * @example
985 *
986 * ```js
987 * var p1 = L.point(10, 10),
988 * p2 = L.point(40, 60),
989 * bounds = L.bounds(p1, p2);
990 * ```
991 *
992 * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
993 *
994 * ```js
995 * otherBounds.intersects([[10, 10], [40, 60]]);
996 * ```
997 *
998 * Note that `Bounds` does not inherit from Leaflet's `Class` object,
999 * which means new classes can't inherit from it, and new methods
1000 * can't be added to it with the `include` function.
1001 */
1002
1003 function Bounds(a, b) {
1004 if (!a) { return; }
1005
1006 var points = b ? [a, b] : a;
1007
1008 for (var i = 0, len = points.length; i < len; i++) {
1009 this.extend(points[i]);
1010 }
1011 }
1012
1013 Bounds.prototype = {
1014 // @method extend(point: Point): this
1015 // Extends the bounds to contain the given point.
1016
1017 // @alternative
1018 // @method extend(otherBounds: Bounds): this
1019 // Extend the bounds to contain the given bounds
1020 extend: function (obj) {
1021 var min2, max2;
1022 if (!obj) { return this; }
1023
1024 if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) {
1025 min2 = max2 = toPoint(obj);
1026 } else {
1027 obj = toBounds(obj);
1028 min2 = obj.min;
1029 max2 = obj.max;
1030
1031 if (!min2 || !max2) { return this; }
1032 }
1033
1034 // @property min: Point
1035 // The top left corner of the rectangle.
1036 // @property max: Point
1037 // The bottom right corner of the rectangle.
1038 if (!this.min && !this.max) {
1039 this.min = min2.clone();
1040 this.max = max2.clone();
1041 } else {
1042 this.min.x = Math.min(min2.x, this.min.x);
1043 this.max.x = Math.max(max2.x, this.max.x);
1044 this.min.y = Math.min(min2.y, this.min.y);
1045 this.max.y = Math.max(max2.y, this.max.y);
1046 }
1047 return this;
1048 },
1049
1050 // @method getCenter(round?: Boolean): Point
1051 // Returns the center point of the bounds.
1052 getCenter: function (round) {
1053 return toPoint(
1054 (this.min.x + this.max.x) / 2,
1055 (this.min.y + this.max.y) / 2, round);
1056 },
1057
1058 // @method getBottomLeft(): Point
1059 // Returns the bottom-left point of the bounds.
1060 getBottomLeft: function () {
1061 return toPoint(this.min.x, this.max.y);
1062 },
1063
1064 // @method getTopRight(): Point
1065 // Returns the top-right point of the bounds.
1066 getTopRight: function () { // -> Point
1067 return toPoint(this.max.x, this.min.y);
1068 },
1069
1070 // @method getTopLeft(): Point
1071 // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).
1072 getTopLeft: function () {
1073 return this.min; // left, top
1074 },
1075
1076 // @method getBottomRight(): Point
1077 // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).
1078 getBottomRight: function () {
1079 return this.max; // right, bottom
1080 },
1081
1082 // @method getSize(): Point
1083 // Returns the size of the given bounds
1084 getSize: function () {
1085 return this.max.subtract(this.min);
1086 },
1087
1088 // @method contains(otherBounds: Bounds): Boolean
1089 // Returns `true` if the rectangle contains the given one.
1090 // @alternative
1091 // @method contains(point: Point): Boolean
1092 // Returns `true` if the rectangle contains the given point.
1093 contains: function (obj) {
1094 var min, max;
1095
1096 if (typeof obj[0] === 'number' || obj instanceof Point) {
1097 obj = toPoint(obj);
1098 } else {
1099 obj = toBounds(obj);
1100 }
1101
1102 if (obj instanceof Bounds) {
1103 min = obj.min;
1104 max = obj.max;
1105 } else {
1106 min = max = obj;
1107 }
1108
1109 return (min.x >= this.min.x) &&
1110 (max.x <= this.max.x) &&
1111 (min.y >= this.min.y) &&
1112 (max.y <= this.max.y);
1113 },
1114
1115 // @method intersects(otherBounds: Bounds): Boolean
1116 // Returns `true` if the rectangle intersects the given bounds. Two bounds
1117 // intersect if they have at least one point in common.
1118 intersects: function (bounds) { // (Bounds) -> Boolean
1119 bounds = toBounds(bounds);
1120
1121 var min = this.min,
1122 max = this.max,
1123 min2 = bounds.min,
1124 max2 = bounds.max,
1125 xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
1126 yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
1127
1128 return xIntersects && yIntersects;
1129 },
1130
1131 // @method overlaps(otherBounds: Bounds): Boolean
1132 // Returns `true` if the rectangle overlaps the given bounds. Two bounds
1133 // overlap if their intersection is an area.
1134 overlaps: function (bounds) { // (Bounds) -> Boolean
1135 bounds = toBounds(bounds);
1136
1137 var min = this.min,
1138 max = this.max,
1139 min2 = bounds.min,
1140 max2 = bounds.max,
1141 xOverlaps = (max2.x > min.x) && (min2.x < max.x),
1142 yOverlaps = (max2.y > min.y) && (min2.y < max.y);
1143
1144 return xOverlaps && yOverlaps;
1145 },
1146
1147 // @method isValid(): Boolean
1148 // Returns `true` if the bounds are properly initialized.
1149 isValid: function () {
1150 return !!(this.min && this.max);
1151 },
1152
1153
1154 // @method pad(bufferRatio: Number): Bounds
1155 // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
1156 // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
1157 // Negative values will retract the bounds.
1158 pad: function (bufferRatio) {
1159 var min = this.min,
1160 max = this.max,
1161 heightBuffer = Math.abs(min.x - max.x) * bufferRatio,
1162 widthBuffer = Math.abs(min.y - max.y) * bufferRatio;
1163
1164
1165 return toBounds(
1166 toPoint(min.x - heightBuffer, min.y - widthBuffer),
1167 toPoint(max.x + heightBuffer, max.y + widthBuffer));
1168 },
1169
1170
1171 // @method equals(otherBounds: Bounds): Boolean
1172 // Returns `true` if the rectangle is equivalent to the given bounds.
1173 equals: function (bounds) {
1174 if (!bounds) { return false; }
1175
1176 bounds = toBounds(bounds);
1177
1178 return this.min.equals(bounds.getTopLeft()) &&
1179 this.max.equals(bounds.getBottomRight());
1180 },
1181 };
1182
1183
1184 // @factory L.bounds(corner1: Point, corner2: Point)
1185 // Creates a Bounds object from two corners coordinate pairs.
1186 // @alternative
1187 // @factory L.bounds(points: Point[])
1188 // Creates a Bounds object from the given array of points.
1189 function toBounds(a, b) {
1190 if (!a || a instanceof Bounds) {
1191 return a;
1192 }
1193 return new Bounds(a, b);
1194 }
1195
1196 /*
1197 * @class LatLngBounds
1198 * @aka L.LatLngBounds
1199 *
1200 * Represents a rectangular geographical area on a map.
1201 *
1202 * @example
1203 *
1204 * ```js
1205 * var corner1 = L.latLng(40.712, -74.227),
1206 * corner2 = L.latLng(40.774, -74.125),
1207 * bounds = L.latLngBounds(corner1, corner2);
1208 * ```
1209 *
1210 * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
1211 *
1212 * ```js
1213 * map.fitBounds([
1214 * [40.712, -74.227],
1215 * [40.774, -74.125]
1216 * ]);
1217 * ```
1218 *
1219 * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.
1220 *
1221 * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object,
1222 * which means new classes can't inherit from it, and new methods
1223 * can't be added to it with the `include` function.
1224 */
1225
1226 function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
1227 if (!corner1) { return; }
1228
1229 var latlngs = corner2 ? [corner1, corner2] : corner1;
1230
1231 for (var i = 0, len = latlngs.length; i < len; i++) {
1232 this.extend(latlngs[i]);
1233 }
1234 }
1235
1236 LatLngBounds.prototype = {
1237
1238 // @method extend(latlng: LatLng): this
1239 // Extend the bounds to contain the given point
1240
1241 // @alternative
1242 // @method extend(otherBounds: LatLngBounds): this
1243 // Extend the bounds to contain the given bounds
1244 extend: function (obj) {
1245 var sw = this._southWest,
1246 ne = this._northEast,
1247 sw2, ne2;
1248
1249 if (obj instanceof LatLng) {
1250 sw2 = obj;
1251 ne2 = obj;
1252
1253 } else if (obj instanceof LatLngBounds) {
1254 sw2 = obj._southWest;
1255 ne2 = obj._northEast;
1256
1257 if (!sw2 || !ne2) { return this; }
1258
1259 } else {
1260 return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this;
1261 }
1262
1263 if (!sw && !ne) {
1264 this._southWest = new LatLng(sw2.lat, sw2.lng);
1265 this._northEast = new LatLng(ne2.lat, ne2.lng);
1266 } else {
1267 sw.lat = Math.min(sw2.lat, sw.lat);
1268 sw.lng = Math.min(sw2.lng, sw.lng);
1269 ne.lat = Math.max(ne2.lat, ne.lat);
1270 ne.lng = Math.max(ne2.lng, ne.lng);
1271 }
1272
1273 return this;
1274 },
1275
1276 // @method pad(bufferRatio: Number): LatLngBounds
1277 // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
1278 // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
1279 // Negative values will retract the bounds.
1280 pad: function (bufferRatio) {
1281 var sw = this._southWest,
1282 ne = this._northEast,
1283 heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
1284 widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
1285
1286 return new LatLngBounds(
1287 new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
1288 new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
1289 },
1290
1291 // @method getCenter(): LatLng
1292 // Returns the center point of the bounds.
1293 getCenter: function () {
1294 return new LatLng(
1295 (this._southWest.lat + this._northEast.lat) / 2,
1296 (this._southWest.lng + this._northEast.lng) / 2);
1297 },
1298
1299 // @method getSouthWest(): LatLng
1300 // Returns the south-west point of the bounds.
1301 getSouthWest: function () {
1302 return this._southWest;
1303 },
1304
1305 // @method getNorthEast(): LatLng
1306 // Returns the north-east point of the bounds.
1307 getNorthEast: function () {
1308 return this._northEast;
1309 },
1310
1311 // @method getNorthWest(): LatLng
1312 // Returns the north-west point of the bounds.
1313 getNorthWest: function () {
1314 return new LatLng(this.getNorth(), this.getWest());
1315 },
1316
1317 // @method getSouthEast(): LatLng
1318 // Returns the south-east point of the bounds.
1319 getSouthEast: function () {
1320 return new LatLng(this.getSouth(), this.getEast());
1321 },
1322
1323 // @method getWest(): Number
1324 // Returns the west longitude of the bounds
1325 getWest: function () {
1326 return this._southWest.lng;
1327 },
1328
1329 // @method getSouth(): Number
1330 // Returns the south latitude of the bounds
1331 getSouth: function () {
1332 return this._southWest.lat;
1333 },
1334
1335 // @method getEast(): Number
1336 // Returns the east longitude of the bounds
1337 getEast: function () {
1338 return this._northEast.lng;
1339 },
1340
1341 // @method getNorth(): Number
1342 // Returns the north latitude of the bounds
1343 getNorth: function () {
1344 return this._northEast.lat;
1345 },
1346
1347 // @method contains(otherBounds: LatLngBounds): Boolean
1348 // Returns `true` if the rectangle contains the given one.
1349
1350 // @alternative
1351 // @method contains (latlng: LatLng): Boolean
1352 // Returns `true` if the rectangle contains the given point.
1353 contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
1354 if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) {
1355 obj = toLatLng(obj);
1356 } else {
1357 obj = toLatLngBounds(obj);
1358 }
1359
1360 var sw = this._southWest,
1361 ne = this._northEast,
1362 sw2, ne2;
1363
1364 if (obj instanceof LatLngBounds) {
1365 sw2 = obj.getSouthWest();
1366 ne2 = obj.getNorthEast();
1367 } else {
1368 sw2 = ne2 = obj;
1369 }
1370
1371 return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
1372 (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
1373 },
1374
1375 // @method intersects(otherBounds: LatLngBounds): Boolean
1376 // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.
1377 intersects: function (bounds) {
1378 bounds = toLatLngBounds(bounds);
1379
1380 var sw = this._southWest,
1381 ne = this._northEast,
1382 sw2 = bounds.getSouthWest(),
1383 ne2 = bounds.getNorthEast(),
1384
1385 latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
1386 lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
1387
1388 return latIntersects && lngIntersects;
1389 },
1390
1391 // @method overlaps(otherBounds: LatLngBounds): Boolean
1392 // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.
1393 overlaps: function (bounds) {
1394 bounds = toLatLngBounds(bounds);
1395
1396 var sw = this._southWest,
1397 ne = this._northEast,
1398 sw2 = bounds.getSouthWest(),
1399 ne2 = bounds.getNorthEast(),
1400
1401 latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
1402 lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
1403
1404 return latOverlaps && lngOverlaps;
1405 },
1406
1407 // @method toBBoxString(): String
1408 // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data.
1409 toBBoxString: function () {
1410 return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
1411 },
1412
1413 // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean
1414 // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number.
1415 equals: function (bounds, maxMargin) {
1416 if (!bounds) { return false; }
1417
1418 bounds = toLatLngBounds(bounds);
1419
1420 return this._southWest.equals(bounds.getSouthWest(), maxMargin) &&
1421 this._northEast.equals(bounds.getNorthEast(), maxMargin);
1422 },
1423
1424 // @method isValid(): Boolean
1425 // Returns `true` if the bounds are properly initialized.
1426 isValid: function () {
1427 return !!(this._southWest && this._northEast);
1428 }
1429 };
1430
1431 // TODO International date line?
1432
1433 // @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
1434 // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
1435
1436 // @alternative
1437 // @factory L.latLngBounds(latlngs: LatLng[])
1438 // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds).
1439 function toLatLngBounds(a, b) {
1440 if (a instanceof LatLngBounds) {
1441 return a;
1442 }
1443 return new LatLngBounds(a, b);
1444 }
1445
1446 /* @class LatLng
1447 * @aka L.LatLng
1448 *
1449 * Represents a geographical point with a certain latitude and longitude.
1450 *
1451 * @example
1452 *
1453 * ```
1454 * var latlng = L.latLng(50.5, 30.5);
1455 * ```
1456 *
1457 * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent:
1458 *
1459 * ```
1460 * map.panTo([50, 30]);
1461 * map.panTo({lon: 30, lat: 50});
1462 * map.panTo({lat: 50, lng: 30});
1463 * map.panTo(L.latLng(50, 30));
1464 * ```
1465 *
1466 * Note that `LatLng` does not inherit from Leaflet's `Class` object,
1467 * which means new classes can't inherit from it, and new methods
1468 * can't be added to it with the `include` function.
1469 */
1470
1471 function LatLng(lat, lng, alt) {
1472 if (isNaN(lat) || isNaN(lng)) {
1473 throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
1474 }
1475
1476 // @property lat: Number
1477 // Latitude in degrees
1478 this.lat = +lat;
1479
1480 // @property lng: Number
1481 // Longitude in degrees
1482 this.lng = +lng;
1483
1484 // @property alt: Number
1485 // Altitude in meters (optional)
1486 if (alt !== undefined) {
1487 this.alt = +alt;
1488 }
1489 }
1490
1491 LatLng.prototype = {
1492 // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean
1493 // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number.
1494 equals: function (obj, maxMargin) {
1495 if (!obj) { return false; }
1496
1497 obj = toLatLng(obj);
1498
1499 var margin = Math.max(
1500 Math.abs(this.lat - obj.lat),
1501 Math.abs(this.lng - obj.lng));
1502
1503 return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
1504 },
1505
1506 // @method toString(): String
1507 // Returns a string representation of the point (for debugging purposes).
1508 toString: function (precision) {
1509 return 'LatLng(' +
1510 formatNum(this.lat, precision) + ', ' +
1511 formatNum(this.lng, precision) + ')';
1512 },
1513
1514 // @method distanceTo(otherLatLng: LatLng): Number
1515 // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines).
1516 distanceTo: function (other) {
1517 return Earth.distance(this, toLatLng(other));
1518 },
1519
1520 // @method wrap(): LatLng
1521 // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.
1522 wrap: function () {
1523 return Earth.wrapLatLng(this);
1524 },
1525
1526 // @method toBounds(sizeInMeters: Number): LatLngBounds
1527 // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.
1528 toBounds: function (sizeInMeters) {
1529 var latAccuracy = 180 * sizeInMeters / 40075017,
1530 lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
1531
1532 return toLatLngBounds(
1533 [this.lat - latAccuracy, this.lng - lngAccuracy],
1534 [this.lat + latAccuracy, this.lng + lngAccuracy]);
1535 },
1536
1537 clone: function () {
1538 return new LatLng(this.lat, this.lng, this.alt);
1539 }
1540 };
1541
1542
1543
1544 // @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng
1545 // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).
1546
1547 // @alternative
1548 // @factory L.latLng(coords: Array): LatLng
1549 // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.
1550
1551 // @alternative
1552 // @factory L.latLng(coords: Object): LatLng
1553 // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.
1554
1555 function toLatLng(a, b, c) {
1556 if (a instanceof LatLng) {
1557 return a;
1558 }
1559 if (isArray(a) && typeof a[0] !== 'object') {
1560 if (a.length === 3) {
1561 return new LatLng(a[0], a[1], a[2]);
1562 }
1563 if (a.length === 2) {
1564 return new LatLng(a[0], a[1]);
1565 }
1566 return null;
1567 }
1568 if (a === undefined || a === null) {
1569 return a;
1570 }
1571 if (typeof a === 'object' && 'lat' in a) {
1572 return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
1573 }
1574 if (b === undefined) {
1575 return null;
1576 }
1577 return new LatLng(a, b, c);
1578 }
1579
1580 /*
1581 * @namespace CRS
1582 * @crs L.CRS.Base
1583 * Object that defines coordinate reference systems for projecting
1584 * geographical points into pixel (screen) coordinates and back (and to
1585 * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See
1586 * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system).
1587 *
1588 * Leaflet defines the most usual CRSs by default. If you want to use a
1589 * CRS not defined by default, take a look at the
1590 * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.
1591 *
1592 * Note that the CRS instances do not inherit from Leaflet's `Class` object,
1593 * and can't be instantiated. Also, new classes can't inherit from them,
1594 * and methods can't be added to them with the `include` function.
1595 */
1596
1597 var CRS = {
1598 // @method latLngToPoint(latlng: LatLng, zoom: Number): Point
1599 // Projects geographical coordinates into pixel coordinates for a given zoom.
1600 latLngToPoint: function (latlng, zoom) {
1601 var projectedPoint = this.projection.project(latlng),
1602 scale = this.scale(zoom);
1603
1604 return this.transformation._transform(projectedPoint, scale);
1605 },
1606
1607 // @method pointToLatLng(point: Point, zoom: Number): LatLng
1608 // The inverse of `latLngToPoint`. Projects pixel coordinates on a given
1609 // zoom into geographical coordinates.
1610 pointToLatLng: function (point, zoom) {
1611 var scale = this.scale(zoom),
1612 untransformedPoint = this.transformation.untransform(point, scale);
1613
1614 return this.projection.unproject(untransformedPoint);
1615 },
1616
1617 // @method project(latlng: LatLng): Point
1618 // Projects geographical coordinates into coordinates in units accepted for
1619 // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).
1620 project: function (latlng) {
1621 return this.projection.project(latlng);
1622 },
1623
1624 // @method unproject(point: Point): LatLng
1625 // Given a projected coordinate returns the corresponding LatLng.
1626 // The inverse of `project`.
1627 unproject: function (point) {
1628 return this.projection.unproject(point);
1629 },
1630
1631 // @method scale(zoom: Number): Number
1632 // Returns the scale used when transforming projected coordinates into
1633 // pixel coordinates for a particular zoom. For example, it returns
1634 // `256 * 2^zoom` for Mercator-based CRS.
1635 scale: function (zoom) {
1636 return 256 * Math.pow(2, zoom);
1637 },
1638
1639 // @method zoom(scale: Number): Number
1640 // Inverse of `scale()`, returns the zoom level corresponding to a scale
1641 // factor of `scale`.
1642 zoom: function (scale) {
1643 return Math.log(scale / 256) / Math.LN2;
1644 },
1645
1646 // @method getProjectedBounds(zoom: Number): Bounds
1647 // Returns the projection's bounds scaled and transformed for the provided `zoom`.
1648 getProjectedBounds: function (zoom) {
1649 if (this.infinite) { return null; }
1650
1651 var b = this.projection.bounds,
1652 s = this.scale(zoom),
1653 min = this.transformation.transform(b.min, s),
1654 max = this.transformation.transform(b.max, s);
1655
1656 return new Bounds(min, max);
1657 },
1658
1659 // @method distance(latlng1: LatLng, latlng2: LatLng): Number
1660 // Returns the distance between two geographical coordinates.
1661
1662 // @property code: String
1663 // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)
1664 //
1665 // @property wrapLng: Number[]
1666 // An array of two numbers defining whether the longitude (horizontal) coordinate
1667 // axis wraps around a given range and how. Defaults to `[-180, 180]` in most
1668 // geographical CRSs. If `undefined`, the longitude axis does not wrap around.
1669 //
1670 // @property wrapLat: Number[]
1671 // Like `wrapLng`, but for the latitude (vertical) axis.
1672
1673 // wrapLng: [min, max],
1674 // wrapLat: [min, max],
1675
1676 // @property infinite: Boolean
1677 // If true, the coordinate space will be unbounded (infinite in both axes)
1678 infinite: false,
1679
1680 // @method wrapLatLng(latlng: LatLng): LatLng
1681 // Returns a `LatLng` where lat and lng has been wrapped according to the
1682 // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.
1683 wrapLatLng: function (latlng) {
1684 var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,
1685 lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat,
1686 alt = latlng.alt;
1687
1688 return new LatLng(lat, lng, alt);
1689 },
1690
1691 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
1692 // Returns a `LatLngBounds` with the same size as the given one, ensuring
1693 // that its center is within the CRS's bounds.
1694 // Only accepts actual `L.LatLngBounds` instances, not arrays.
1695 wrapLatLngBounds: function (bounds) {
1696 var center = bounds.getCenter(),
1697 newCenter = this.wrapLatLng(center),
1698 latShift = center.lat - newCenter.lat,
1699 lngShift = center.lng - newCenter.lng;
1700
1701 if (latShift === 0 && lngShift === 0) {
1702 return bounds;
1703 }
1704
1705 var sw = bounds.getSouthWest(),
1706 ne = bounds.getNorthEast(),
1707 newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift),
1708 newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift);
1709
1710 return new LatLngBounds(newSw, newNe);
1711 }
1712 };
1713
1714 /*
1715 * @namespace CRS
1716 * @crs L.CRS.Earth
1717 *
1718 * Serves as the base for CRS that are global such that they cover the earth.
1719 * Can only be used as the base for other CRS and cannot be used directly,
1720 * since it does not have a `code`, `projection` or `transformation`. `distance()` returns
1721 * meters.
1722 */
1723
1724 var Earth = extend({}, CRS, {
1725 wrapLng: [-180, 180],
1726
1727 // Mean Earth Radius, as recommended for use by
1728 // the International Union of Geodesy and Geophysics,
1729 // see https://rosettacode.org/wiki/Haversine_formula
1730 R: 6371000,
1731
1732 // distance between two geographical points using spherical law of cosines approximation
1733 distance: function (latlng1, latlng2) {
1734 var rad = Math.PI / 180,
1735 lat1 = latlng1.lat * rad,
1736 lat2 = latlng2.lat * rad,
1737 sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2),
1738 sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2),
1739 a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,
1740 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1741 return this.R * c;
1742 }
1743 });
1744
1745 /*
1746 * @namespace Projection
1747 * @projection L.Projection.SphericalMercator
1748 *
1749 * Spherical Mercator projection — the most common projection for online maps,
1750 * used by almost all free and commercial tile providers. Assumes that Earth is
1751 * a sphere. Used by the `EPSG:3857` CRS.
1752 */
1753
1754 var earthRadius = 6378137;
1755
1756 var SphericalMercator = {
1757
1758 R: earthRadius,
1759 MAX_LATITUDE: 85.0511287798,
1760
1761 project: function (latlng) {
1762 var d = Math.PI / 180,
1763 max = this.MAX_LATITUDE,
1764 lat = Math.max(Math.min(max, latlng.lat), -max),
1765 sin = Math.sin(lat * d);
1766
1767 return new Point(
1768 this.R * latlng.lng * d,
1769 this.R * Math.log((1 + sin) / (1 - sin)) / 2);
1770 },
1771
1772 unproject: function (point) {
1773 var d = 180 / Math.PI;
1774
1775 return new LatLng(
1776 (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
1777 point.x * d / this.R);
1778 },
1779
1780 bounds: (function () {
1781 var d = earthRadius * Math.PI;
1782 return new Bounds([-d, -d], [d, d]);
1783 })()
1784 };
1785
1786 /*
1787 * @class Transformation
1788 * @aka L.Transformation
1789 *
1790 * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
1791 * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
1792 * the reverse. Used by Leaflet in its projections code.
1793 *
1794 * @example
1795 *
1796 * ```js
1797 * var transformation = L.transformation(2, 5, -1, 10),
1798 * p = L.point(1, 2),
1799 * p2 = transformation.transform(p), // L.point(7, 8)
1800 * p3 = transformation.untransform(p2); // L.point(1, 2)
1801 * ```
1802 */
1803
1804
1805 // factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
1806 // Creates a `Transformation` object with the given coefficients.
1807 function Transformation(a, b, c, d) {
1808 if (isArray(a)) {
1809 // use array properties
1810 this._a = a[0];
1811 this._b = a[1];
1812 this._c = a[2];
1813 this._d = a[3];
1814 return;
1815 }
1816 this._a = a;
1817 this._b = b;
1818 this._c = c;
1819 this._d = d;
1820 }
1821
1822 Transformation.prototype = {
1823 // @method transform(point: Point, scale?: Number): Point
1824 // Returns a transformed point, optionally multiplied by the given scale.
1825 // Only accepts actual `L.Point` instances, not arrays.
1826 transform: function (point, scale) { // (Point, Number) -> Point
1827 return this._transform(point.clone(), scale);
1828 },
1829
1830 // destructive transform (faster)
1831 _transform: function (point, scale) {
1832 scale = scale || 1;
1833 point.x = scale * (this._a * point.x + this._b);
1834 point.y = scale * (this._c * point.y + this._d);
1835 return point;
1836 },
1837
1838 // @method untransform(point: Point, scale?: Number): Point
1839 // Returns the reverse transformation of the given point, optionally divided
1840 // by the given scale. Only accepts actual `L.Point` instances, not arrays.
1841 untransform: function (point, scale) {
1842 scale = scale || 1;
1843 return new Point(
1844 (point.x / scale - this._b) / this._a,
1845 (point.y / scale - this._d) / this._c);
1846 }
1847 };
1848
1849 // factory L.transformation(a: Number, b: Number, c: Number, d: Number)
1850
1851 // @factory L.transformation(a: Number, b: Number, c: Number, d: Number)
1852 // Instantiates a Transformation object with the given coefficients.
1853
1854 // @alternative
1855 // @factory L.transformation(coefficients: Array): Transformation
1856 // Expects an coefficients array of the form
1857 // `[a: Number, b: Number, c: Number, d: Number]`.
1858
1859 function toTransformation(a, b, c, d) {
1860 return new Transformation(a, b, c, d);
1861 }
1862
1863 /*
1864 * @namespace CRS
1865 * @crs L.CRS.EPSG3857
1866 *
1867 * The most common CRS for online maps, used by almost all free and commercial
1868 * tile providers. Uses Spherical Mercator projection. Set in by default in
1869 * Map's `crs` option.
1870 */
1871
1872 var EPSG3857 = extend({}, Earth, {
1873 code: 'EPSG:3857',
1874 projection: SphericalMercator,
1875
1876 transformation: (function () {
1877 var scale = 0.5 / (Math.PI * SphericalMercator.R);
1878 return toTransformation(scale, 0.5, -scale, 0.5);
1879 }())
1880 });
1881
1882 var EPSG900913 = extend({}, EPSG3857, {
1883 code: 'EPSG:900913'
1884 });
1885
1886 // @namespace SVG; @section
1887 // There are several static functions which can be called without instantiating L.SVG:
1888
1889 // @function create(name: String): SVGElement
1890 // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
1891 // corresponding to the class name passed. For example, using 'line' will return
1892 // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
1893 function svgCreate(name) {
1894 return document.createElementNS('http://www.w3.org/2000/svg', name);
1895 }
1896
1897 // @function pointsToPath(rings: Point[], closed: Boolean): String
1898 // Generates a SVG path string for multiple rings, with each ring turning
1899 // into "M..L..L.." instructions
1900 function pointsToPath(rings, closed) {
1901 var str = '',
1902 i, j, len, len2, points, p;
1903
1904 for (i = 0, len = rings.length; i < len; i++) {
1905 points = rings[i];
1906
1907 for (j = 0, len2 = points.length; j < len2; j++) {
1908 p = points[j];
1909 str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
1910 }
1911
1912 // closes the ring for polygons; "x" is VML syntax
1913 str += closed ? (Browser.svg ? 'z' : 'x') : '';
1914 }
1915
1916 // SVG complains about empty path strings
1917 return str || 'M0 0';
1918 }
1919
1920 /*
1921 * @namespace Browser
1922 * @aka L.Browser
1923 *
1924 * A namespace with static properties for browser/feature detection used by Leaflet internally.
1925 *
1926 * @example
1927 *
1928 * ```js
1929 * if (L.Browser.ielt9) {
1930 * alert('Upgrade your browser, dude!');
1931 * }
1932 * ```
1933 */
1934
1935 var style = document.documentElement.style;
1936
1937 // @property ie: Boolean; `true` for all Internet Explorer versions (not Edge).
1938 var ie = 'ActiveXObject' in window;
1939
1940 // @property ielt9: Boolean; `true` for Internet Explorer versions less than 9.
1941 var ielt9 = ie && !document.addEventListener;
1942
1943 // @property edge: Boolean; `true` for the Edge web browser.
1944 var edge = 'msLaunchUri' in navigator && !('documentMode' in document);
1945
1946 // @property webkit: Boolean;
1947 // `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
1948 var webkit = userAgentContains('webkit');
1949
1950 // @property android: Boolean
1951 // **Deprecated.** `true` for any browser running on an Android platform.
1952 var android = userAgentContains('android');
1953
1954 // @property android23: Boolean; **Deprecated.** `true` for browsers running on Android 2 or Android 3.
1955 var android23 = userAgentContains('android 2') || userAgentContains('android 3');
1956
1957 /* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */
1958 var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit
1959 // @property androidStock: Boolean; **Deprecated.** `true` for the Android stock browser (i.e. not Chrome)
1960 var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window);
1961
1962 // @property opera: Boolean; `true` for the Opera browser
1963 var opera = !!window.opera;
1964
1965 // @property chrome: Boolean; `true` for the Chrome browser.
1966 var chrome = !edge && userAgentContains('chrome');
1967
1968 // @property gecko: Boolean; `true` for gecko-based browsers like Firefox.
1969 var gecko = userAgentContains('gecko') && !webkit && !opera && !ie;
1970
1971 // @property safari: Boolean; `true` for the Safari browser.
1972 var safari = !chrome && userAgentContains('safari');
1973
1974 var phantom = userAgentContains('phantom');
1975
1976 // @property opera12: Boolean
1977 // `true` for the Opera browser supporting CSS transforms (version 12 or later).
1978 var opera12 = 'OTransition' in style;
1979
1980 // @property win: Boolean; `true` when the browser is running in a Windows platform
1981 var win = navigator.platform.indexOf('Win') === 0;
1982
1983 // @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms.
1984 var ie3d = ie && ('transition' in style);
1985
1986 // @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms.
1987 var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23;
1988
1989 // @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms.
1990 var gecko3d = 'MozPerspective' in style;
1991
1992 // @property any3d: Boolean
1993 // `true` for all browsers supporting CSS transforms.
1994 var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom;
1995
1996 // @property mobile: Boolean; `true` for all browsers running in a mobile device.
1997 var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile');
1998
1999 // @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device.
2000 var mobileWebkit = mobile && webkit;
2001
2002 // @property mobileWebkit3d: Boolean
2003 // `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
2004 var mobileWebkit3d = mobile && webkit3d;
2005
2006 // @property msPointer: Boolean
2007 // `true` for browsers implementing the Microsoft touch events model (notably IE10).
2008 var msPointer = !window.PointerEvent && window.MSPointerEvent;
2009
2010 // @property pointer: Boolean
2011 // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
2012 var pointer = !!(window.PointerEvent || msPointer);
2013
2014 // @property touchNative: Boolean
2015 // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
2016 // **This does not necessarily mean** that the browser is running in a computer with
2017 // a touchscreen, it only means that the browser is capable of understanding
2018 // touch events.
2019 var touchNative = 'ontouchstart' in window || !!window.TouchEvent;
2020
2021 // @property touch: Boolean
2022 // `true` for all browsers supporting either [touch](#browser-touch) or [pointer](#browser-pointer) events.
2023 // Note: pointer events will be preferred (if available), and processed for all `touch*` listeners.
2024 var touch = !window.L_NO_TOUCH && (touchNative || pointer);
2025
2026 // @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device.
2027 var mobileOpera = mobile && opera;
2028
2029 // @property mobileGecko: Boolean
2030 // `true` for gecko-based browsers running in a mobile device.
2031 var mobileGecko = mobile && gecko;
2032
2033 // @property retina: Boolean
2034 // `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%.
2035 var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1;
2036
2037 // @property passiveEvents: Boolean
2038 // `true` for browsers that support passive events.
2039 var passiveEvents = (function () {
2040 var supportsPassiveOption = false;
2041 try {
2042 var opts = Object.defineProperty({}, 'passive', {
2043 get: function () { // eslint-disable-line getter-return
2044 supportsPassiveOption = true;
2045 }
2046 });
2047 window.addEventListener('testPassiveEventSupport', falseFn, opts);
2048 window.removeEventListener('testPassiveEventSupport', falseFn, opts);
2049 } catch (e) {
2050 // Errors can safely be ignored since this is only a browser support test.
2051 }
2052 return supportsPassiveOption;
2053 }());
2054
2055 // @property canvas: Boolean
2056 // `true` when the browser supports [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
2057 var canvas$1 = (function () {
2058 return !!document.createElement('canvas').getContext;
2059 }());
2060
2061 // @property svg: Boolean
2062 // `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG).
2063 var svg$1 = !!(document.createElementNS && svgCreate('svg').createSVGRect);
2064
2065 var inlineSvg = !!svg$1 && (function () {
2066 var div = document.createElement('div');
2067 div.innerHTML = '<svg/>';
2068 return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
2069 })();
2070
2071 // @property vml: Boolean
2072 // `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language).
2073 var vml = !svg$1 && (function () {
2074 try {
2075 var div = document.createElement('div');
2076 div.innerHTML = '<v:shape adj="1"/>';
2077
2078 var shape = div.firstChild;
2079 shape.style.behavior = 'url(#default#VML)';
2080
2081 return shape && (typeof shape.adj === 'object');
2082
2083 } catch (e) {
2084 return false;
2085 }
2086 }());
2087
2088
2089 // @property mac: Boolean; `true` when the browser is running in a Mac platform
2090 var mac = navigator.platform.indexOf('Mac') === 0;
2091
2092 // @property mac: Boolean; `true` when the browser is running in a Linux platform
2093 var linux = navigator.platform.indexOf('Linux') === 0;
2094
2095 function userAgentContains(str) {
2096 return navigator.userAgent.toLowerCase().indexOf(str) >= 0;
2097 }
2098
2099
2100 var Browser = {
2101 ie: ie,
2102 ielt9: ielt9,
2103 edge: edge,
2104 webkit: webkit,
2105 android: android,
2106 android23: android23,
2107 androidStock: androidStock,
2108 opera: opera,
2109 chrome: chrome,
2110 gecko: gecko,
2111 safari: safari,
2112 phantom: phantom,
2113 opera12: opera12,
2114 win: win,
2115 ie3d: ie3d,
2116 webkit3d: webkit3d,
2117 gecko3d: gecko3d,
2118 any3d: any3d,
2119 mobile: mobile,
2120 mobileWebkit: mobileWebkit,
2121 mobileWebkit3d: mobileWebkit3d,
2122 msPointer: msPointer,
2123 pointer: pointer,
2124 touch: touch,
2125 touchNative: touchNative,
2126 mobileOpera: mobileOpera,
2127 mobileGecko: mobileGecko,
2128 retina: retina,
2129 passiveEvents: passiveEvents,
2130 canvas: canvas$1,
2131 svg: svg$1,
2132 vml: vml,
2133 inlineSvg: inlineSvg,
2134 mac: mac,
2135 linux: linux
2136 };
2137
2138 /*
2139 * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices.
2140 */
2141
2142 var POINTER_DOWN = Browser.msPointer ? 'MSPointerDown' : 'pointerdown';
2143 var POINTER_MOVE = Browser.msPointer ? 'MSPointerMove' : 'pointermove';
2144 var POINTER_UP = Browser.msPointer ? 'MSPointerUp' : 'pointerup';
2145 var POINTER_CANCEL = Browser.msPointer ? 'MSPointerCancel' : 'pointercancel';
2146 var pEvent = {
2147 touchstart : POINTER_DOWN,
2148 touchmove : POINTER_MOVE,
2149 touchend : POINTER_UP,
2150 touchcancel : POINTER_CANCEL
2151 };
2152 var handle = {
2153 touchstart : _onPointerStart,
2154 touchmove : _handlePointer,
2155 touchend : _handlePointer,
2156 touchcancel : _handlePointer
2157 };
2158 var _pointers = {};
2159 var _pointerDocListener = false;
2160
2161 // Provides a touch events wrapper for (ms)pointer events.
2162 // ref https://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890
2163
2164 function addPointerListener(obj, type, handler) {
2165 if (type === 'touchstart') {
2166 _addPointerDocListener();
2167 }
2168 if (!handle[type]) {
2169 console.warn('wrong event specified:', type);
2170 return falseFn;
2171 }
2172 handler = handle[type].bind(this, handler);
2173 obj.addEventListener(pEvent[type], handler, false);
2174 return handler;
2175 }
2176
2177 function removePointerListener(obj, type, handler) {
2178 if (!pEvent[type]) {
2179 console.warn('wrong event specified:', type);
2180 return;
2181 }
2182 obj.removeEventListener(pEvent[type], handler, false);
2183 }
2184
2185 function _globalPointerDown(e) {
2186 _pointers[e.pointerId] = e;
2187 }
2188
2189 function _globalPointerMove(e) {
2190 if (_pointers[e.pointerId]) {
2191 _pointers[e.pointerId] = e;
2192 }
2193 }
2194
2195 function _globalPointerUp(e) {
2196 delete _pointers[e.pointerId];
2197 }
2198
2199 function _addPointerDocListener() {
2200 // need to keep track of what pointers and how many are active to provide e.touches emulation
2201 if (!_pointerDocListener) {
2202 // we listen document as any drags that end by moving the touch off the screen get fired there
2203 document.addEventListener(POINTER_DOWN, _globalPointerDown, true);
2204 document.addEventListener(POINTER_MOVE, _globalPointerMove, true);
2205 document.addEventListener(POINTER_UP, _globalPointerUp, true);
2206 document.addEventListener(POINTER_CANCEL, _globalPointerUp, true);
2207
2208 _pointerDocListener = true;
2209 }
2210 }
2211
2212 function _handlePointer(handler, e) {
2213 if (e.pointerType === (e.MSPOINTER_TYPE_MOUSE || 'mouse')) { return; }
2214
2215 e.touches = [];
2216 for (var i in _pointers) {
2217 e.touches.push(_pointers[i]);
2218 }
2219 e.changedTouches = [e];
2220
2221 handler(e);
2222 }
2223
2224 function _onPointerStart(handler, e) {
2225 // IE10 specific: MsTouch needs preventDefault. See #2000
2226 if (e.MSPOINTER_TYPE_TOUCH && e.pointerType === e.MSPOINTER_TYPE_TOUCH) {
2227 preventDefault(e);
2228 }
2229 _handlePointer(handler, e);
2230 }
2231
2232 /*
2233 * Extends the event handling code with double tap support for mobile browsers.
2234 *
2235 * Note: currently most browsers fire native dblclick, with only a few exceptions
2236 * (see https://github.com/Leaflet/Leaflet/issues/7012#issuecomment-595087386)
2237 */
2238
2239 function makeDblclick(event) {
2240 // in modern browsers `type` cannot be just overridden:
2241 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only
2242 var newEvent = {},
2243 prop, i;
2244 for (i in event) {
2245 prop = event[i];
2246 newEvent[i] = prop && prop.bind ? prop.bind(event) : prop;
2247 }
2248 event = newEvent;
2249 newEvent.type = 'dblclick';
2250 newEvent.detail = 2;
2251 newEvent.isTrusted = false;
2252 newEvent._simulated = true; // for debug purposes
2253 return newEvent;
2254 }
2255
2256 var delay = 200;
2257 function addDoubleTapListener(obj, handler) {
2258 // Most browsers handle double tap natively
2259 obj.addEventListener('dblclick', handler);
2260
2261 // On some platforms the browser doesn't fire native dblclicks for touch events.
2262 // It seems that in all such cases `detail` property of `click` event is always `1`.
2263 // So here we rely on that fact to avoid excessive 'dblclick' simulation when not needed.
2264 var last = 0,
2265 detail;
2266 function simDblclick(e) {
2267 if (e.detail !== 1) {
2268 detail = e.detail; // keep in sync to avoid false dblclick in some cases
2269 return;
2270 }
2271
2272 if (e.pointerType === 'mouse' ||
2273 (e.sourceCapabilities && !e.sourceCapabilities.firesTouchEvents)) {
2274
2275 return;
2276 }
2277
2278 // When clicking on an <input>, the browser generates a click on its
2279 // <label> (and vice versa) triggering two clicks in quick succession.
2280 // This ignores clicks on elements which are a label with a 'for'
2281 // attribute (or children of such a label), but not children of
2282 // a <input>.
2283 var path = getPropagationPath(e);
2284 if (path.some(function (el) {
2285 return el instanceof HTMLLabelElement && el.attributes.for;
2286 }) &&
2287 !path.some(function (el) {
2288 return (
2289 el instanceof HTMLInputElement ||
2290 el instanceof HTMLSelectElement
2291 );
2292 })
2293 ) {
2294 return;
2295 }
2296
2297 var now = Date.now();
2298 if (now - last <= delay) {
2299 detail++;
2300 if (detail === 2) {
2301 handler(makeDblclick(e));
2302 }
2303 } else {
2304 detail = 1;
2305 }
2306 last = now;
2307 }
2308
2309 obj.addEventListener('click', simDblclick);
2310
2311 return {
2312 dblclick: handler,
2313 simDblclick: simDblclick
2314 };
2315 }
2316
2317 function removeDoubleTapListener(obj, handlers) {
2318 obj.removeEventListener('dblclick', handlers.dblclick);
2319 obj.removeEventListener('click', handlers.simDblclick);
2320 }
2321
2322 /*
2323 * @namespace DomUtil
2324 *
2325 * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model)
2326 * tree, used by Leaflet internally.
2327 *
2328 * Most functions expecting or returning a `HTMLElement` also work for
2329 * SVG elements. The only difference is that classes refer to CSS classes
2330 * in HTML and SVG classes in SVG.
2331 */
2332
2333
2334 // @property TRANSFORM: String
2335 // Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit).
2336 var TRANSFORM = testProp(
2337 ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
2338
2339 // webkitTransition comes first because some browser versions that drop vendor prefix don't do
2340 // the same for the transitionend event, in particular the Android 4.1 stock browser
2341
2342 // @property TRANSITION: String
2343 // Vendor-prefixed transition style name.
2344 var TRANSITION = testProp(
2345 ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
2346
2347 // @property TRANSITION_END: String
2348 // Vendor-prefixed transitionend event name.
2349 var TRANSITION_END =
2350 TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend';
2351
2352
2353 // @function get(id: String|HTMLElement): HTMLElement
2354 // Returns an element given its DOM id, or returns the element itself
2355 // if it was passed directly.
2356 function get(id) {
2357 return typeof id === 'string' ? document.getElementById(id) : id;
2358 }
2359
2360 // @function getStyle(el: HTMLElement, styleAttrib: String): String
2361 // Returns the value for a certain style attribute on an element,
2362 // including computed values or values set through CSS.
2363 function getStyle(el, style) {
2364 var value = el.style[style] || (el.currentStyle && el.currentStyle[style]);
2365
2366 if ((!value || value === 'auto') && document.defaultView) {
2367 var css = document.defaultView.getComputedStyle(el, null);
2368 value = css ? css[style] : null;
2369 }
2370 return value === 'auto' ? null : value;
2371 }
2372
2373 // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement
2374 // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element.
2375 function create$1(tagName, className, container) {
2376 var el = document.createElement(tagName);
2377 el.className = className || '';
2378
2379 if (container) {
2380 container.appendChild(el);
2381 }
2382 return el;
2383 }
2384
2385 // @function remove(el: HTMLElement)
2386 // Removes `el` from its parent element
2387 function remove(el) {
2388 var parent = el.parentNode;
2389 if (parent) {
2390 parent.removeChild(el);
2391 }
2392 }
2393
2394 // @function empty(el: HTMLElement)
2395 // Removes all of `el`'s children elements from `el`
2396 function empty(el) {
2397 while (el.firstChild) {
2398 el.removeChild(el.firstChild);
2399 }
2400 }
2401
2402 // @function toFront(el: HTMLElement)
2403 // Makes `el` the last child of its parent, so it renders in front of the other children.
2404 function toFront(el) {
2405 var parent = el.parentNode;
2406 if (parent && parent.lastChild !== el) {
2407 parent.appendChild(el);
2408 }
2409 }
2410
2411 // @function toBack(el: HTMLElement)
2412 // Makes `el` the first child of its parent, so it renders behind the other children.
2413 function toBack(el) {
2414 var parent = el.parentNode;
2415 if (parent && parent.firstChild !== el) {
2416 parent.insertBefore(el, parent.firstChild);
2417 }
2418 }
2419
2420 // @function hasClass(el: HTMLElement, name: String): Boolean
2421 // Returns `true` if the element's class attribute contains `name`.
2422 function hasClass(el, name) {
2423 if (el.classList !== undefined) {
2424 return el.classList.contains(name);
2425 }
2426 var className = getClass(el);
2427 return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
2428 }
2429
2430 // @function addClass(el: HTMLElement, name: String)
2431 // Adds `name` to the element's class attribute.
2432 function addClass(el, name) {
2433 if (el.classList !== undefined) {
2434 var classes = splitWords(name);
2435 for (var i = 0, len = classes.length; i < len; i++) {
2436 el.classList.add(classes[i]);
2437 }
2438 } else if (!hasClass(el, name)) {
2439 var className = getClass(el);
2440 setClass(el, (className ? className + ' ' : '') + name);
2441 }
2442 }
2443
2444 // @function removeClass(el: HTMLElement, name: String)
2445 // Removes `name` from the element's class attribute.
2446 function removeClass(el, name) {
2447 if (el.classList !== undefined) {
2448 el.classList.remove(name);
2449 } else {
2450 setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
2451 }
2452 }
2453
2454 // @function setClass(el: HTMLElement, name: String)
2455 // Sets the element's class.
2456 function setClass(el, name) {
2457 if (el.className.baseVal === undefined) {
2458 el.className = name;
2459 } else {
2460 // in case of SVG element
2461 el.className.baseVal = name;
2462 }
2463 }
2464
2465 // @function getClass(el: HTMLElement): String
2466 // Returns the element's class.
2467 function getClass(el) {
2468 // Check if the element is an SVGElementInstance and use the correspondingElement instead
2469 // (Required for linked SVG elements in IE11.)
2470 if (el.correspondingElement) {
2471 el = el.correspondingElement;
2472 }
2473 return el.className.baseVal === undefined ? el.className : el.className.baseVal;
2474 }
2475
2476 // @function setOpacity(el: HTMLElement, opacity: Number)
2477 // Set the opacity of an element (including old IE support).
2478 // `opacity` must be a number from `0` to `1`.
2479 function setOpacity(el, value) {
2480 if ('opacity' in el.style) {
2481 el.style.opacity = value;
2482 } else if ('filter' in el.style) {
2483 _setOpacityIE(el, value);
2484 }
2485 }
2486
2487 function _setOpacityIE(el, value) {
2488 var filter = false,
2489 filterName = 'DXImageTransform.Microsoft.Alpha';
2490
2491 // filters collection throws an error if we try to retrieve a filter that doesn't exist
2492 try {
2493 filter = el.filters.item(filterName);
2494 } catch (e) {
2495 // don't set opacity to 1 if we haven't already set an opacity,
2496 // it isn't needed and breaks transparent pngs.
2497 if (value === 1) { return; }
2498 }
2499
2500 value = Math.round(value * 100);
2501
2502 if (filter) {
2503 filter.Enabled = (value !== 100);
2504 filter.Opacity = value;
2505 } else {
2506 el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
2507 }
2508 }
2509
2510 // @function testProp(props: String[]): String|false
2511 // Goes through the array of style names and returns the first name
2512 // that is a valid style name for an element. If no such name is found,
2513 // it returns false. Useful for vendor-prefixed styles like `transform`.
2514 function testProp(props) {
2515 var style = document.documentElement.style;
2516
2517 for (var i = 0; i < props.length; i++) {
2518 if (props[i] in style) {
2519 return props[i];
2520 }
2521 }
2522 return false;
2523 }
2524
2525 // @function setTransform(el: HTMLElement, offset: Point, scale?: Number)
2526 // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels
2527 // and optionally scaled by `scale`. Does not have an effect if the
2528 // browser doesn't support 3D CSS transforms.
2529 function setTransform(el, offset, scale) {
2530 var pos = offset || new Point(0, 0);
2531
2532 el.style[TRANSFORM] =
2533 (Browser.ie3d ?
2534 'translate(' + pos.x + 'px,' + pos.y + 'px)' :
2535 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') +
2536 (scale ? ' scale(' + scale + ')' : '');
2537 }
2538
2539 // @function setPosition(el: HTMLElement, position: Point)
2540 // Sets the position of `el` to coordinates specified by `position`,
2541 // using CSS translate or top/left positioning depending on the browser
2542 // (used by Leaflet internally to position its layers).
2543 function setPosition(el, point) {
2544
2545 /*eslint-disable */
2546 el._leaflet_pos = point;
2547 /* eslint-enable */
2548
2549 if (Browser.any3d) {
2550 setTransform(el, point);
2551 } else {
2552 el.style.left = point.x + 'px';
2553 el.style.top = point.y + 'px';
2554 }
2555 }
2556
2557 // @function getPosition(el: HTMLElement): Point
2558 // Returns the coordinates of an element previously positioned with setPosition.
2559 function getPosition(el) {
2560 // this method is only used for elements previously positioned using setPosition,
2561 // so it's safe to cache the position for performance
2562
2563 return el._leaflet_pos || new Point(0, 0);
2564 }
2565
2566 // @function disableTextSelection()
2567 // Prevents the user from generating `selectstart` DOM events, usually generated
2568 // when the user drags the mouse through a page with text. Used internally
2569 // by Leaflet to override the behaviour of any click-and-drag interaction on
2570 // the map. Affects drag interactions on the whole document.
2571
2572 // @function enableTextSelection()
2573 // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection).
2574 var disableTextSelection;
2575 var enableTextSelection;
2576 var _userSelect;
2577 if ('onselectstart' in document) {
2578 disableTextSelection = function () {
2579 on(window, 'selectstart', preventDefault);
2580 };
2581 enableTextSelection = function () {
2582 off(window, 'selectstart', preventDefault);
2583 };
2584 } else {
2585 var userSelectProperty = testProp(
2586 ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);
2587
2588 disableTextSelection = function () {
2589 if (userSelectProperty) {
2590 var style = document.documentElement.style;
2591 _userSelect = style[userSelectProperty];
2592 style[userSelectProperty] = 'none';
2593 }
2594 };
2595 enableTextSelection = function () {
2596 if (userSelectProperty) {
2597 document.documentElement.style[userSelectProperty] = _userSelect;
2598 _userSelect = undefined;
2599 }
2600 };
2601 }
2602
2603 // @function disableImageDrag()
2604 // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but
2605 // for `dragstart` DOM events, usually generated when the user drags an image.
2606 function disableImageDrag() {
2607 on(window, 'dragstart', preventDefault);
2608 }
2609
2610 // @function enableImageDrag()
2611 // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection).
2612 function enableImageDrag() {
2613 off(window, 'dragstart', preventDefault);
2614 }
2615
2616 var _outlineElement, _outlineStyle;
2617 // @function preventOutline(el: HTMLElement)
2618 // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline)
2619 // of the element `el` invisible. Used internally by Leaflet to prevent
2620 // focusable elements from displaying an outline when the user performs a
2621 // drag interaction on them.
2622 function preventOutline(element) {
2623 while (element.tabIndex === -1) {
2624 element = element.parentNode;
2625 }
2626 if (!element.style) { return; }
2627 restoreOutline();
2628 _outlineElement = element;
2629 _outlineStyle = element.style.outline;
2630 element.style.outline = 'none';
2631 on(window, 'keydown', restoreOutline);
2632 }
2633
2634 // @function restoreOutline()
2635 // Cancels the effects of a previous [`L.DomUtil.preventOutline`]().
2636 function restoreOutline() {
2637 if (!_outlineElement) { return; }
2638 _outlineElement.style.outline = _outlineStyle;
2639 _outlineElement = undefined;
2640 _outlineStyle = undefined;
2641 off(window, 'keydown', restoreOutline);
2642 }
2643
2644 // @function getSizedParentNode(el: HTMLElement): HTMLElement
2645 // Finds the closest parent node which size (width and height) is not null.
2646 function getSizedParentNode(element) {
2647 do {
2648 element = element.parentNode;
2649 } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body);
2650 return element;
2651 }
2652
2653 // @function getScale(el: HTMLElement): Object
2654 // Computes the CSS scale currently applied on the element.
2655 // Returns an object with `x` and `y` members as horizontal and vertical scales respectively,
2656 // and `boundingClientRect` as the result of [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
2657 function getScale(element) {
2658 var rect = element.getBoundingClientRect(); // Read-only in old browsers.
2659
2660 return {
2661 x: rect.width / element.offsetWidth || 1,
2662 y: rect.height / element.offsetHeight || 1,
2663 boundingClientRect: rect
2664 };
2665 }
2666
2667 var DomUtil = {
2668 __proto__: null,
2669 TRANSFORM: TRANSFORM,
2670 TRANSITION: TRANSITION,
2671 TRANSITION_END: TRANSITION_END,
2672 get: get,
2673 getStyle: getStyle,
2674 create: create$1,
2675 remove: remove,
2676 empty: empty,
2677 toFront: toFront,
2678 toBack: toBack,
2679 hasClass: hasClass,
2680 addClass: addClass,
2681 removeClass: removeClass,
2682 setClass: setClass,
2683 getClass: getClass,
2684 setOpacity: setOpacity,
2685 testProp: testProp,
2686 setTransform: setTransform,
2687 setPosition: setPosition,
2688 getPosition: getPosition,
2689 get disableTextSelection () { return disableTextSelection; },
2690 get enableTextSelection () { return enableTextSelection; },
2691 disableImageDrag: disableImageDrag,
2692 enableImageDrag: enableImageDrag,
2693 preventOutline: preventOutline,
2694 restoreOutline: restoreOutline,
2695 getSizedParentNode: getSizedParentNode,
2696 getScale: getScale
2697 };
2698
2699 /*
2700 * @namespace DomEvent
2701 * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
2702 */
2703
2704 // Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
2705
2706 // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
2707 // Adds a listener function (`fn`) to a particular DOM event type of the
2708 // element `el`. You can optionally specify the context of the listener
2709 // (object the `this` keyword will point to). You can also pass several
2710 // space-separated types (e.g. `'click dblclick'`).
2711
2712 // @alternative
2713 // @function on(el: HTMLElement, eventMap: Object, context?: Object): this
2714 // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
2715 function on(obj, types, fn, context) {
2716
2717 if (types && typeof types === 'object') {
2718 for (var type in types) {
2719 addOne(obj, type, types[type], fn);
2720 }
2721 } else {
2722 types = splitWords(types);
2723
2724 for (var i = 0, len = types.length; i < len; i++) {
2725 addOne(obj, types[i], fn, context);
2726 }
2727 }
2728
2729 return this;
2730 }
2731
2732 var eventsKey = '_leaflet_events';
2733
2734 // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
2735 // Removes a previously added listener function.
2736 // Note that if you passed a custom context to on, you must pass the same
2737 // context to `off` in order to remove the listener.
2738
2739 // @alternative
2740 // @function off(el: HTMLElement, eventMap: Object, context?: Object): this
2741 // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
2742
2743 // @alternative
2744 // @function off(el: HTMLElement, types: String): this
2745 // Removes all previously added listeners of given types.
2746
2747 // @alternative
2748 // @function off(el: HTMLElement): this
2749 // Removes all previously added listeners from given HTMLElement
2750 function off(obj, types, fn, context) {
2751
2752 if (arguments.length === 1) {
2753 batchRemove(obj);
2754 delete obj[eventsKey];
2755
2756 } else if (types && typeof types === 'object') {
2757 for (var type in types) {
2758 removeOne(obj, type, types[type], fn);
2759 }
2760
2761 } else {
2762 types = splitWords(types);
2763
2764 if (arguments.length === 2) {
2765 batchRemove(obj, function (type) {
2766 return indexOf(types, type) !== -1;
2767 });
2768 } else {
2769 for (var i = 0, len = types.length; i < len; i++) {
2770 removeOne(obj, types[i], fn, context);
2771 }
2772 }
2773 }
2774
2775 return this;
2776 }
2777
2778 function batchRemove(obj, filterFn) {
2779 for (var id in obj[eventsKey]) {
2780 var type = id.split(/\d/)[0];
2781 if (!filterFn || filterFn(type)) {
2782 removeOne(obj, type, null, null, id);
2783 }
2784 }
2785 }
2786
2787 var mouseSubst = {
2788 mouseenter: 'mouseover',
2789 mouseleave: 'mouseout',
2790 wheel: !('onwheel' in window) && 'mousewheel'
2791 };
2792
2793 function addOne(obj, type, fn, context) {
2794 var id = type + stamp(fn) + (context ? '_' + stamp(context) : '');
2795
2796 if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
2797
2798 var handler = function (e) {
2799 return fn.call(context || obj, e || window.event);
2800 };
2801
2802 var originalHandler = handler;
2803
2804 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) {
2805 // Needs DomEvent.Pointer.js
2806 handler = addPointerListener(obj, type, handler);
2807
2808 } else if (Browser.touch && (type === 'dblclick')) {
2809 handler = addDoubleTapListener(obj, handler);
2810
2811 } else if ('addEventListener' in obj) {
2812
2813 if (type === 'touchstart' || type === 'touchmove' || type === 'wheel' || type === 'mousewheel') {
2814 obj.addEventListener(mouseSubst[type] || type, handler, Browser.passiveEvents ? {passive: false} : false);
2815
2816 } else if (type === 'mouseenter' || type === 'mouseleave') {
2817 handler = function (e) {
2818 e = e || window.event;
2819 if (isExternalTarget(obj, e)) {
2820 originalHandler(e);
2821 }
2822 };
2823 obj.addEventListener(mouseSubst[type], handler, false);
2824
2825 } else {
2826 obj.addEventListener(type, originalHandler, false);
2827 }
2828
2829 } else {
2830 obj.attachEvent('on' + type, handler);
2831 }
2832
2833 obj[eventsKey] = obj[eventsKey] || {};
2834 obj[eventsKey][id] = handler;
2835 }
2836
2837 function removeOne(obj, type, fn, context, id) {
2838 id = id || type + stamp(fn) + (context ? '_' + stamp(context) : '');
2839 var handler = obj[eventsKey] && obj[eventsKey][id];
2840
2841 if (!handler) { return this; }
2842
2843 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) {
2844 removePointerListener(obj, type, handler);
2845
2846 } else if (Browser.touch && (type === 'dblclick')) {
2847 removeDoubleTapListener(obj, handler);
2848
2849 } else if ('removeEventListener' in obj) {
2850
2851 obj.removeEventListener(mouseSubst[type] || type, handler, false);
2852
2853 } else {
2854 obj.detachEvent('on' + type, handler);
2855 }
2856
2857 obj[eventsKey][id] = null;
2858 }
2859
2860 // @function stopPropagation(ev: DOMEvent): this
2861 // Stop the given event from propagation to parent elements. Used inside the listener functions:
2862 // ```js
2863 // L.DomEvent.on(div, 'click', function (ev) {
2864 // L.DomEvent.stopPropagation(ev);
2865 // });
2866 // ```
2867 function stopPropagation(e) {
2868
2869 if (e.stopPropagation) {
2870 e.stopPropagation();
2871 } else if (e.originalEvent) { // In case of Leaflet event.
2872 e.originalEvent._stopped = true;
2873 } else {
2874 e.cancelBubble = true;
2875 }
2876
2877 return this;
2878 }
2879
2880 // @function disableScrollPropagation(el: HTMLElement): this
2881 // Adds `stopPropagation` to the element's `'wheel'` events (plus browser variants).
2882 function disableScrollPropagation(el) {
2883 addOne(el, 'wheel', stopPropagation);
2884 return this;
2885 }
2886
2887 // @function disableClickPropagation(el: HTMLElement): this
2888 // Adds `stopPropagation` to the element's `'click'`, `'dblclick'`, `'contextmenu'`,
2889 // `'mousedown'` and `'touchstart'` events (plus browser variants).
2890 function disableClickPropagation(el) {
2891 on(el, 'mousedown touchstart dblclick contextmenu', stopPropagation);
2892 el['_leaflet_disable_click'] = true;
2893 return this;
2894 }
2895
2896 // @function preventDefault(ev: DOMEvent): this
2897 // Prevents the default action of the DOM Event `ev` from happening (such as
2898 // following a link in the href of the a element, or doing a POST request
2899 // with page reload when a `<form>` is submitted).
2900 // Use it inside listener functions.
2901 function preventDefault(e) {
2902 if (e.preventDefault) {
2903 e.preventDefault();
2904 } else {
2905 e.returnValue = false;
2906 }
2907 return this;
2908 }
2909
2910 // @function stop(ev: DOMEvent): this
2911 // Does `stopPropagation` and `preventDefault` at the same time.
2912 function stop(e) {
2913 preventDefault(e);
2914 stopPropagation(e);
2915 return this;
2916 }
2917
2918 // @function getPropagationPath(ev: DOMEvent): Array
2919 // Compatibility polyfill for [`Event.composedPath()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath).
2920 // Returns an array containing the `HTMLElement`s that the given DOM event
2921 // should propagate to (if not stopped).
2922 function getPropagationPath(ev) {
2923 if (ev.composedPath) {
2924 return ev.composedPath();
2925 }
2926
2927 var path = [];
2928 var el = ev.target;
2929
2930 while (el) {
2931 path.push(el);
2932 el = el.parentNode;
2933 }
2934 return path;
2935 }
2936
2937
2938 // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
2939 // Gets normalized mouse position from a DOM event relative to the
2940 // `container` (border excluded) or to the whole page if not specified.
2941 function getMousePosition(e, container) {
2942 if (!container) {
2943 return new Point(e.clientX, e.clientY);
2944 }
2945
2946 var scale = getScale(container),
2947 offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
2948
2949 return new Point(
2950 // offset.left/top values are in page scale (like clientX/Y),
2951 // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
2952 (e.clientX - offset.left) / scale.x - container.clientLeft,
2953 (e.clientY - offset.top) / scale.y - container.clientTop
2954 );
2955 }
2956
2957
2958 // except , Safari and
2959 // We need double the scroll pixels (see #7403 and #4538) for all Browsers
2960 // except OSX (Mac) -> 3x, Chrome running on Linux 1x
2961
2962 var wheelPxFactor =
2963 (Browser.linux && Browser.chrome) ? window.devicePixelRatio :
2964 Browser.mac ? window.devicePixelRatio * 3 :
2965 window.devicePixelRatio > 0 ? 2 * window.devicePixelRatio : 1;
2966 // @function getWheelDelta(ev: DOMEvent): Number
2967 // Gets normalized wheel delta from a wheel DOM event, in vertical
2968 // pixels scrolled (negative if scrolling down).
2969 // Events from pointing devices without precise scrolling are mapped to
2970 // a best guess of 60 pixels.
2971 function getWheelDelta(e) {
2972 return (Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
2973 (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels
2974 (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
2975 (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
2976 (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events
2977 e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
2978 (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
2979 e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
2980 0;
2981 }
2982
2983 // check if element really left/entered the event target (for mouseenter/mouseleave)
2984 function isExternalTarget(el, e) {
2985
2986 var related = e.relatedTarget;
2987
2988 if (!related) { return true; }
2989
2990 try {
2991 while (related && (related !== el)) {
2992 related = related.parentNode;
2993 }
2994 } catch (err) {
2995 return false;
2996 }
2997 return (related !== el);
2998 }
2999
3000 var DomEvent = {
3001 __proto__: null,
3002 on: on,
3003 off: off,
3004 stopPropagation: stopPropagation,
3005 disableScrollPropagation: disableScrollPropagation,
3006 disableClickPropagation: disableClickPropagation,
3007 preventDefault: preventDefault,
3008 stop: stop,
3009 getPropagationPath: getPropagationPath,
3010 getMousePosition: getMousePosition,
3011 getWheelDelta: getWheelDelta,
3012 isExternalTarget: isExternalTarget,
3013 addListener: on,
3014 removeListener: off
3015 };
3016
3017 /*
3018 * @class PosAnimation
3019 * @aka L.PosAnimation
3020 * @inherits Evented
3021 * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9.
3022 *
3023 * @example
3024 * ```js
3025 * var myPositionMarker = L.marker([48.864716, 2.294694]).addTo(map);
3026 *
3027 * myPositionMarker.on("click", function() {
3028 * var pos = map.latLngToLayerPoint(myPositionMarker.getLatLng());
3029 * pos.y -= 25;
3030 * var fx = new L.PosAnimation();
3031 *
3032 * fx.once('end',function() {
3033 * pos.y += 25;
3034 * fx.run(myPositionMarker._icon, pos, 0.8);
3035 * });
3036 *
3037 * fx.run(myPositionMarker._icon, pos, 0.3);
3038 * });
3039 *
3040 * ```
3041 *
3042 * @constructor L.PosAnimation()
3043 * Creates a `PosAnimation` object.
3044 *
3045 */
3046
3047 var PosAnimation = Evented.extend({
3048
3049 // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number)
3050 // Run an animation of a given element to a new position, optionally setting
3051 // duration in seconds (`0.25` by default) and easing linearity factor (3rd
3052 // argument of the [cubic bezier curve](https://cubic-bezier.com/#0,0,.5,1),
3053 // `0.5` by default).
3054 run: function (el, newPos, duration, easeLinearity) {
3055 this.stop();
3056
3057 this._el = el;
3058 this._inProgress = true;
3059 this._duration = duration || 0.25;
3060 this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2);
3061
3062 this._startPos = getPosition(el);
3063 this._offset = newPos.subtract(this._startPos);
3064 this._startTime = +new Date();
3065
3066 // @event start: Event
3067 // Fired when the animation starts
3068 this.fire('start');
3069
3070 this._animate();
3071 },
3072
3073 // @method stop()
3074 // Stops the animation (if currently running).
3075 stop: function () {
3076 if (!this._inProgress) { return; }
3077
3078 this._step(true);
3079 this._complete();
3080 },
3081
3082 _animate: function () {
3083 // animation loop
3084 this._animId = requestAnimFrame(this._animate, this);
3085 this._step();
3086 },
3087
3088 _step: function (round) {
3089 var elapsed = (+new Date()) - this._startTime,
3090 duration = this._duration * 1000;
3091
3092 if (elapsed < duration) {
3093 this._runFrame(this._easeOut(elapsed / duration), round);
3094 } else {
3095 this._runFrame(1);
3096 this._complete();
3097 }
3098 },
3099
3100 _runFrame: function (progress, round) {
3101 var pos = this._startPos.add(this._offset.multiplyBy(progress));
3102 if (round) {
3103 pos._round();
3104 }
3105 setPosition(this._el, pos);
3106
3107 // @event step: Event
3108 // Fired continuously during the animation.
3109 this.fire('step');
3110 },
3111
3112 _complete: function () {
3113 cancelAnimFrame(this._animId);
3114
3115 this._inProgress = false;
3116 // @event end: Event
3117 // Fired when the animation ends.
3118 this.fire('end');
3119 },
3120
3121 _easeOut: function (t) {
3122 return 1 - Math.pow(1 - t, this._easeOutPower);
3123 }
3124 });
3125
3126 /*
3127 * @class Map
3128 * @aka L.Map
3129 * @inherits Evented
3130 *
3131 * The central class of the API — it is used to create a map on a page and manipulate it.
3132 *
3133 * @example
3134 *
3135 * ```js
3136 * // initialize the map on the "map" div with a given center and zoom
3137 * var map = L.map('map', {
3138 * center: [51.505, -0.09],
3139 * zoom: 13
3140 * });
3141 * ```
3142 *
3143 */
3144
3145 var Map = Evented.extend({
3146
3147 options: {
3148 // @section Map State Options
3149 // @option crs: CRS = L.CRS.EPSG3857
3150 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not
3151 // sure what it means.
3152 crs: EPSG3857,
3153
3154 // @option center: LatLng = undefined
3155 // Initial geographic center of the map
3156 center: undefined,
3157
3158 // @option zoom: Number = undefined
3159 // Initial map zoom level
3160 zoom: undefined,
3161
3162 // @option minZoom: Number = *
3163 // Minimum zoom level of the map.
3164 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
3165 // the lowest of their `minZoom` options will be used instead.
3166 minZoom: undefined,
3167
3168 // @option maxZoom: Number = *
3169 // Maximum zoom level of the map.
3170 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
3171 // the highest of their `maxZoom` options will be used instead.
3172 maxZoom: undefined,
3173
3174 // @option layers: Layer[] = []
3175 // Array of layers that will be added to the map initially
3176 layers: [],
3177
3178 // @option maxBounds: LatLngBounds = null
3179 // When this option is set, the map restricts the view to the given
3180 // geographical bounds, bouncing the user back if the user tries to pan
3181 // outside the view. To set the restriction dynamically, use
3182 // [`setMaxBounds`](#map-setmaxbounds) method.
3183 maxBounds: undefined,
3184
3185 // @option renderer: Renderer = *
3186 // The default method for drawing vector layers on the map. `L.SVG`
3187 // or `L.Canvas` by default depending on browser support.
3188 renderer: undefined,
3189
3190
3191 // @section Animation Options
3192 // @option zoomAnimation: Boolean = true
3193 // Whether the map zoom animation is enabled. By default it's enabled
3194 // in all browsers that support CSS3 Transitions except Android.
3195 zoomAnimation: true,
3196
3197 // @option zoomAnimationThreshold: Number = 4
3198 // Won't animate zoom if the zoom difference exceeds this value.
3199 zoomAnimationThreshold: 4,
3200
3201 // @option fadeAnimation: Boolean = true
3202 // Whether the tile fade animation is enabled. By default it's enabled
3203 // in all browsers that support CSS3 Transitions except Android.
3204 fadeAnimation: true,
3205
3206 // @option markerZoomAnimation: Boolean = true
3207 // Whether markers animate their zoom with the zoom animation, if disabled
3208 // they will disappear for the length of the animation. By default it's
3209 // enabled in all browsers that support CSS3 Transitions except Android.
3210 markerZoomAnimation: true,
3211
3212 // @option transform3DLimit: Number = 2^23
3213 // Defines the maximum size of a CSS translation transform. The default
3214 // value should not be changed unless a web browser positions layers in
3215 // the wrong place after doing a large `panBy`.
3216 transform3DLimit: 8388608, // Precision limit of a 32-bit float
3217
3218 // @section Interaction Options
3219 // @option zoomSnap: Number = 1
3220 // Forces the map's zoom level to always be a multiple of this, particularly
3221 // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom.
3222 // By default, the zoom level snaps to the nearest integer; lower values
3223 // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0`
3224 // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom.
3225 zoomSnap: 1,
3226
3227 // @option zoomDelta: Number = 1
3228 // Controls how much the map's zoom level will change after a
3229 // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+`
3230 // or `-` on the keyboard, or using the [zoom controls](#control-zoom).
3231 // Values smaller than `1` (e.g. `0.5`) allow for greater granularity.
3232 zoomDelta: 1,
3233
3234 // @option trackResize: Boolean = true
3235 // Whether the map automatically handles browser window resize to update itself.
3236 trackResize: true
3237 },
3238
3239 initialize: function (id, options) { // (HTMLElement or String, Object)
3240 options = setOptions(this, options);
3241
3242 // Make sure to assign internal flags at the beginning,
3243 // to avoid inconsistent state in some edge cases.
3244 this._handlers = [];
3245 this._layers = {};
3246 this._zoomBoundLayers = {};
3247 this._sizeChanged = true;
3248
3249 this._initContainer(id);
3250 this._initLayout();
3251
3252 // hack for https://github.com/Leaflet/Leaflet/issues/1980
3253 this._onResize = bind(this._onResize, this);
3254
3255 this._initEvents();
3256
3257 if (options.maxBounds) {
3258 this.setMaxBounds(options.maxBounds);
3259 }
3260
3261 if (options.zoom !== undefined) {
3262 this._zoom = this._limitZoom(options.zoom);
3263 }
3264
3265 if (options.center && options.zoom !== undefined) {
3266 this.setView(toLatLng(options.center), options.zoom, {reset: true});
3267 }
3268
3269 this.callInitHooks();
3270
3271 // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
3272 this._zoomAnimated = TRANSITION && Browser.any3d && !Browser.mobileOpera &&
3273 this.options.zoomAnimation;
3274
3275 // zoom transitions run with the same duration for all layers, so if one of transitionend events
3276 // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
3277 if (this._zoomAnimated) {
3278 this._createAnimProxy();
3279 on(this._proxy, TRANSITION_END, this._catchTransitionEnd, this);
3280 }
3281
3282 this._addLayers(this.options.layers);
3283 },
3284
3285
3286 // @section Methods for modifying map state
3287
3288 // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
3289 // Sets the view of the map (geographical center and zoom) with the given
3290 // animation options.
3291 setView: function (center, zoom, options) {
3292
3293 zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
3294 center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds);
3295 options = options || {};
3296
3297 this._stop();
3298
3299 if (this._loaded && !options.reset && options !== true) {
3300
3301 if (options.animate !== undefined) {
3302 options.zoom = extend({animate: options.animate}, options.zoom);
3303 options.pan = extend({animate: options.animate, duration: options.duration}, options.pan);
3304 }
3305
3306 // try animating pan or zoom
3307 var moved = (this._zoom !== zoom) ?
3308 this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
3309 this._tryAnimatedPan(center, options.pan);
3310
3311 if (moved) {
3312 // prevent resize handler call, the view will refresh after animation anyway
3313 clearTimeout(this._sizeTimer);
3314 return this;
3315 }
3316 }
3317
3318 // animation didn't start, just reset the map view
3319 this._resetView(center, zoom, options.pan && options.pan.noMoveStart);
3320
3321 return this;
3322 },
3323
3324 // @method setZoom(zoom: Number, options?: Zoom/pan options): this
3325 // Sets the zoom of the map.
3326 setZoom: function (zoom, options) {
3327 if (!this._loaded) {
3328 this._zoom = zoom;
3329 return this;
3330 }
3331 return this.setView(this.getCenter(), zoom, {zoom: options});
3332 },
3333
3334 // @method zoomIn(delta?: Number, options?: Zoom options): this
3335 // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
3336 zoomIn: function (delta, options) {
3337 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
3338 return this.setZoom(this._zoom + delta, options);
3339 },
3340
3341 // @method zoomOut(delta?: Number, options?: Zoom options): this
3342 // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
3343 zoomOut: function (delta, options) {
3344 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
3345 return this.setZoom(this._zoom - delta, options);
3346 },
3347
3348 // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this
3349 // Zooms the map while keeping a specified geographical point on the map
3350 // stationary (e.g. used internally for scroll zoom and double-click zoom).
3351 // @alternative
3352 // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this
3353 // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary.
3354 setZoomAround: function (latlng, zoom, options) {
3355 var scale = this.getZoomScale(zoom),
3356 viewHalf = this.getSize().divideBy(2),
3357 containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng),
3358
3359 centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
3360 newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
3361
3362 return this.setView(newCenter, zoom, {zoom: options});
3363 },
3364
3365 _getBoundsCenterZoom: function (bounds, options) {
3366
3367 options = options || {};
3368 bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds);
3369
3370 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
3371 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
3372
3373 zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
3374
3375 zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom;
3376
3377 if (zoom === Infinity) {
3378 return {
3379 center: bounds.getCenter(),
3380 zoom: zoom
3381 };
3382 }
3383
3384 var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
3385
3386 swPoint = this.project(bounds.getSouthWest(), zoom),
3387 nePoint = this.project(bounds.getNorthEast(), zoom),
3388 center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
3389
3390 return {
3391 center: center,
3392 zoom: zoom
3393 };
3394 },
3395
3396 // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this
3397 // Sets a map view that contains the given geographical bounds with the
3398 // maximum zoom level possible.
3399 fitBounds: function (bounds, options) {
3400
3401 bounds = toLatLngBounds(bounds);
3402
3403 if (!bounds.isValid()) {
3404 throw new Error('Bounds are not valid.');
3405 }
3406
3407 var target = this._getBoundsCenterZoom(bounds, options);
3408 return this.setView(target.center, target.zoom, options);
3409 },
3410
3411 // @method fitWorld(options?: fitBounds options): this
3412 // Sets a map view that mostly contains the whole world with the maximum
3413 // zoom level possible.
3414 fitWorld: function (options) {
3415 return this.fitBounds([[-90, -180], [90, 180]], options);
3416 },
3417
3418 // @method panTo(latlng: LatLng, options?: Pan options): this
3419 // Pans the map to a given center.
3420 panTo: function (center, options) { // (LatLng)
3421 return this.setView(center, this._zoom, {pan: options});
3422 },
3423
3424 // @method panBy(offset: Point, options?: Pan options): this
3425 // Pans the map by a given number of pixels (animated).
3426 panBy: function (offset, options) {
3427 offset = toPoint(offset).round();
3428 options = options || {};
3429
3430 if (!offset.x && !offset.y) {
3431 return this.fire('moveend');
3432 }
3433 // If we pan too far, Chrome gets issues with tiles
3434 // and makes them disappear or appear in the wrong place (slightly offset) #2602
3435 if (options.animate !== true && !this.getSize().contains(offset)) {
3436 this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
3437 return this;
3438 }
3439
3440 if (!this._panAnim) {
3441 this._panAnim = new PosAnimation();
3442
3443 this._panAnim.on({
3444 'step': this._onPanTransitionStep,
3445 'end': this._onPanTransitionEnd
3446 }, this);
3447 }
3448
3449 // don't fire movestart if animating inertia
3450 if (!options.noMoveStart) {
3451 this.fire('movestart');
3452 }
3453
3454 // animate pan unless animate: false specified
3455 if (options.animate !== false) {
3456 addClass(this._mapPane, 'leaflet-pan-anim');
3457
3458 var newPos = this._getMapPanePos().subtract(offset).round();
3459 this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
3460 } else {
3461 this._rawPanBy(offset);
3462 this.fire('move').fire('moveend');
3463 }
3464
3465 return this;
3466 },
3467
3468 // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
3469 // Sets the view of the map (geographical center and zoom) performing a smooth
3470 // pan-zoom animation.
3471 flyTo: function (targetCenter, targetZoom, options) {
3472
3473 options = options || {};
3474 if (options.animate === false || !Browser.any3d) {
3475 return this.setView(targetCenter, targetZoom, options);
3476 }
3477
3478 this._stop();
3479
3480 var from = this.project(this.getCenter()),
3481 to = this.project(targetCenter),
3482 size = this.getSize(),
3483 startZoom = this._zoom;
3484
3485 targetCenter = toLatLng(targetCenter);
3486 targetZoom = targetZoom === undefined ? startZoom : targetZoom;
3487
3488 var w0 = Math.max(size.x, size.y),
3489 w1 = w0 * this.getZoomScale(startZoom, targetZoom),
3490 u1 = (to.distanceTo(from)) || 1,
3491 rho = 1.42,
3492 rho2 = rho * rho;
3493
3494 function r(i) {
3495 var s1 = i ? -1 : 1,
3496 s2 = i ? w1 : w0,
3497 t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
3498 b1 = 2 * s2 * rho2 * u1,
3499 b = t1 / b1,
3500 sq = Math.sqrt(b * b + 1) - b;
3501
3502 // workaround for floating point precision bug when sq = 0, log = -Infinite,
3503 // thus triggering an infinite loop in flyTo
3504 var log = sq < 0.000000001 ? -18 : Math.log(sq);
3505
3506 return log;
3507 }
3508
3509 function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
3510 function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
3511 function tanh(n) { return sinh(n) / cosh(n); }
3512
3513 var r0 = r(0);
3514
3515 function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
3516 function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }
3517
3518 function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }
3519
3520 var start = Date.now(),
3521 S = (r(1) - r0) / rho,
3522 duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
3523
3524 function frame() {
3525 var t = (Date.now() - start) / duration,
3526 s = easeOut(t) * S;
3527
3528 if (t <= 1) {
3529 this._flyToFrame = requestAnimFrame(frame, this);
3530
3531 this._move(
3532 this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
3533 this.getScaleZoom(w0 / w(s), startZoom),
3534 {flyTo: true});
3535
3536 } else {
3537 this
3538 ._move(targetCenter, targetZoom)
3539 ._moveEnd(true);
3540 }
3541 }
3542
3543 this._moveStart(true, options.noMoveStart);
3544
3545 frame.call(this);
3546 return this;
3547 },
3548
3549 // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
3550 // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
3551 // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
3552 flyToBounds: function (bounds, options) {
3553 var target = this._getBoundsCenterZoom(bounds, options);
3554 return this.flyTo(target.center, target.zoom, options);
3555 },
3556
3557 // @method setMaxBounds(bounds: LatLngBounds): this
3558 // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option).
3559 setMaxBounds: function (bounds) {
3560 bounds = toLatLngBounds(bounds);
3561
3562 if (this.listens('moveend', this._panInsideMaxBounds)) {
3563 this.off('moveend', this._panInsideMaxBounds);
3564 }
3565
3566 if (!bounds.isValid()) {
3567 this.options.maxBounds = null;
3568 return this;
3569 }
3570
3571 this.options.maxBounds = bounds;
3572
3573 if (this._loaded) {
3574 this._panInsideMaxBounds();
3575 }
3576
3577 return this.on('moveend', this._panInsideMaxBounds);
3578 },
3579
3580 // @method setMinZoom(zoom: Number): this
3581 // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option).
3582 setMinZoom: function (zoom) {
3583 var oldZoom = this.options.minZoom;
3584 this.options.minZoom = zoom;
3585
3586 if (this._loaded && oldZoom !== zoom) {
3587 this.fire('zoomlevelschange');
3588
3589 if (this.getZoom() < this.options.minZoom) {
3590 return this.setZoom(zoom);
3591 }
3592 }
3593
3594 return this;
3595 },
3596
3597 // @method setMaxZoom(zoom: Number): this
3598 // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option).
3599 setMaxZoom: function (zoom) {
3600 var oldZoom = this.options.maxZoom;
3601 this.options.maxZoom = zoom;
3602
3603 if (this._loaded && oldZoom !== zoom) {
3604 this.fire('zoomlevelschange');
3605
3606 if (this.getZoom() > this.options.maxZoom) {
3607 return this.setZoom(zoom);
3608 }
3609 }
3610
3611 return this;
3612 },
3613
3614 // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this
3615 // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any.
3616 panInsideBounds: function (bounds, options) {
3617 this._enforcingBounds = true;
3618 var center = this.getCenter(),
3619 newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds));
3620
3621 if (!center.equals(newCenter)) {
3622 this.panTo(newCenter, options);
3623 }
3624
3625 this._enforcingBounds = false;
3626 return this;
3627 },
3628
3629 // @method panInside(latlng: LatLng, options?: padding options): this
3630 // Pans the map the minimum amount to make the `latlng` visible. Use
3631 // padding options to fit the display to more restricted bounds.
3632 // If `latlng` is already within the (optionally padded) display bounds,
3633 // the map will not be panned.
3634 panInside: function (latlng, options) {
3635 options = options || {};
3636
3637 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
3638 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
3639 pixelCenter = this.project(this.getCenter()),
3640 pixelPoint = this.project(latlng),
3641 pixelBounds = this.getPixelBounds(),
3642 paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
3643 paddedSize = paddedBounds.getSize();
3644
3645 if (!paddedBounds.contains(pixelPoint)) {
3646 this._enforcingBounds = true;
3647 var centerOffset = pixelPoint.subtract(paddedBounds.getCenter());
3648 var offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize);
3649 pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x;
3650 pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y;
3651 this.panTo(this.unproject(pixelCenter), options);
3652 this._enforcingBounds = false;
3653 }
3654 return this;
3655 },
3656
3657 // @method invalidateSize(options: Zoom/pan options): this
3658 // Checks if the map container size changed and updates the map if so —
3659 // call it after you've changed the map size dynamically, also animating
3660 // pan by default. If `options.pan` is `false`, panning will not occur.
3661 // If `options.debounceMoveend` is `true`, it will delay `moveend` event so
3662 // that it doesn't happen often even if the method is called many
3663 // times in a row.
3664
3665 // @alternative
3666 // @method invalidateSize(animate: Boolean): this
3667 // Checks if the map container size changed and updates the map if so —
3668 // call it after you've changed the map size dynamically, also animating
3669 // pan by default.
3670 invalidateSize: function (options) {
3671 if (!this._loaded) { return this; }
3672
3673 options = extend({
3674 animate: false,
3675 pan: true
3676 }, options === true ? {animate: true} : options);
3677
3678 var oldSize = this.getSize();
3679 this._sizeChanged = true;
3680 this._lastCenter = null;
3681
3682 var newSize = this.getSize(),
3683 oldCenter = oldSize.divideBy(2).round(),
3684 newCenter = newSize.divideBy(2).round(),
3685 offset = oldCenter.subtract(newCenter);
3686
3687 if (!offset.x && !offset.y) { return this; }
3688
3689 if (options.animate && options.pan) {
3690 this.panBy(offset);
3691
3692 } else {
3693 if (options.pan) {
3694 this._rawPanBy(offset);
3695 }
3696
3697 this.fire('move');
3698
3699 if (options.debounceMoveend) {
3700 clearTimeout(this._sizeTimer);
3701 this._sizeTimer = setTimeout(bind(this.fire, this, 'moveend'), 200);
3702 } else {
3703 this.fire('moveend');
3704 }
3705 }
3706
3707 // @section Map state change events
3708 // @event resize: ResizeEvent
3709 // Fired when the map is resized.
3710 return this.fire('resize', {
3711 oldSize: oldSize,
3712 newSize: newSize
3713 });
3714 },
3715
3716 // @section Methods for modifying map state
3717 // @method stop(): this
3718 // Stops the currently running `panTo` or `flyTo` animation, if any.
3719 stop: function () {
3720 this.setZoom(this._limitZoom(this._zoom));
3721 if (!this.options.zoomSnap) {
3722 this.fire('viewreset');
3723 }
3724 return this._stop();
3725 },
3726
3727 // @section Geolocation methods
3728 // @method locate(options?: Locate options): this
3729 // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
3730 // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
3731 // and optionally sets the map view to the user's location with respect to
3732 // detection accuracy (or to the world view if geolocation failed).
3733 // Note that, if your page doesn't use HTTPS, this method will fail in
3734 // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
3735 // See `Locate options` for more details.
3736 locate: function (options) {
3737
3738 options = this._locateOptions = extend({
3739 timeout: 10000,
3740 watch: false
3741 // setView: false
3742 // maxZoom: <Number>
3743 // maximumAge: 0
3744 // enableHighAccuracy: false
3745 }, options);
3746
3747 if (!('geolocation' in navigator)) {
3748 this._handleGeolocationError({
3749 code: 0,
3750 message: 'Geolocation not supported.'
3751 });
3752 return this;
3753 }
3754
3755 var onResponse = bind(this._handleGeolocationResponse, this),
3756 onError = bind(this._handleGeolocationError, this);
3757
3758 if (options.watch) {
3759 this._locationWatchId =
3760 navigator.geolocation.watchPosition(onResponse, onError, options);
3761 } else {
3762 navigator.geolocation.getCurrentPosition(onResponse, onError, options);
3763 }
3764 return this;
3765 },
3766
3767 // @method stopLocate(): this
3768 // Stops watching location previously initiated by `map.locate({watch: true})`
3769 // and aborts resetting the map view if map.locate was called with
3770 // `{setView: true}`.
3771 stopLocate: function () {
3772 if (navigator.geolocation && navigator.geolocation.clearWatch) {
3773 navigator.geolocation.clearWatch(this._locationWatchId);
3774 }
3775 if (this._locateOptions) {
3776 this._locateOptions.setView = false;
3777 }
3778 return this;
3779 },
3780
3781 _handleGeolocationError: function (error) {
3782 if (!this._container._leaflet_id) { return; }
3783
3784 var c = error.code,
3785 message = error.message ||
3786 (c === 1 ? 'permission denied' :
3787 (c === 2 ? 'position unavailable' : 'timeout'));
3788
3789 if (this._locateOptions.setView && !this._loaded) {
3790 this.fitWorld();
3791 }
3792
3793 // @section Location events
3794 // @event locationerror: ErrorEvent
3795 // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
3796 this.fire('locationerror', {
3797 code: c,
3798 message: 'Geolocation error: ' + message + '.'
3799 });
3800 },
3801
3802 _handleGeolocationResponse: function (pos) {
3803 if (!this._container._leaflet_id) { return; }
3804
3805 var lat = pos.coords.latitude,
3806 lng = pos.coords.longitude,
3807 latlng = new LatLng(lat, lng),
3808 bounds = latlng.toBounds(pos.coords.accuracy * 2),
3809 options = this._locateOptions;
3810
3811 if (options.setView) {
3812 var zoom = this.getBoundsZoom(bounds);
3813 this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
3814 }
3815
3816 var data = {
3817 latlng: latlng,
3818 bounds: bounds,
3819 timestamp: pos.timestamp
3820 };
3821
3822 for (var i in pos.coords) {
3823 if (typeof pos.coords[i] === 'number') {
3824 data[i] = pos.coords[i];
3825 }
3826 }
3827
3828 // @event locationfound: LocationEvent
3829 // Fired when geolocation (using the [`locate`](#map-locate) method)
3830 // went successfully.
3831 this.fire('locationfound', data);
3832 },
3833
3834 // TODO Appropriate docs section?
3835 // @section Other Methods
3836 // @method addHandler(name: String, HandlerClass: Function): this
3837 // Adds a new `Handler` to the map, given its name and constructor function.
3838 addHandler: function (name, HandlerClass) {
3839 if (!HandlerClass) { return this; }
3840
3841 var handler = this[name] = new HandlerClass(this);
3842
3843 this._handlers.push(handler);
3844
3845 if (this.options[name]) {
3846 handler.enable();
3847 }
3848
3849 return this;
3850 },
3851
3852 // @method remove(): this
3853 // Destroys the map and clears all related event listeners.
3854 remove: function () {
3855
3856 this._initEvents(true);
3857 if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); }
3858
3859 if (this._containerId !== this._container._leaflet_id) {
3860 throw new Error('Map container is being reused by another instance');
3861 }
3862
3863 try {
3864 // throws error in IE6-8
3865 delete this._container._leaflet_id;
3866 delete this._containerId;
3867 } catch (e) {
3868 /*eslint-disable */
3869 this._container._leaflet_id = undefined;
3870 /* eslint-enable */
3871 this._containerId = undefined;
3872 }
3873
3874 if (this._locationWatchId !== undefined) {
3875 this.stopLocate();
3876 }
3877
3878 this._stop();
3879
3880 remove(this._mapPane);
3881
3882 if (this._clearControlPos) {
3883 this._clearControlPos();
3884 }
3885 if (this._resizeRequest) {
3886 cancelAnimFrame(this._resizeRequest);
3887 this._resizeRequest = null;
3888 }
3889
3890 this._clearHandlers();
3891
3892 if (this._loaded) {
3893 // @section Map state change events
3894 // @event unload: Event
3895 // Fired when the map is destroyed with [remove](#map-remove) method.
3896 this.fire('unload');
3897 }
3898
3899 var i;
3900 for (i in this._layers) {
3901 this._layers[i].remove();
3902 }
3903 for (i in this._panes) {
3904 remove(this._panes[i]);
3905 }
3906
3907 this._layers = [];
3908 this._panes = [];
3909 delete this._mapPane;
3910 delete this._renderer;
3911
3912 return this;
3913 },
3914
3915 // @section Other Methods
3916 // @method createPane(name: String, container?: HTMLElement): HTMLElement
3917 // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already,
3918 // then returns it. The pane is created as a child of `container`, or
3919 // as a child of the main map pane if not set.
3920 createPane: function (name, container) {
3921 var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''),
3922 pane = create$1('div', className, container || this._mapPane);
3923
3924 if (name) {
3925 this._panes[name] = pane;
3926 }
3927 return pane;
3928 },
3929
3930 // @section Methods for Getting Map State
3931
3932 // @method getCenter(): LatLng
3933 // Returns the geographical center of the map view
3934 getCenter: function () {
3935 this._checkIfLoaded();
3936
3937 if (this._lastCenter && !this._moved()) {
3938 return this._lastCenter.clone();
3939 }
3940 return this.layerPointToLatLng(this._getCenterLayerPoint());
3941 },
3942
3943 // @method getZoom(): Number
3944 // Returns the current zoom level of the map view
3945 getZoom: function () {
3946 return this._zoom;
3947 },
3948
3949 // @method getBounds(): LatLngBounds
3950 // Returns the geographical bounds visible in the current map view
3951 getBounds: function () {
3952 var bounds = this.getPixelBounds(),
3953 sw = this.unproject(bounds.getBottomLeft()),
3954 ne = this.unproject(bounds.getTopRight());
3955
3956 return new LatLngBounds(sw, ne);
3957 },
3958
3959 // @method getMinZoom(): Number
3960 // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default.
3961 getMinZoom: function () {
3962 return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom;
3963 },
3964
3965 // @method getMaxZoom(): Number
3966 // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers).
3967 getMaxZoom: function () {
3968 return this.options.maxZoom === undefined ?
3969 (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
3970 this.options.maxZoom;
3971 },
3972
3973 // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean, padding?: Point): Number
3974 // Returns the maximum zoom level on which the given bounds fit to the map
3975 // view in its entirety. If `inside` (optional) is set to `true`, the method
3976 // instead returns the minimum zoom level on which the map view fits into
3977 // the given bounds in its entirety.
3978 getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
3979 bounds = toLatLngBounds(bounds);
3980 padding = toPoint(padding || [0, 0]);
3981
3982 var zoom = this.getZoom() || 0,
3983 min = this.getMinZoom(),
3984 max = this.getMaxZoom(),
3985 nw = bounds.getNorthWest(),
3986 se = bounds.getSouthEast(),
3987 size = this.getSize().subtract(padding),
3988 boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
3989 snap = Browser.any3d ? this.options.zoomSnap : 1,
3990 scalex = size.x / boundsSize.x,
3991 scaley = size.y / boundsSize.y,
3992 scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);
3993
3994 zoom = this.getScaleZoom(scale, zoom);
3995
3996 if (snap) {
3997 zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
3998 zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
3999 }
4000
4001 return Math.max(min, Math.min(max, zoom));
4002 },
4003
4004 // @method getSize(): Point
4005 // Returns the current size of the map container (in pixels).
4006 getSize: function () {
4007 if (!this._size || this._sizeChanged) {
4008 this._size = new Point(
4009 this._container.clientWidth || 0,
4010 this._container.clientHeight || 0);
4011
4012 this._sizeChanged = false;
4013 }
4014 return this._size.clone();
4015 },
4016
4017 // @method getPixelBounds(): Bounds
4018 // Returns the bounds of the current map view in projected pixel
4019 // coordinates (sometimes useful in layer and overlay implementations).
4020 getPixelBounds: function (center, zoom) {
4021 var topLeftPoint = this._getTopLeftPoint(center, zoom);
4022 return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
4023 },
4024
4025 // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to
4026 // the map pane? "left point of the map layer" can be confusing, specially
4027 // since there can be negative offsets.
4028 // @method getPixelOrigin(): Point
4029 // Returns the projected pixel coordinates of the top left point of
4030 // the map layer (useful in custom layer and overlay implementations).
4031 getPixelOrigin: function () {
4032 this._checkIfLoaded();
4033 return this._pixelOrigin;
4034 },
4035
4036 // @method getPixelWorldBounds(zoom?: Number): Bounds
4037 // Returns the world's bounds in pixel coordinates for zoom level `zoom`.
4038 // If `zoom` is omitted, the map's current zoom level is used.
4039 getPixelWorldBounds: function (zoom) {
4040 return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom);
4041 },
4042
4043 // @section Other Methods
4044
4045 // @method getPane(pane: String|HTMLElement): HTMLElement
4046 // Returns a [map pane](#map-pane), given its name or its HTML element (its identity).
4047 getPane: function (pane) {
4048 return typeof pane === 'string' ? this._panes[pane] : pane;
4049 },
4050
4051 // @method getPanes(): Object
4052 // Returns a plain object containing the names of all [panes](#map-pane) as keys and
4053 // the panes as values.
4054 getPanes: function () {
4055 return this._panes;
4056 },
4057
4058 // @method getContainer: HTMLElement
4059 // Returns the HTML element that contains the map.
4060 getContainer: function () {
4061 return this._container;
4062 },
4063
4064
4065 // @section Conversion Methods
4066
4067 // @method getZoomScale(toZoom: Number, fromZoom: Number): Number
4068 // Returns the scale factor to be applied to a map transition from zoom level
4069 // `fromZoom` to `toZoom`. Used internally to help with zoom animations.
4070 getZoomScale: function (toZoom, fromZoom) {
4071 // TODO replace with universal implementation after refactoring projections
4072 var crs = this.options.crs;
4073 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
4074 return crs.scale(toZoom) / crs.scale(fromZoom);
4075 },
4076
4077 // @method getScaleZoom(scale: Number, fromZoom: Number): Number
4078 // Returns the zoom level that the map would end up at, if it is at `fromZoom`
4079 // level and everything is scaled by a factor of `scale`. Inverse of
4080 // [`getZoomScale`](#map-getZoomScale).
4081 getScaleZoom: function (scale, fromZoom) {
4082 var crs = this.options.crs;
4083 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
4084 var zoom = crs.zoom(scale * crs.scale(fromZoom));
4085 return isNaN(zoom) ? Infinity : zoom;
4086 },
4087
4088 // @method project(latlng: LatLng, zoom: Number): Point
4089 // Projects a geographical coordinate `LatLng` according to the projection
4090 // of the map's CRS, then scales it according to `zoom` and the CRS's
4091 // `Transformation`. The result is pixel coordinate relative to
4092 // the CRS origin.
4093 project: function (latlng, zoom) {
4094 zoom = zoom === undefined ? this._zoom : zoom;
4095 return this.options.crs.latLngToPoint(toLatLng(latlng), zoom);
4096 },
4097
4098 // @method unproject(point: Point, zoom: Number): LatLng
4099 // Inverse of [`project`](#map-project).
4100 unproject: function (point, zoom) {
4101 zoom = zoom === undefined ? this._zoom : zoom;
4102 return this.options.crs.pointToLatLng(toPoint(point), zoom);
4103 },
4104
4105 // @method layerPointToLatLng(point: Point): LatLng
4106 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
4107 // returns the corresponding geographical coordinate (for the current zoom level).
4108 layerPointToLatLng: function (point) {
4109 var projectedPoint = toPoint(point).add(this.getPixelOrigin());
4110 return this.unproject(projectedPoint);
4111 },
4112
4113 // @method latLngToLayerPoint(latlng: LatLng): Point
4114 // Given a geographical coordinate, returns the corresponding pixel coordinate
4115 // relative to the [origin pixel](#map-getpixelorigin).
4116 latLngToLayerPoint: function (latlng) {
4117 var projectedPoint = this.project(toLatLng(latlng))._round();
4118 return projectedPoint._subtract(this.getPixelOrigin());
4119 },
4120
4121 // @method wrapLatLng(latlng: LatLng): LatLng
4122 // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the
4123 // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the
4124 // CRS's bounds.
4125 // By default this means longitude is wrapped around the dateline so its
4126 // value is between -180 and +180 degrees.
4127 wrapLatLng: function (latlng) {
4128 return this.options.crs.wrapLatLng(toLatLng(latlng));
4129 },
4130
4131 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
4132 // Returns a `LatLngBounds` with the same size as the given one, ensuring that
4133 // its center is within the CRS's bounds.
4134 // By default this means the center longitude is wrapped around the dateline so its
4135 // value is between -180 and +180 degrees, and the majority of the bounds
4136 // overlaps the CRS's bounds.
4137 wrapLatLngBounds: function (latlng) {
4138 return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng));
4139 },
4140
4141 // @method distance(latlng1: LatLng, latlng2: LatLng): Number
4142 // Returns the distance between two geographical coordinates according to
4143 // the map's CRS. By default this measures distance in meters.
4144 distance: function (latlng1, latlng2) {
4145 return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2));
4146 },
4147
4148 // @method containerPointToLayerPoint(point: Point): Point
4149 // Given a pixel coordinate relative to the map container, returns the corresponding
4150 // pixel coordinate relative to the [origin pixel](#map-getpixelorigin).
4151 containerPointToLayerPoint: function (point) { // (Point)
4152 return toPoint(point).subtract(this._getMapPanePos());
4153 },
4154
4155 // @method layerPointToContainerPoint(point: Point): Point
4156 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
4157 // returns the corresponding pixel coordinate relative to the map container.
4158 layerPointToContainerPoint: function (point) { // (Point)
4159 return toPoint(point).add(this._getMapPanePos());
4160 },
4161
4162 // @method containerPointToLatLng(point: Point): LatLng
4163 // Given a pixel coordinate relative to the map container, returns
4164 // the corresponding geographical coordinate (for the current zoom level).
4165 containerPointToLatLng: function (point) {
4166 var layerPoint = this.containerPointToLayerPoint(toPoint(point));
4167 return this.layerPointToLatLng(layerPoint);
4168 },
4169
4170 // @method latLngToContainerPoint(latlng: LatLng): Point
4171 // Given a geographical coordinate, returns the corresponding pixel coordinate
4172 // relative to the map container.
4173 latLngToContainerPoint: function (latlng) {
4174 return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
4175 },
4176
4177 // @method mouseEventToContainerPoint(ev: MouseEvent): Point
4178 // Given a MouseEvent object, returns the pixel coordinate relative to the
4179 // map container where the event took place.
4180 mouseEventToContainerPoint: function (e) {
4181 return getMousePosition(e, this._container);
4182 },
4183
4184 // @method mouseEventToLayerPoint(ev: MouseEvent): Point
4185 // Given a MouseEvent object, returns the pixel coordinate relative to
4186 // the [origin pixel](#map-getpixelorigin) where the event took place.
4187 mouseEventToLayerPoint: function (e) {
4188 return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
4189 },
4190
4191 // @method mouseEventToLatLng(ev: MouseEvent): LatLng
4192 // Given a MouseEvent object, returns geographical coordinate where the
4193 // event took place.
4194 mouseEventToLatLng: function (e) { // (MouseEvent)
4195 return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
4196 },
4197
4198
4199 // map initialization methods
4200
4201 _initContainer: function (id) {
4202 var container = this._container = get(id);
4203
4204 if (!container) {
4205 throw new Error('Map container not found.');
4206 } else if (container._leaflet_id) {
4207 throw new Error('Map container is already initialized.');
4208 }
4209
4210 on(container, 'scroll', this._onScroll, this);
4211 this._containerId = stamp(container);
4212 },
4213
4214 _initLayout: function () {
4215 var container = this._container;
4216
4217 this._fadeAnimated = this.options.fadeAnimation && Browser.any3d;
4218
4219 addClass(container, 'leaflet-container' +
4220 (Browser.touch ? ' leaflet-touch' : '') +
4221 (Browser.retina ? ' leaflet-retina' : '') +
4222 (Browser.ielt9 ? ' leaflet-oldie' : '') +
4223 (Browser.safari ? ' leaflet-safari' : '') +
4224 (this._fadeAnimated ? ' leaflet-fade-anim' : ''));
4225
4226 var position = getStyle(container, 'position');
4227
4228 if (position !== 'absolute' && position !== 'relative' && position !== 'fixed' && position !== 'sticky') {
4229 container.style.position = 'relative';
4230 }
4231
4232 this._initPanes();
4233
4234 if (this._initControlPos) {
4235 this._initControlPos();
4236 }
4237 },
4238
4239 _initPanes: function () {
4240 var panes = this._panes = {};
4241 this._paneRenderers = {};
4242
4243 // @section
4244 //
4245 // Panes are DOM elements used to control the ordering of layers on the map. You
4246 // can access panes with [`map.getPane`](#map-getpane) or
4247 // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the
4248 // [`map.createPane`](#map-createpane) method.
4249 //
4250 // Every map has the following default panes that differ only in zIndex.
4251 //
4252 // @pane mapPane: HTMLElement = 'auto'
4253 // Pane that contains all other map panes
4254
4255 this._mapPane = this.createPane('mapPane', this._container);
4256 setPosition(this._mapPane, new Point(0, 0));
4257
4258 // @pane tilePane: HTMLElement = 200
4259 // Pane for `GridLayer`s and `TileLayer`s
4260 this.createPane('tilePane');
4261 // @pane overlayPane: HTMLElement = 400
4262 // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s
4263 this.createPane('overlayPane');
4264 // @pane shadowPane: HTMLElement = 500
4265 // Pane for overlay shadows (e.g. `Marker` shadows)
4266 this.createPane('shadowPane');
4267 // @pane markerPane: HTMLElement = 600
4268 // Pane for `Icon`s of `Marker`s
4269 this.createPane('markerPane');
4270 // @pane tooltipPane: HTMLElement = 650
4271 // Pane for `Tooltip`s.
4272 this.createPane('tooltipPane');
4273 // @pane popupPane: HTMLElement = 700
4274 // Pane for `Popup`s.
4275 this.createPane('popupPane');
4276
4277 if (!this.options.markerZoomAnimation) {
4278 addClass(panes.markerPane, 'leaflet-zoom-hide');
4279 addClass(panes.shadowPane, 'leaflet-zoom-hide');
4280 }
4281 },
4282
4283
4284 // private methods that modify map state
4285
4286 // @section Map state change events
4287 _resetView: function (center, zoom, noMoveStart) {
4288 setPosition(this._mapPane, new Point(0, 0));
4289
4290 var loading = !this._loaded;
4291 this._loaded = true;
4292 zoom = this._limitZoom(zoom);
4293
4294 this.fire('viewprereset');
4295
4296 var zoomChanged = this._zoom !== zoom;
4297 this
4298 ._moveStart(zoomChanged, noMoveStart)
4299 ._move(center, zoom)
4300 ._moveEnd(zoomChanged);
4301
4302 // @event viewreset: Event
4303 // Fired when the map needs to redraw its content (this usually happens
4304 // on map zoom or load). Very useful for creating custom overlays.
4305 this.fire('viewreset');
4306
4307 // @event load: Event
4308 // Fired when the map is initialized (when its center and zoom are set
4309 // for the first time).
4310 if (loading) {
4311 this.fire('load');
4312 }
4313 },
4314
4315 _moveStart: function (zoomChanged, noMoveStart) {
4316 // @event zoomstart: Event
4317 // Fired when the map zoom is about to change (e.g. before zoom animation).
4318 // @event movestart: Event
4319 // Fired when the view of the map starts changing (e.g. user starts dragging the map).
4320 if (zoomChanged) {
4321 this.fire('zoomstart');
4322 }
4323 if (!noMoveStart) {
4324 this.fire('movestart');
4325 }
4326 return this;
4327 },
4328
4329 _move: function (center, zoom, data, supressEvent) {
4330 if (zoom === undefined) {
4331 zoom = this._zoom;
4332 }
4333 var zoomChanged = this._zoom !== zoom;
4334
4335 this._zoom = zoom;
4336 this._lastCenter = center;
4337 this._pixelOrigin = this._getNewPixelOrigin(center);
4338
4339 if (!supressEvent) {
4340 // @event zoom: Event
4341 // Fired repeatedly during any change in zoom level,
4342 // including zoom and fly animations.
4343 if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530
4344 this.fire('zoom', data);
4345 }
4346
4347 // @event move: Event
4348 // Fired repeatedly during any movement of the map,
4349 // including pan and fly animations.
4350 this.fire('move', data);
4351 } else if (data && data.pinch) { // Always fire 'zoom' if pinching because #3530
4352 this.fire('zoom', data);
4353 }
4354 return this;
4355 },
4356
4357 _moveEnd: function (zoomChanged) {
4358 // @event zoomend: Event
4359 // Fired when the map zoom changed, after any animations.
4360 if (zoomChanged) {
4361 this.fire('zoomend');
4362 }
4363
4364 // @event moveend: Event
4365 // Fired when the center of the map stops changing
4366 // (e.g. user stopped dragging the map or after non-centered zoom).
4367 return this.fire('moveend');
4368 },
4369
4370 _stop: function () {
4371 cancelAnimFrame(this._flyToFrame);
4372 if (this._panAnim) {
4373 this._panAnim.stop();
4374 }
4375 return this;
4376 },
4377
4378 _rawPanBy: function (offset) {
4379 setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
4380 },
4381
4382 _getZoomSpan: function () {
4383 return this.getMaxZoom() - this.getMinZoom();
4384 },
4385
4386 _panInsideMaxBounds: function () {
4387 if (!this._enforcingBounds) {
4388 this.panInsideBounds(this.options.maxBounds);
4389 }
4390 },
4391
4392 _checkIfLoaded: function () {
4393 if (!this._loaded) {
4394 throw new Error('Set map center and zoom first.');
4395 }
4396 },
4397
4398 // DOM event handling
4399
4400 // @section Interaction events
4401 _initEvents: function (remove) {
4402 this._targets = {};
4403 this._targets[stamp(this._container)] = this;
4404
4405 var onOff = remove ? off : on;
4406
4407 // @event click: MouseEvent
4408 // Fired when the user clicks (or taps) the map.
4409 // @event dblclick: MouseEvent
4410 // Fired when the user double-clicks (or double-taps) the map.
4411 // @event mousedown: MouseEvent
4412 // Fired when the user pushes the mouse button on the map.
4413 // @event mouseup: MouseEvent
4414 // Fired when the user releases the mouse button on the map.
4415 // @event mouseover: MouseEvent
4416 // Fired when the mouse enters the map.
4417 // @event mouseout: MouseEvent
4418 // Fired when the mouse leaves the map.
4419 // @event mousemove: MouseEvent
4420 // Fired while the mouse moves over the map.
4421 // @event contextmenu: MouseEvent
4422 // Fired when the user pushes the right mouse button on the map, prevents
4423 // default browser context menu from showing if there are listeners on
4424 // this event. Also fired on mobile when the user holds a single touch
4425 // for a second (also called long press).
4426 // @event keypress: KeyboardEvent
4427 // Fired when the user presses a key from the keyboard that produces a character value while the map is focused.
4428 // @event keydown: KeyboardEvent
4429 // Fired when the user presses a key from the keyboard while the map is focused. Unlike the `keypress` event,
4430 // the `keydown` event is fired for keys that produce a character value and for keys
4431 // that do not produce a character value.
4432 // @event keyup: KeyboardEvent
4433 // Fired when the user releases a key from the keyboard while the map is focused.
4434 onOff(this._container, 'click dblclick mousedown mouseup ' +
4435 'mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this);
4436
4437 if (this.options.trackResize) {
4438 onOff(window, 'resize', this._onResize, this);
4439 }
4440
4441 if (Browser.any3d && this.options.transform3DLimit) {
4442 (remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
4443 }
4444 },
4445
4446 _onResize: function () {
4447 cancelAnimFrame(this._resizeRequest);
4448 this._resizeRequest = requestAnimFrame(
4449 function () { this.invalidateSize({debounceMoveend: true}); }, this);
4450 },
4451
4452 _onScroll: function () {
4453 this._container.scrollTop = 0;
4454 this._container.scrollLeft = 0;
4455 },
4456
4457 _onMoveEnd: function () {
4458 var pos = this._getMapPanePos();
4459 if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
4460 // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
4461 // a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/
4462 this._resetView(this.getCenter(), this.getZoom());
4463 }
4464 },
4465
4466 _findEventTargets: function (e, type) {
4467 var targets = [],
4468 target,
4469 isHover = type === 'mouseout' || type === 'mouseover',
4470 src = e.target || e.srcElement,
4471 dragging = false;
4472
4473 while (src) {
4474 target = this._targets[stamp(src)];
4475 if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) {
4476 // Prevent firing click after you just dragged an object.
4477 dragging = true;
4478 break;
4479 }
4480 if (target && target.listens(type, true)) {
4481 if (isHover && !isExternalTarget(src, e)) { break; }
4482 targets.push(target);
4483 if (isHover) { break; }
4484 }
4485 if (src === this._container) { break; }
4486 src = src.parentNode;
4487 }
4488 if (!targets.length && !dragging && !isHover && this.listens(type, true)) {
4489 targets = [this];
4490 }
4491 return targets;
4492 },
4493
4494 _isClickDisabled: function (el) {
4495 while (el && el !== this._container) {
4496 if (el['_leaflet_disable_click']) { return true; }
4497 el = el.parentNode;
4498 }
4499 },
4500
4501 _handleDOMEvent: function (e) {
4502 var el = (e.target || e.srcElement);
4503 if (!this._loaded || el['_leaflet_disable_events'] || e.type === 'click' && this._isClickDisabled(el)) {
4504 return;
4505 }
4506
4507 var type = e.type;
4508
4509 if (type === 'mousedown') {
4510 // prevents outline when clicking on keyboard-focusable element
4511 preventOutline(el);
4512 }
4513
4514 this._fireDOMEvent(e, type);
4515 },
4516
4517 _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],
4518
4519 _fireDOMEvent: function (e, type, canvasTargets) {
4520
4521 if (e.type === 'click') {
4522 // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
4523 // @event preclick: MouseEvent
4524 // Fired before mouse click on the map (sometimes useful when you
4525 // want something to happen on click before any existing click
4526 // handlers start running).
4527 var synth = extend({}, e);
4528 synth.type = 'preclick';
4529 this._fireDOMEvent(synth, synth.type, canvasTargets);
4530 }
4531
4532 // Find the layer the event is propagating from and its parents.
4533 var targets = this._findEventTargets(e, type);
4534
4535 if (canvasTargets) {
4536 var filtered = []; // pick only targets with listeners
4537 for (var i = 0; i < canvasTargets.length; i++) {
4538 if (canvasTargets[i].listens(type, true)) {
4539 filtered.push(canvasTargets[i]);
4540 }
4541 }
4542 targets = filtered.concat(targets);
4543 }
4544
4545 if (!targets.length) { return; }
4546
4547 if (type === 'contextmenu') {
4548 preventDefault(e);
4549 }
4550
4551 var target = targets[0];
4552 var data = {
4553 originalEvent: e
4554 };
4555
4556 if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') {
4557 var isMarker = target.getLatLng && (!target._radius || target._radius <= 10);
4558 data.containerPoint = isMarker ?
4559 this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
4560 data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
4561 data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
4562 }
4563
4564 for (i = 0; i < targets.length; i++) {
4565 targets[i].fire(type, data, true);
4566 if (data.originalEvent._stopped ||
4567 (targets[i].options.bubblingMouseEvents === false && indexOf(this._mouseEvents, type) !== -1)) { return; }
4568 }
4569 },
4570
4571 _draggableMoved: function (obj) {
4572 obj = obj.dragging && obj.dragging.enabled() ? obj : this;
4573 return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
4574 },
4575
4576 _clearHandlers: function () {
4577 for (var i = 0, len = this._handlers.length; i < len; i++) {
4578 this._handlers[i].disable();
4579 }
4580 },
4581
4582 // @section Other Methods
4583
4584 // @method whenReady(fn: Function, context?: Object): this
4585 // Runs the given function `fn` when the map gets initialized with
4586 // a view (center and zoom) and at least one layer, or immediately
4587 // if it's already initialized, optionally passing a function context.
4588 whenReady: function (callback, context) {
4589 if (this._loaded) {
4590 callback.call(context || this, {target: this});
4591 } else {
4592 this.on('load', callback, context);
4593 }
4594 return this;
4595 },
4596
4597
4598 // private methods for getting map state
4599
4600 _getMapPanePos: function () {
4601 return getPosition(this._mapPane) || new Point(0, 0);
4602 },
4603
4604 _moved: function () {
4605 var pos = this._getMapPanePos();
4606 return pos && !pos.equals([0, 0]);
4607 },
4608
4609 _getTopLeftPoint: function (center, zoom) {
4610 var pixelOrigin = center && zoom !== undefined ?
4611 this._getNewPixelOrigin(center, zoom) :
4612 this.getPixelOrigin();
4613 return pixelOrigin.subtract(this._getMapPanePos());
4614 },
4615
4616 _getNewPixelOrigin: function (center, zoom) {
4617 var viewHalf = this.getSize()._divideBy(2);
4618 return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round();
4619 },
4620
4621 _latLngToNewLayerPoint: function (latlng, zoom, center) {
4622 var topLeft = this._getNewPixelOrigin(center, zoom);
4623 return this.project(latlng, zoom)._subtract(topLeft);
4624 },
4625
4626 _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
4627 var topLeft = this._getNewPixelOrigin(center, zoom);
4628 return toBounds([
4629 this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
4630 this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
4631 this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
4632 this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)
4633 ]);
4634 },
4635
4636 // layer point of the current center
4637 _getCenterLayerPoint: function () {
4638 return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
4639 },
4640
4641 // offset of the specified place to the current center in pixels
4642 _getCenterOffset: function (latlng) {
4643 return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
4644 },
4645
4646 // adjust center for view to get inside bounds
4647 _limitCenter: function (center, zoom, bounds) {
4648
4649 if (!bounds) { return center; }
4650
4651 var centerPoint = this.project(center, zoom),
4652 viewHalf = this.getSize().divideBy(2),
4653 viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
4654 offset = this._getBoundsOffset(viewBounds, bounds, zoom);
4655
4656 // If offset is less than a pixel, ignore.
4657 // This prevents unstable projections from getting into
4658 // an infinite loop of tiny offsets.
4659 if (Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) {
4660 return center;
4661 }
4662
4663 return this.unproject(centerPoint.add(offset), zoom);
4664 },
4665
4666 // adjust offset for view to get inside bounds
4667 _limitOffset: function (offset, bounds) {
4668 if (!bounds) { return offset; }
4669
4670 var viewBounds = this.getPixelBounds(),
4671 newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
4672
4673 return offset.add(this._getBoundsOffset(newBounds, bounds));
4674 },
4675
4676 // returns offset needed for pxBounds to get inside maxBounds at a specified zoom
4677 _getBoundsOffset: function (pxBounds, maxBounds, zoom) {
4678 var projectedMaxBounds = toBounds(
4679 this.project(maxBounds.getNorthEast(), zoom),
4680 this.project(maxBounds.getSouthWest(), zoom)
4681 ),
4682 minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
4683 maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),
4684
4685 dx = this._rebound(minOffset.x, -maxOffset.x),
4686 dy = this._rebound(minOffset.y, -maxOffset.y);
4687
4688 return new Point(dx, dy);
4689 },
4690
4691 _rebound: function (left, right) {
4692 return left + right > 0 ?
4693 Math.round(left - right) / 2 :
4694 Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
4695 },
4696
4697 _limitZoom: function (zoom) {
4698 var min = this.getMinZoom(),
4699 max = this.getMaxZoom(),
4700 snap = Browser.any3d ? this.options.zoomSnap : 1;
4701 if (snap) {
4702 zoom = Math.round(zoom / snap) * snap;
4703 }
4704 return Math.max(min, Math.min(max, zoom));
4705 },
4706
4707 _onPanTransitionStep: function () {
4708 this.fire('move');
4709 },
4710
4711 _onPanTransitionEnd: function () {
4712 removeClass(this._mapPane, 'leaflet-pan-anim');
4713 this.fire('moveend');
4714 },
4715
4716 _tryAnimatedPan: function (center, options) {
4717 // difference between the new and current centers in pixels
4718 var offset = this._getCenterOffset(center)._trunc();
4719
4720 // don't animate too far unless animate: true specified in options
4721 if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
4722
4723 this.panBy(offset, options);
4724
4725 return true;
4726 },
4727
4728 _createAnimProxy: function () {
4729
4730 var proxy = this._proxy = create$1('div', 'leaflet-proxy leaflet-zoom-animated');
4731 this._panes.mapPane.appendChild(proxy);
4732
4733 this.on('zoomanim', function (e) {
4734 var prop = TRANSFORM,
4735 transform = this._proxy.style[prop];
4736
4737 setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));
4738
4739 // workaround for case when transform is the same and so transitionend event is not fired
4740 if (transform === this._proxy.style[prop] && this._animatingZoom) {
4741 this._onZoomTransitionEnd();
4742 }
4743 }, this);
4744
4745 this.on('load moveend', this._animMoveEnd, this);
4746
4747 this._on('unload', this._destroyAnimProxy, this);
4748 },
4749
4750 _destroyAnimProxy: function () {
4751 remove(this._proxy);
4752 this.off('load moveend', this._animMoveEnd, this);
4753 delete this._proxy;
4754 },
4755
4756 _animMoveEnd: function () {
4757 var c = this.getCenter(),
4758 z = this.getZoom();
4759 setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1));
4760 },
4761
4762 _catchTransitionEnd: function (e) {
4763 if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
4764 this._onZoomTransitionEnd();
4765 }
4766 },
4767
4768 _nothingToAnimate: function () {
4769 return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
4770 },
4771
4772 _tryAnimatedZoom: function (center, zoom, options) {
4773
4774 if (this._animatingZoom) { return true; }
4775
4776 options = options || {};
4777
4778 // don't animate if disabled, not supported or zoom difference is too large
4779 if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
4780 Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
4781
4782 // offset is the pixel coords of the zoom origin relative to the current center
4783 var scale = this.getZoomScale(zoom),
4784 offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);
4785
4786 // don't animate if the zoom origin isn't within one screen from the current center, unless forced
4787 if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
4788
4789 requestAnimFrame(function () {
4790 this
4791 ._moveStart(true, false)
4792 ._animateZoom(center, zoom, true);
4793 }, this);
4794
4795 return true;
4796 },
4797
4798 _animateZoom: function (center, zoom, startAnim, noUpdate) {
4799 if (!this._mapPane) { return; }
4800
4801 if (startAnim) {
4802 this._animatingZoom = true;
4803
4804 // remember what center/zoom to set after animation
4805 this._animateToCenter = center;
4806 this._animateToZoom = zoom;
4807
4808 addClass(this._mapPane, 'leaflet-zoom-anim');
4809 }
4810
4811 // @section Other Events
4812 // @event zoomanim: ZoomAnimEvent
4813 // Fired at least once per zoom animation. For continuous zoom, like pinch zooming, fired once per frame during zoom.
4814 this.fire('zoomanim', {
4815 center: center,
4816 zoom: zoom,
4817 noUpdate: noUpdate
4818 });
4819
4820 if (!this._tempFireZoomEvent) {
4821 this._tempFireZoomEvent = this._zoom !== this._animateToZoom;
4822 }
4823
4824 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
4825
4826 // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
4827 setTimeout(bind(this._onZoomTransitionEnd, this), 250);
4828 },
4829
4830 _onZoomTransitionEnd: function () {
4831 if (!this._animatingZoom) { return; }
4832
4833 if (this._mapPane) {
4834 removeClass(this._mapPane, 'leaflet-zoom-anim');
4835 }
4836
4837 this._animatingZoom = false;
4838
4839 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
4840
4841 if (this._tempFireZoomEvent) {
4842 this.fire('zoom');
4843 }
4844 delete this._tempFireZoomEvent;
4845
4846 this.fire('move');
4847
4848 this._moveEnd(true);
4849 }
4850 });
4851
4852 // @section
4853
4854 // @factory L.map(id: String, options?: Map options)
4855 // Instantiates a map object given the DOM ID of a `<div>` element
4856 // and optionally an object literal with `Map options`.
4857 //
4858 // @alternative
4859 // @factory L.map(el: HTMLElement, options?: Map options)
4860 // Instantiates a map object given an instance of a `<div>` HTML element
4861 // and optionally an object literal with `Map options`.
4862 function createMap(id, options) {
4863 return new Map(id, options);
4864 }
4865
4866 /*
4867 * @class Control
4868 * @aka L.Control
4869 * @inherits Class
4870 *
4871 * L.Control is a base class for implementing map controls. Handles positioning.
4872 * All other controls extend from this class.
4873 */
4874
4875 var Control = Class.extend({
4876 // @section
4877 // @aka Control Options
4878 options: {
4879 // @option position: String = 'topright'
4880 // The position of the control (one of the map corners). Possible values are `'topleft'`,
4881 // `'topright'`, `'bottomleft'` or `'bottomright'`
4882 position: 'topright'
4883 },
4884
4885 initialize: function (options) {
4886 setOptions(this, options);
4887 },
4888
4889 /* @section
4890 * Classes extending L.Control will inherit the following methods:
4891 *
4892 * @method getPosition: string
4893 * Returns the position of the control.
4894 */
4895 getPosition: function () {
4896 return this.options.position;
4897 },
4898
4899 // @method setPosition(position: string): this
4900 // Sets the position of the control.
4901 setPosition: function (position) {
4902 var map = this._map;
4903
4904 if (map) {
4905 map.removeControl(this);
4906 }
4907
4908 this.options.position = position;
4909
4910 if (map) {
4911 map.addControl(this);
4912 }
4913
4914 return this;
4915 },
4916
4917 // @method getContainer: HTMLElement
4918 // Returns the HTMLElement that contains the control.
4919 getContainer: function () {
4920 return this._container;
4921 },
4922
4923 // @method addTo(map: Map): this
4924 // Adds the control to the given map.
4925 addTo: function (map) {
4926 this.remove();
4927 this._map = map;
4928
4929 var container = this._container = this.onAdd(map),
4930 pos = this.getPosition(),
4931 corner = map._controlCorners[pos];
4932
4933 addClass(container, 'leaflet-control');
4934
4935 if (pos.indexOf('bottom') !== -1) {
4936 corner.insertBefore(container, corner.firstChild);
4937 } else {
4938 corner.appendChild(container);
4939 }
4940
4941 this._map.on('unload', this.remove, this);
4942
4943 return this;
4944 },
4945
4946 // @method remove: this
4947 // Removes the control from the map it is currently active on.
4948 remove: function () {
4949 if (!this._map) {
4950 return this;
4951 }
4952
4953 remove(this._container);
4954
4955 if (this.onRemove) {
4956 this.onRemove(this._map);
4957 }
4958
4959 this._map.off('unload', this.remove, this);
4960 this._map = null;
4961
4962 return this;
4963 },
4964
4965 _refocusOnMap: function (e) {
4966 // if map exists and event is not a keyboard event
4967 if (this._map && e && e.screenX > 0 && e.screenY > 0) {
4968 this._map.getContainer().focus();
4969 }
4970 }
4971 });
4972
4973 var control = function (options) {
4974 return new Control(options);
4975 };
4976
4977 /* @section Extension methods
4978 * @uninheritable
4979 *
4980 * Every control should extend from `L.Control` and (re-)implement the following methods.
4981 *
4982 * @method onAdd(map: Map): HTMLElement
4983 * Should return the container DOM element for the control and add listeners on relevant map events. Called on [`control.addTo(map)`](#control-addTo).
4984 *
4985 * @method onRemove(map: Map)
4986 * Optional method. Should contain all clean up code that removes the listeners previously added in [`onAdd`](#control-onadd). Called on [`control.remove()`](#control-remove).
4987 */
4988
4989 /* @namespace Map
4990 * @section Methods for Layers and Controls
4991 */
4992 Map.include({
4993 // @method addControl(control: Control): this
4994 // Adds the given control to the map
4995 addControl: function (control) {
4996 control.addTo(this);
4997 return this;
4998 },
4999
5000 // @method removeControl(control: Control): this
5001 // Removes the given control from the map
5002 removeControl: function (control) {
5003 control.remove();
5004 return this;
5005 },
5006
5007 _initControlPos: function () {
5008 var corners = this._controlCorners = {},
5009 l = 'leaflet-',
5010 container = this._controlContainer =
5011 create$1('div', l + 'control-container', this._container);
5012
5013 function createCorner(vSide, hSide) {
5014 var className = l + vSide + ' ' + l + hSide;
5015
5016 corners[vSide + hSide] = create$1('div', className, container);
5017 }
5018
5019 createCorner('top', 'left');
5020 createCorner('top', 'right');
5021 createCorner('bottom', 'left');
5022 createCorner('bottom', 'right');
5023 },
5024
5025 _clearControlPos: function () {
5026 for (var i in this._controlCorners) {
5027 remove(this._controlCorners[i]);
5028 }
5029 remove(this._controlContainer);
5030 delete this._controlCorners;
5031 delete this._controlContainer;
5032 }
5033 });
5034
5035 /*
5036 * @class Control.Layers
5037 * @aka L.Control.Layers
5038 * @inherits Control
5039 *
5040 * The layers control gives users the ability to switch between different base layers and switch overlays on/off (check out the [detailed example](https://leafletjs.com/examples/layers-control/)). Extends `Control`.
5041 *
5042 * @example
5043 *
5044 * ```js
5045 * var baseLayers = {
5046 * "Mapbox": mapbox,
5047 * "OpenStreetMap": osm
5048 * };
5049 *
5050 * var overlays = {
5051 * "Marker": marker,
5052 * "Roads": roadsLayer
5053 * };
5054 *
5055 * L.control.layers(baseLayers, overlays).addTo(map);
5056 * ```
5057 *
5058 * The `baseLayers` and `overlays` parameters are object literals with layer names as keys and `Layer` objects as values:
5059 *
5060 * ```js
5061 * {
5062 * "<someName1>": layer1,
5063 * "<someName2>": layer2
5064 * }
5065 * ```
5066 *
5067 * The layer names can contain HTML, which allows you to add additional styling to the items:
5068 *
5069 * ```js
5070 * {"<img src='my-layer-icon' /> <span class='my-layer-item'>My Layer</span>": myLayer}
5071 * ```
5072 */
5073
5074 var Layers = Control.extend({
5075 // @section
5076 // @aka Control.Layers options
5077 options: {
5078 // @option collapsed: Boolean = true
5079 // If `true`, the control will be collapsed into an icon and expanded on mouse hover, touch, or keyboard activation.
5080 collapsed: true,
5081 position: 'topright',
5082
5083 // @option autoZIndex: Boolean = true
5084 // If `true`, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off.
5085 autoZIndex: true,
5086
5087 // @option hideSingleBase: Boolean = false
5088 // If `true`, the base layers in the control will be hidden when there is only one.
5089 hideSingleBase: false,
5090
5091 // @option sortLayers: Boolean = false
5092 // Whether to sort the layers. When `false`, layers will keep the order
5093 // in which they were added to the control.
5094 sortLayers: false,
5095
5096 // @option sortFunction: Function = *
5097 // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
5098 // that will be used for sorting the layers, when `sortLayers` is `true`.
5099 // The function receives both the `L.Layer` instances and their names, as in
5100 // `sortFunction(layerA, layerB, nameA, nameB)`.
5101 // By default, it sorts layers alphabetically by their name.
5102 sortFunction: function (layerA, layerB, nameA, nameB) {
5103 return nameA < nameB ? -1 : (nameB < nameA ? 1 : 0);
5104 }
5105 },
5106
5107 initialize: function (baseLayers, overlays, options) {
5108 setOptions(this, options);
5109
5110 this._layerControlInputs = [];
5111 this._layers = [];
5112 this._lastZIndex = 0;
5113 this._handlingClick = false;
5114
5115 for (var i in baseLayers) {
5116 this._addLayer(baseLayers[i], i);
5117 }
5118
5119 for (i in overlays) {
5120 this._addLayer(overlays[i], i, true);
5121 }
5122 },
5123
5124 onAdd: function (map) {
5125 this._initLayout();
5126 this._update();
5127
5128 this._map = map;
5129 map.on('zoomend', this._checkDisabledLayers, this);
5130
5131 for (var i = 0; i < this._layers.length; i++) {
5132 this._layers[i].layer.on('add remove', this._onLayerChange, this);
5133 }
5134
5135 return this._container;
5136 },
5137
5138 addTo: function (map) {
5139 Control.prototype.addTo.call(this, map);
5140 // Trigger expand after Layers Control has been inserted into DOM so that is now has an actual height.
5141 return this._expandIfNotCollapsed();
5142 },
5143
5144 onRemove: function () {
5145 this._map.off('zoomend', this._checkDisabledLayers, this);
5146
5147 for (var i = 0; i < this._layers.length; i++) {
5148 this._layers[i].layer.off('add remove', this._onLayerChange, this);
5149 }
5150 },
5151
5152 // @method addBaseLayer(layer: Layer, name: String): this
5153 // Adds a base layer (radio button entry) with the given name to the control.
5154 addBaseLayer: function (layer, name) {
5155 this._addLayer(layer, name);
5156 return (this._map) ? this._update() : this;
5157 },
5158
5159 // @method addOverlay(layer: Layer, name: String): this
5160 // Adds an overlay (checkbox entry) with the given name to the control.
5161 addOverlay: function (layer, name) {
5162 this._addLayer(layer, name, true);
5163 return (this._map) ? this._update() : this;
5164 },
5165
5166 // @method removeLayer(layer: Layer): this
5167 // Remove the given layer from the control.
5168 removeLayer: function (layer) {
5169 layer.off('add remove', this._onLayerChange, this);
5170
5171 var obj = this._getLayer(stamp(layer));
5172 if (obj) {
5173 this._layers.splice(this._layers.indexOf(obj), 1);
5174 }
5175 return (this._map) ? this._update() : this;
5176 },
5177
5178 // @method expand(): this
5179 // Expand the control container if collapsed.
5180 expand: function () {
5181 addClass(this._container, 'leaflet-control-layers-expanded');
5182 this._section.style.height = null;
5183 var acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50);
5184 if (acceptableHeight < this._section.clientHeight) {
5185 addClass(this._section, 'leaflet-control-layers-scrollbar');
5186 this._section.style.height = acceptableHeight + 'px';
5187 } else {
5188 removeClass(this._section, 'leaflet-control-layers-scrollbar');
5189 }
5190 this._checkDisabledLayers();
5191 return this;
5192 },
5193
5194 // @method collapse(): this
5195 // Collapse the control container if expanded.
5196 collapse: function () {
5197 removeClass(this._container, 'leaflet-control-layers-expanded');
5198 return this;
5199 },
5200
5201 _initLayout: function () {
5202 var className = 'leaflet-control-layers',
5203 container = this._container = create$1('div', className),
5204 collapsed = this.options.collapsed;
5205
5206 // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released
5207 container.setAttribute('aria-haspopup', true);
5208
5209 disableClickPropagation(container);
5210 disableScrollPropagation(container);
5211
5212 var section = this._section = create$1('section', className + '-list');
5213
5214 if (collapsed) {
5215 this._map.on('click', this.collapse, this);
5216
5217 on(container, {
5218 mouseenter: this._expandSafely,
5219 mouseleave: this.collapse
5220 }, this);
5221 }
5222
5223 var link = this._layersLink = create$1('a', className + '-toggle', container);
5224 link.href = '#';
5225 link.title = 'Layers';
5226 link.setAttribute('role', 'button');
5227
5228 on(link, {
5229 keydown: function (e) {
5230 if (e.keyCode === 13) {
5231 this._expandSafely();
5232 }
5233 },
5234 // Certain screen readers intercept the key event and instead send a click event
5235 click: function (e) {
5236 preventDefault(e);
5237 this._expandSafely();
5238 }
5239 }, this);
5240
5241 if (!collapsed) {
5242 this.expand();
5243 }
5244
5245 this._baseLayersList = create$1('div', className + '-base', section);
5246 this._separator = create$1('div', className + '-separator', section);
5247 this._overlaysList = create$1('div', className + '-overlays', section);
5248
5249 container.appendChild(section);
5250 },
5251
5252 _getLayer: function (id) {
5253 for (var i = 0; i < this._layers.length; i++) {
5254
5255 if (this._layers[i] && stamp(this._layers[i].layer) === id) {
5256 return this._layers[i];
5257 }
5258 }
5259 },
5260
5261 _addLayer: function (layer, name, overlay) {
5262 if (this._map) {
5263 layer.on('add remove', this._onLayerChange, this);
5264 }
5265
5266 this._layers.push({
5267 layer: layer,
5268 name: name,
5269 overlay: overlay
5270 });
5271
5272 if (this.options.sortLayers) {
5273 this._layers.sort(bind(function (a, b) {
5274 return this.options.sortFunction(a.layer, b.layer, a.name, b.name);
5275 }, this));
5276 }
5277
5278 if (this.options.autoZIndex && layer.setZIndex) {
5279 this._lastZIndex++;
5280 layer.setZIndex(this._lastZIndex);
5281 }
5282
5283 this._expandIfNotCollapsed();
5284 },
5285
5286 _update: function () {
5287 if (!this._container) { return this; }
5288
5289 empty(this._baseLayersList);
5290 empty(this._overlaysList);
5291
5292 this._layerControlInputs = [];
5293 var baseLayersPresent, overlaysPresent, i, obj, baseLayersCount = 0;
5294
5295 for (i = 0; i < this._layers.length; i++) {
5296 obj = this._layers[i];
5297 this._addItem(obj);
5298 overlaysPresent = overlaysPresent || obj.overlay;
5299 baseLayersPresent = baseLayersPresent || !obj.overlay;
5300 baseLayersCount += !obj.overlay ? 1 : 0;
5301 }
5302
5303 // Hide base layers section if there's only one layer.
5304 if (this.options.hideSingleBase) {
5305 baseLayersPresent = baseLayersPresent && baseLayersCount > 1;
5306 this._baseLayersList.style.display = baseLayersPresent ? '' : 'none';
5307 }
5308
5309 this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
5310
5311 return this;
5312 },
5313
5314 _onLayerChange: function (e) {
5315 if (!this._handlingClick) {
5316 this._update();
5317 }
5318
5319 var obj = this._getLayer(stamp(e.target));
5320
5321 // @namespace Map
5322 // @section Layer events
5323 // @event baselayerchange: LayersControlEvent
5324 // Fired when the base layer is changed through the [layers control](#control-layers).
5325 // @event overlayadd: LayersControlEvent
5326 // Fired when an overlay is selected through the [layers control](#control-layers).
5327 // @event overlayremove: LayersControlEvent
5328 // Fired when an overlay is deselected through the [layers control](#control-layers).
5329 // @namespace Control.Layers
5330 var type = obj.overlay ?
5331 (e.type === 'add' ? 'overlayadd' : 'overlayremove') :
5332 (e.type === 'add' ? 'baselayerchange' : null);
5333
5334 if (type) {
5335 this._map.fire(type, obj);
5336 }
5337 },
5338
5339 // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see https://stackoverflow.com/a/119079)
5340 _createRadioElement: function (name, checked) {
5341
5342 var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' +
5343 name + '"' + (checked ? ' checked="checked"' : '') + '/>';
5344
5345 var radioFragment = document.createElement('div');
5346 radioFragment.innerHTML = radioHtml;
5347
5348 return radioFragment.firstChild;
5349 },
5350
5351 _addItem: function (obj) {
5352 var label = document.createElement('label'),
5353 checked = this._map.hasLayer(obj.layer),
5354 input;
5355
5356 if (obj.overlay) {
5357 input = document.createElement('input');
5358 input.type = 'checkbox';
5359 input.className = 'leaflet-control-layers-selector';
5360 input.defaultChecked = checked;
5361 } else {
5362 input = this._createRadioElement('leaflet-base-layers_' + stamp(this), checked);
5363 }
5364
5365 this._layerControlInputs.push(input);
5366 input.layerId = stamp(obj.layer);
5367
5368 on(input, 'click', this._onInputClick, this);
5369
5370 var name = document.createElement('span');
5371 name.innerHTML = ' ' + obj.name;
5372
5373 // Helps from preventing layer control flicker when checkboxes are disabled
5374 // https://github.com/Leaflet/Leaflet/issues/2771
5375 var holder = document.createElement('span');
5376
5377 label.appendChild(holder);
5378 holder.appendChild(input);
5379 holder.appendChild(name);
5380
5381 var container = obj.overlay ? this._overlaysList : this._baseLayersList;
5382 container.appendChild(label);
5383
5384 this._checkDisabledLayers();
5385 return label;
5386 },
5387
5388 _onInputClick: function () {
5389 var inputs = this._layerControlInputs,
5390 input, layer;
5391 var addedLayers = [],
5392 removedLayers = [];
5393
5394 this._handlingClick = true;
5395
5396 for (var i = inputs.length - 1; i >= 0; i--) {
5397 input = inputs[i];
5398 layer = this._getLayer(input.layerId).layer;
5399
5400 if (input.checked) {
5401 addedLayers.push(layer);
5402 } else if (!input.checked) {
5403 removedLayers.push(layer);
5404 }
5405 }
5406
5407 // Bugfix issue 2318: Should remove all old layers before readding new ones
5408 for (i = 0; i < removedLayers.length; i++) {
5409 if (this._map.hasLayer(removedLayers[i])) {
5410 this._map.removeLayer(removedLayers[i]);
5411 }
5412 }
5413 for (i = 0; i < addedLayers.length; i++) {
5414 if (!this._map.hasLayer(addedLayers[i])) {
5415 this._map.addLayer(addedLayers[i]);
5416 }
5417 }
5418
5419 this._handlingClick = false;
5420
5421 this._refocusOnMap();
5422 },
5423
5424 _checkDisabledLayers: function () {
5425 var inputs = this._layerControlInputs,
5426 input,
5427 layer,
5428 zoom = this._map.getZoom();
5429
5430 for (var i = inputs.length - 1; i >= 0; i--) {
5431 input = inputs[i];
5432 layer = this._getLayer(input.layerId).layer;
5433 input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) ||
5434 (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom);
5435
5436 }
5437 },
5438
5439 _expandIfNotCollapsed: function () {
5440 if (this._map && !this.options.collapsed) {
5441 this.expand();
5442 }
5443 return this;
5444 },
5445
5446 _expandSafely: function () {
5447 var section = this._section;
5448 on(section, 'click', preventDefault);
5449 this.expand();
5450 setTimeout(function () {
5451 off(section, 'click', preventDefault);
5452 });
5453 }
5454
5455 });
5456
5457
5458 // @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options)
5459 // Creates a layers control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. Note that all base layers should be passed in the base layers object, but only one should be added to the map during map instantiation.
5460 var layers = function (baseLayers, overlays, options) {
5461 return new Layers(baseLayers, overlays, options);
5462 };
5463
5464 /*
5465 * @class Control.Zoom
5466 * @aka L.Control.Zoom
5467 * @inherits Control
5468 *
5469 * A basic zoom control with two buttons (zoom in and zoom out). It is put on the map by default unless you set its [`zoomControl` option](#map-zoomcontrol) to `false`. Extends `Control`.
5470 */
5471
5472 var Zoom = Control.extend({
5473 // @section
5474 // @aka Control.Zoom options
5475 options: {
5476 position: 'topleft',
5477
5478 // @option zoomInText: String = '<span aria-hidden="true">+</span>'
5479 // The text set on the 'zoom in' button.
5480 zoomInText: '<span aria-hidden="true">+</span>',
5481
5482 // @option zoomInTitle: String = 'Zoom in'
5483 // The title set on the 'zoom in' button.
5484 zoomInTitle: 'Zoom in',
5485
5486 // @option zoomOutText: String = '<span aria-hidden="true">&#x2212;</span>'
5487 // The text set on the 'zoom out' button.
5488 zoomOutText: '<span aria-hidden="true">&#x2212;</span>',
5489
5490 // @option zoomOutTitle: String = 'Zoom out'
5491 // The title set on the 'zoom out' button.
5492 zoomOutTitle: 'Zoom out'
5493 },
5494
5495 onAdd: function (map) {
5496 var zoomName = 'leaflet-control-zoom',
5497 container = create$1('div', zoomName + ' leaflet-bar'),
5498 options = this.options;
5499
5500 this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle,
5501 zoomName + '-in', container, this._zoomIn);
5502 this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle,
5503 zoomName + '-out', container, this._zoomOut);
5504
5505 this._updateDisabled();
5506 map.on('zoomend zoomlevelschange', this._updateDisabled, this);
5507
5508 return container;
5509 },
5510
5511 onRemove: function (map) {
5512 map.off('zoomend zoomlevelschange', this._updateDisabled, this);
5513 },
5514
5515 disable: function () {
5516 this._disabled = true;
5517 this._updateDisabled();
5518 return this;
5519 },
5520
5521 enable: function () {
5522 this._disabled = false;
5523 this._updateDisabled();
5524 return this;
5525 },
5526
5527 _zoomIn: function (e) {
5528 if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) {
5529 this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
5530 }
5531 },
5532
5533 _zoomOut: function (e) {
5534 if (!this._disabled && this._map._zoom > this._map.getMinZoom()) {
5535 this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
5536 }
5537 },
5538
5539 _createButton: function (html, title, className, container, fn) {
5540 var link = create$1('a', className, container);
5541 link.innerHTML = html;
5542 link.href = '#';
5543 link.title = title;
5544
5545 /*
5546 * Will force screen readers like VoiceOver to read this as "Zoom in - button"
5547 */
5548 link.setAttribute('role', 'button');
5549 link.setAttribute('aria-label', title);
5550
5551 disableClickPropagation(link);
5552 on(link, 'click', stop);
5553 on(link, 'click', fn, this);
5554 on(link, 'click', this._refocusOnMap, this);
5555
5556 return link;
5557 },
5558
5559 _updateDisabled: function () {
5560 var map = this._map,
5561 className = 'leaflet-disabled';
5562
5563 removeClass(this._zoomInButton, className);
5564 removeClass(this._zoomOutButton, className);
5565 this._zoomInButton.setAttribute('aria-disabled', 'false');
5566 this._zoomOutButton.setAttribute('aria-disabled', 'false');
5567
5568 if (this._disabled || map._zoom === map.getMinZoom()) {
5569 addClass(this._zoomOutButton, className);
5570 this._zoomOutButton.setAttribute('aria-disabled', 'true');
5571 }
5572 if (this._disabled || map._zoom === map.getMaxZoom()) {
5573 addClass(this._zoomInButton, className);
5574 this._zoomInButton.setAttribute('aria-disabled', 'true');
5575 }
5576 }
5577 });
5578
5579 // @namespace Map
5580 // @section Control options
5581 // @option zoomControl: Boolean = true
5582 // Whether a [zoom control](#control-zoom) is added to the map by default.
5583 Map.mergeOptions({
5584 zoomControl: true
5585 });
5586
5587 Map.addInitHook(function () {
5588 if (this.options.zoomControl) {
5589 // @section Controls
5590 // @property zoomControl: Control.Zoom
5591 // The default zoom control (only available if the
5592 // [`zoomControl` option](#map-zoomcontrol) was `true` when creating the map).
5593 this.zoomControl = new Zoom();
5594 this.addControl(this.zoomControl);
5595 }
5596 });
5597
5598 // @namespace Control.Zoom
5599 // @factory L.control.zoom(options: Control.Zoom options)
5600 // Creates a zoom control
5601 var zoom = function (options) {
5602 return new Zoom(options);
5603 };
5604
5605 /*
5606 * @class Control.Scale
5607 * @aka L.Control.Scale
5608 * @inherits Control
5609 *
5610 * A simple scale control that shows the scale of the current center of screen in metric (m/km) and imperial (mi/ft) systems. Extends `Control`.
5611 *
5612 * @example
5613 *
5614 * ```js
5615 * L.control.scale().addTo(map);
5616 * ```
5617 */
5618
5619 var Scale = Control.extend({
5620 // @section
5621 // @aka Control.Scale options
5622 options: {
5623 position: 'bottomleft',
5624
5625 // @option maxWidth: Number = 100
5626 // Maximum width of the control in pixels. The width is set dynamically to show round values (e.g. 100, 200, 500).
5627 maxWidth: 100,
5628
5629 // @option metric: Boolean = True
5630 // Whether to show the metric scale line (m/km).
5631 metric: true,
5632
5633 // @option imperial: Boolean = True
5634 // Whether to show the imperial scale line (mi/ft).
5635 imperial: true
5636
5637 // @option updateWhenIdle: Boolean = false
5638 // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)).
5639 },
5640
5641 onAdd: function (map) {
5642 var className = 'leaflet-control-scale',
5643 container = create$1('div', className),
5644 options = this.options;
5645
5646 this._addScales(options, className + '-line', container);
5647
5648 map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
5649 map.whenReady(this._update, this);
5650
5651 return container;
5652 },
5653
5654 onRemove: function (map) {
5655 map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
5656 },
5657
5658 _addScales: function (options, className, container) {
5659 if (options.metric) {
5660 this._mScale = create$1('div', className, container);
5661 }
5662 if (options.imperial) {
5663 this._iScale = create$1('div', className, container);
5664 }
5665 },
5666
5667 _update: function () {
5668 var map = this._map,
5669 y = map.getSize().y / 2;
5670
5671 var maxMeters = map.distance(
5672 map.containerPointToLatLng([0, y]),
5673 map.containerPointToLatLng([this.options.maxWidth, y]));
5674
5675 this._updateScales(maxMeters);
5676 },
5677
5678 _updateScales: function (maxMeters) {
5679 if (this.options.metric && maxMeters) {
5680 this._updateMetric(maxMeters);
5681 }
5682 if (this.options.imperial && maxMeters) {
5683 this._updateImperial(maxMeters);
5684 }
5685 },
5686
5687 _updateMetric: function (maxMeters) {
5688 var meters = this._getRoundNum(maxMeters),
5689 label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
5690
5691 this._updateScale(this._mScale, label, meters / maxMeters);
5692 },
5693
5694 _updateImperial: function (maxMeters) {
5695 var maxFeet = maxMeters * 3.2808399,
5696 maxMiles, miles, feet;
5697
5698 if (maxFeet > 5280) {
5699 maxMiles = maxFeet / 5280;
5700 miles = this._getRoundNum(maxMiles);
5701 this._updateScale(this._iScale, miles + ' mi', miles / maxMiles);
5702
5703 } else {
5704 feet = this._getRoundNum(maxFeet);
5705 this._updateScale(this._iScale, feet + ' ft', feet / maxFeet);
5706 }
5707 },
5708
5709 _updateScale: function (scale, text, ratio) {
5710 scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px';
5711 scale.innerHTML = text;
5712 },
5713
5714 _getRoundNum: function (num) {
5715 var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
5716 d = num / pow10;
5717
5718 d = d >= 10 ? 10 :
5719 d >= 5 ? 5 :
5720 d >= 3 ? 3 :
5721 d >= 2 ? 2 : 1;
5722
5723 return pow10 * d;
5724 }
5725 });
5726
5727
5728 // @factory L.control.scale(options?: Control.Scale options)
5729 // Creates an scale control with the given options.
5730 var scale = function (options) {
5731 return new Scale(options);
5732 };
5733
5734 var ukrainianFlag = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" class="leaflet-attribution-flag"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg>';
5735
5736
5737 /*
5738 * @class Control.Attribution
5739 * @aka L.Control.Attribution
5740 * @inherits Control
5741 *
5742 * The attribution control allows you to display attribution data in a small text box on a map. It is put on the map by default unless you set its [`attributionControl` option](#map-attributioncontrol) to `false`, and it fetches attribution texts from layers with the [`getAttribution` method](#layer-getattribution) automatically. Extends Control.
5743 */
5744
5745 var Attribution = Control.extend({
5746 // @section
5747 // @aka Control.Attribution options
5748 options: {
5749 position: 'bottomright',
5750
5751 // @option prefix: String|false = 'Leaflet'
5752 // The HTML text shown before the attributions. Pass `false` to disable.
5753 prefix: '<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">' + (Browser.inlineSvg ? ukrainianFlag + ' ' : '') + 'Leaflet</a>'
5754 },
5755
5756 initialize: function (options) {
5757 setOptions(this, options);
5758
5759 this._attributions = {};
5760 },
5761
5762 onAdd: function (map) {
5763 map.attributionControl = this;
5764 this._container = create$1('div', 'leaflet-control-attribution');
5765 disableClickPropagation(this._container);
5766
5767 // TODO ugly, refactor
5768 for (var i in map._layers) {
5769 if (map._layers[i].getAttribution) {
5770 this.addAttribution(map._layers[i].getAttribution());
5771 }
5772 }
5773
5774 this._update();
5775
5776 map.on('layeradd', this._addAttribution, this);
5777
5778 return this._container;
5779 },
5780
5781 onRemove: function (map) {
5782 map.off('layeradd', this._addAttribution, this);
5783 },
5784
5785 _addAttribution: function (ev) {
5786 if (ev.layer.getAttribution) {
5787 this.addAttribution(ev.layer.getAttribution());
5788 ev.layer.once('remove', function () {
5789 this.removeAttribution(ev.layer.getAttribution());
5790 }, this);
5791 }
5792 },
5793
5794 // @method setPrefix(prefix: String|false): this
5795 // The HTML text shown before the attributions. Pass `false` to disable.
5796 setPrefix: function (prefix) {
5797 this.options.prefix = prefix;
5798 this._update();
5799 return this;
5800 },
5801
5802 // @method addAttribution(text: String): this
5803 // Adds an attribution text (e.g. `'&copy; OpenStreetMap contributors'`).
5804 addAttribution: function (text) {
5805 if (!text) { return this; }
5806
5807 if (!this._attributions[text]) {
5808 this._attributions[text] = 0;
5809 }
5810 this._attributions[text]++;
5811
5812 this._update();
5813
5814 return this;
5815 },
5816
5817 // @method removeAttribution(text: String): this
5818 // Removes an attribution text.
5819 removeAttribution: function (text) {
5820 if (!text) { return this; }
5821
5822 if (this._attributions[text]) {
5823 this._attributions[text]--;
5824 this._update();
5825 }
5826
5827 return this;
5828 },
5829
5830 _update: function () {
5831 if (!this._map) { return; }
5832
5833 var attribs = [];
5834
5835 for (var i in this._attributions) {
5836 if (this._attributions[i]) {
5837 attribs.push(i);
5838 }
5839 }
5840
5841 var prefixAndAttribs = [];
5842
5843 if (this.options.prefix) {
5844 prefixAndAttribs.push(this.options.prefix);
5845 }
5846 if (attribs.length) {
5847 prefixAndAttribs.push(attribs.join(', '));
5848 }
5849
5850 this._container.innerHTML = prefixAndAttribs.join(' <span aria-hidden="true">|</span> ');
5851 }
5852 });
5853
5854 // @namespace Map
5855 // @section Control options
5856 // @option attributionControl: Boolean = true
5857 // Whether a [attribution control](#control-attribution) is added to the map by default.
5858 Map.mergeOptions({
5859 attributionControl: true
5860 });
5861
5862 Map.addInitHook(function () {
5863 if (this.options.attributionControl) {
5864 new Attribution().addTo(this);
5865 }
5866 });
5867
5868 // @namespace Control.Attribution
5869 // @factory L.control.attribution(options: Control.Attribution options)
5870 // Creates an attribution control.
5871 var attribution = function (options) {
5872 return new Attribution(options);
5873 };
5874
5875 Control.Layers = Layers;
5876 Control.Zoom = Zoom;
5877 Control.Scale = Scale;
5878 Control.Attribution = Attribution;
5879
5880 control.layers = layers;
5881 control.zoom = zoom;
5882 control.scale = scale;
5883 control.attribution = attribution;
5884
5885 /*
5886 L.Handler is a base class for handler classes that are used internally to inject
5887 interaction features like dragging to classes like Map and Marker.
5888 */
5889
5890 // @class Handler
5891 // @aka L.Handler
5892 // Abstract class for map interaction handlers
5893
5894 var Handler = Class.extend({
5895 initialize: function (map) {
5896 this._map = map;
5897 },
5898
5899 // @method enable(): this
5900 // Enables the handler
5901 enable: function () {
5902 if (this._enabled) { return this; }
5903
5904 this._enabled = true;
5905 this.addHooks();
5906 return this;
5907 },
5908
5909 // @method disable(): this
5910 // Disables the handler
5911 disable: function () {
5912 if (!this._enabled) { return this; }
5913
5914 this._enabled = false;
5915 this.removeHooks();
5916 return this;
5917 },
5918
5919 // @method enabled(): Boolean
5920 // Returns `true` if the handler is enabled
5921 enabled: function () {
5922 return !!this._enabled;
5923 }
5924
5925 // @section Extension methods
5926 // Classes inheriting from `Handler` must implement the two following methods:
5927 // @method addHooks()
5928 // Called when the handler is enabled, should add event hooks.
5929 // @method removeHooks()
5930 // Called when the handler is disabled, should remove the event hooks added previously.
5931 });
5932
5933 // @section There is static function which can be called without instantiating L.Handler:
5934 // @function addTo(map: Map, name: String): this
5935 // Adds a new Handler to the given map with the given name.
5936 Handler.addTo = function (map, name) {
5937 map.addHandler(name, this);
5938 return this;
5939 };
5940
5941 var Mixin = {Events: Events};
5942
5943 /*
5944 * @class Draggable
5945 * @aka L.Draggable
5946 * @inherits Evented
5947 *
5948 * A class for making DOM elements draggable (including touch support).
5949 * Used internally for map and marker dragging. Only works for elements
5950 * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition).
5951 *
5952 * @example
5953 * ```js
5954 * var draggable = new L.Draggable(elementToDrag);
5955 * draggable.enable();
5956 * ```
5957 */
5958
5959 var START = Browser.touch ? 'touchstart mousedown' : 'mousedown';
5960
5961 var Draggable = Evented.extend({
5962
5963 options: {
5964 // @section
5965 // @aka Draggable options
5966 // @option clickTolerance: Number = 3
5967 // The max number of pixels a user can shift the mouse pointer during a click
5968 // for it to be considered a valid click (as opposed to a mouse drag).
5969 clickTolerance: 3
5970 },
5971
5972 // @constructor L.Draggable(el: HTMLElement, dragHandle?: HTMLElement, preventOutline?: Boolean, options?: Draggable options)
5973 // Creates a `Draggable` object for moving `el` when you start dragging the `dragHandle` element (equals `el` itself by default).
5974 initialize: function (element, dragStartTarget, preventOutline, options) {
5975 setOptions(this, options);
5976
5977 this._element = element;
5978 this._dragStartTarget = dragStartTarget || element;
5979 this._preventOutline = preventOutline;
5980 },
5981
5982 // @method enable()
5983 // Enables the dragging ability
5984 enable: function () {
5985 if (this._enabled) { return; }
5986
5987 on(this._dragStartTarget, START, this._onDown, this);
5988
5989 this._enabled = true;
5990 },
5991
5992 // @method disable()
5993 // Disables the dragging ability
5994 disable: function () {
5995 if (!this._enabled) { return; }
5996
5997 // If we're currently dragging this draggable,
5998 // disabling it counts as first ending the drag.
5999 if (Draggable._dragging === this) {
6000 this.finishDrag(true);
6001 }
6002
6003 off(this._dragStartTarget, START, this._onDown, this);
6004
6005 this._enabled = false;
6006 this._moved = false;
6007 },
6008
6009 _onDown: function (e) {
6010 // Ignore the event if disabled; this happens in IE11
6011 // under some circumstances, see #3666.
6012 if (!this._enabled) { return; }
6013
6014 this._moved = false;
6015
6016 if (hasClass(this._element, 'leaflet-zoom-anim')) { return; }
6017
6018 if (e.touches && e.touches.length !== 1) {
6019 // Finish dragging to avoid conflict with touchZoom
6020 if (Draggable._dragging === this) {
6021 this.finishDrag();
6022 }
6023 return;
6024 }
6025
6026 if (Draggable._dragging || e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; }
6027 Draggable._dragging = this; // Prevent dragging multiple objects at once.
6028
6029 if (this._preventOutline) {
6030 preventOutline(this._element);
6031 }
6032
6033 disableImageDrag();
6034 disableTextSelection();
6035
6036 if (this._moving) { return; }
6037
6038 // @event down: Event
6039 // Fired when a drag is about to start.
6040 this.fire('down');
6041
6042 var first = e.touches ? e.touches[0] : e,
6043 sizedParent = getSizedParentNode(this._element);
6044
6045 this._startPoint = new Point(first.clientX, first.clientY);
6046 this._startPos = getPosition(this._element);
6047
6048 // Cache the scale, so that we can continuously compensate for it during drag (_onMove).
6049 this._parentScale = getScale(sizedParent);
6050
6051 var mouseevent = e.type === 'mousedown';
6052 on(document, mouseevent ? 'mousemove' : 'touchmove', this._onMove, this);
6053 on(document, mouseevent ? 'mouseup' : 'touchend touchcancel', this._onUp, this);
6054 },
6055
6056 _onMove: function (e) {
6057 // Ignore the event if disabled; this happens in IE11
6058 // under some circumstances, see #3666.
6059 if (!this._enabled) { return; }
6060
6061 if (e.touches && e.touches.length > 1) {
6062 this._moved = true;
6063 return;
6064 }
6065
6066 var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
6067 offset = new Point(first.clientX, first.clientY)._subtract(this._startPoint);
6068
6069 if (!offset.x && !offset.y) { return; }
6070 if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) { return; }
6071
6072 // We assume that the parent container's position, border and scale do not change for the duration of the drag.
6073 // Therefore there is no need to account for the position and border (they are eliminated by the subtraction)
6074 // and we can use the cached value for the scale.
6075 offset.x /= this._parentScale.x;
6076 offset.y /= this._parentScale.y;
6077
6078 preventDefault(e);
6079
6080 if (!this._moved) {
6081 // @event dragstart: Event
6082 // Fired when a drag starts
6083 this.fire('dragstart');
6084
6085 this._moved = true;
6086
6087 addClass(document.body, 'leaflet-dragging');
6088
6089 this._lastTarget = e.target || e.srcElement;
6090 // IE and Edge do not give the <use> element, so fetch it
6091 // if necessary
6092 if (window.SVGElementInstance && this._lastTarget instanceof window.SVGElementInstance) {
6093 this._lastTarget = this._lastTarget.correspondingUseElement;
6094 }
6095 addClass(this._lastTarget, 'leaflet-drag-target');
6096 }
6097
6098 this._newPos = this._startPos.add(offset);
6099 this._moving = true;
6100
6101 this._lastEvent = e;
6102 this._updatePosition();
6103 },
6104
6105 _updatePosition: function () {
6106 var e = {originalEvent: this._lastEvent};
6107
6108 // @event predrag: Event
6109 // Fired continuously during dragging *before* each corresponding
6110 // update of the element's position.
6111 this.fire('predrag', e);
6112 setPosition(this._element, this._newPos);
6113
6114 // @event drag: Event
6115 // Fired continuously during dragging.
6116 this.fire('drag', e);
6117 },
6118
6119 _onUp: function () {
6120 // Ignore the event if disabled; this happens in IE11
6121 // under some circumstances, see #3666.
6122 if (!this._enabled) { return; }
6123 this.finishDrag();
6124 },
6125
6126 finishDrag: function (noInertia) {
6127 removeClass(document.body, 'leaflet-dragging');
6128
6129 if (this._lastTarget) {
6130 removeClass(this._lastTarget, 'leaflet-drag-target');
6131 this._lastTarget = null;
6132 }
6133
6134 off(document, 'mousemove touchmove', this._onMove, this);
6135 off(document, 'mouseup touchend touchcancel', this._onUp, this);
6136
6137 enableImageDrag();
6138 enableTextSelection();
6139
6140 if (this._moved && this._moving) {
6141
6142 // @event dragend: DragEndEvent
6143 // Fired when the drag ends.
6144 this.fire('dragend', {
6145 noInertia: noInertia,
6146 distance: this._newPos.distanceTo(this._startPos)
6147 });
6148 }
6149
6150 this._moving = false;
6151 Draggable._dragging = false;
6152 }
6153
6154 });
6155
6156 /*
6157 * @namespace LineUtil
6158 *
6159 * Various utility functions for polyline points processing, used by Leaflet internally to make polylines lightning-fast.
6160 */
6161
6162 // Simplify polyline with vertex reduction and Douglas-Peucker simplification.
6163 // Improves rendering performance dramatically by lessening the number of points to draw.
6164
6165 // @function simplify(points: Point[], tolerance: Number): Point[]
6166 // Dramatically reduces the number of points in a polyline while retaining
6167 // its shape and returns a new array of simplified points, using the
6168 // [Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm).
6169 // Used for a huge performance boost when processing/displaying Leaflet polylines for
6170 // each zoom level and also reducing visual noise. tolerance affects the amount of
6171 // simplification (lesser value means higher quality but slower and with more points).
6172 // Also released as a separated micro-library [Simplify.js](https://mourner.github.io/simplify-js/).
6173 function simplify(points, tolerance) {
6174 if (!tolerance || !points.length) {
6175 return points.slice();
6176 }
6177
6178 var sqTolerance = tolerance * tolerance;
6179
6180 // stage 1: vertex reduction
6181 points = _reducePoints(points, sqTolerance);
6182
6183 // stage 2: Douglas-Peucker simplification
6184 points = _simplifyDP(points, sqTolerance);
6185
6186 return points;
6187 }
6188
6189 // @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number
6190 // Returns the distance between point `p` and segment `p1` to `p2`.
6191 function pointToSegmentDistance(p, p1, p2) {
6192 return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true));
6193 }
6194
6195 // @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number
6196 // Returns the closest point from a point `p` on a segment `p1` to `p2`.
6197 function closestPointOnSegment(p, p1, p2) {
6198 return _sqClosestPointOnSegment(p, p1, p2);
6199 }
6200
6201 // Ramer-Douglas-Peucker simplification, see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
6202 function _simplifyDP(points, sqTolerance) {
6203
6204 var len = points.length,
6205 ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array,
6206 markers = new ArrayConstructor(len);
6207
6208 markers[0] = markers[len - 1] = 1;
6209
6210 _simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
6211
6212 var i,
6213 newPoints = [];
6214
6215 for (i = 0; i < len; i++) {
6216 if (markers[i]) {
6217 newPoints.push(points[i]);
6218 }
6219 }
6220
6221 return newPoints;
6222 }
6223
6224 function _simplifyDPStep(points, markers, sqTolerance, first, last) {
6225
6226 var maxSqDist = 0,
6227 index, i, sqDist;
6228
6229 for (i = first + 1; i <= last - 1; i++) {
6230 sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true);
6231
6232 if (sqDist > maxSqDist) {
6233 index = i;
6234 maxSqDist = sqDist;
6235 }
6236 }
6237
6238 if (maxSqDist > sqTolerance) {
6239 markers[index] = 1;
6240
6241 _simplifyDPStep(points, markers, sqTolerance, first, index);
6242 _simplifyDPStep(points, markers, sqTolerance, index, last);
6243 }
6244 }
6245
6246 // reduce points that are too close to each other to a single point
6247 function _reducePoints(points, sqTolerance) {
6248 var reducedPoints = [points[0]];
6249
6250 for (var i = 1, prev = 0, len = points.length; i < len; i++) {
6251 if (_sqDist(points[i], points[prev]) > sqTolerance) {
6252 reducedPoints.push(points[i]);
6253 prev = i;
6254 }
6255 }
6256 if (prev < len - 1) {
6257 reducedPoints.push(points[len - 1]);
6258 }
6259 return reducedPoints;
6260 }
6261
6262 var _lastCode;
6263
6264 // @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean
6265 // Clips the segment a to b by rectangular bounds with the
6266 // [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm)
6267 // (modifying the segment points directly!). Used by Leaflet to only show polyline
6268 // points that are on the screen or near, increasing performance.
6269 function clipSegment(a, b, bounds, useLastCode, round) {
6270 var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds),
6271 codeB = _getBitCode(b, bounds),
6272
6273 codeOut, p, newCode;
6274
6275 // save 2nd code to avoid calculating it on the next segment
6276 _lastCode = codeB;
6277
6278 while (true) {
6279 // if a,b is inside the clip window (trivial accept)
6280 if (!(codeA | codeB)) {
6281 return [a, b];
6282 }
6283
6284 // if a,b is outside the clip window (trivial reject)
6285 if (codeA & codeB) {
6286 return false;
6287 }
6288
6289 // other cases
6290 codeOut = codeA || codeB;
6291 p = _getEdgeIntersection(a, b, codeOut, bounds, round);
6292 newCode = _getBitCode(p, bounds);
6293
6294 if (codeOut === codeA) {
6295 a = p;
6296 codeA = newCode;
6297 } else {
6298 b = p;
6299 codeB = newCode;
6300 }
6301 }
6302 }
6303
6304 function _getEdgeIntersection(a, b, code, bounds, round) {
6305 var dx = b.x - a.x,
6306 dy = b.y - a.y,
6307 min = bounds.min,
6308 max = bounds.max,
6309 x, y;
6310
6311 if (code & 8) { // top
6312 x = a.x + dx * (max.y - a.y) / dy;
6313 y = max.y;
6314
6315 } else if (code & 4) { // bottom
6316 x = a.x + dx * (min.y - a.y) / dy;
6317 y = min.y;
6318
6319 } else if (code & 2) { // right
6320 x = max.x;
6321 y = a.y + dy * (max.x - a.x) / dx;
6322
6323 } else if (code & 1) { // left
6324 x = min.x;
6325 y = a.y + dy * (min.x - a.x) / dx;
6326 }
6327
6328 return new Point(x, y, round);
6329 }
6330
6331 function _getBitCode(p, bounds) {
6332 var code = 0;
6333
6334 if (p.x < bounds.min.x) { // left
6335 code |= 1;
6336 } else if (p.x > bounds.max.x) { // right
6337 code |= 2;
6338 }
6339
6340 if (p.y < bounds.min.y) { // bottom
6341 code |= 4;
6342 } else if (p.y > bounds.max.y) { // top
6343 code |= 8;
6344 }
6345
6346 return code;
6347 }
6348
6349 // square distance (to avoid unnecessary Math.sqrt calls)
6350 function _sqDist(p1, p2) {
6351 var dx = p2.x - p1.x,
6352 dy = p2.y - p1.y;
6353 return dx * dx + dy * dy;
6354 }
6355
6356 // return closest point on segment or distance to that point
6357 function _sqClosestPointOnSegment(p, p1, p2, sqDist) {
6358 var x = p1.x,
6359 y = p1.y,
6360 dx = p2.x - x,
6361 dy = p2.y - y,
6362 dot = dx * dx + dy * dy,
6363 t;
6364
6365 if (dot > 0) {
6366 t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
6367
6368 if (t > 1) {
6369 x = p2.x;
6370 y = p2.y;
6371 } else if (t > 0) {
6372 x += dx * t;
6373 y += dy * t;
6374 }
6375 }
6376
6377 dx = p.x - x;
6378 dy = p.y - y;
6379
6380 return sqDist ? dx * dx + dy * dy : new Point(x, y);
6381 }
6382
6383
6384 // @function isFlat(latlngs: LatLng[]): Boolean
6385 // Returns true if `latlngs` is a flat array, false is nested.
6386 function isFlat(latlngs) {
6387 return !isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
6388 }
6389
6390 function _flat(latlngs) {
6391 console.warn('Deprecated use of _flat, please use L.LineUtil.isFlat instead.');
6392 return isFlat(latlngs);
6393 }
6394
6395 /* @function polylineCenter(latlngs: LatLng[], crs: CRS): LatLng
6396 * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the passed LatLngs (first ring) from a polyline.
6397 */
6398 function polylineCenter(latlngs, crs) {
6399 var i, halfDist, segDist, dist, p1, p2, ratio, center;
6400
6401 if (!latlngs || latlngs.length === 0) {
6402 throw new Error('latlngs not passed');
6403 }
6404
6405 if (!isFlat(latlngs)) {
6406 console.warn('latlngs are not flat! Only the first ring will be used');
6407 latlngs = latlngs[0];
6408 }
6409
6410 var points = [];
6411 for (var j in latlngs) {
6412 points.push(crs.project(toLatLng(latlngs[j])));
6413 }
6414
6415 var len = points.length;
6416
6417 for (i = 0, halfDist = 0; i < len - 1; i++) {
6418 halfDist += points[i].distanceTo(points[i + 1]) / 2;
6419 }
6420
6421 // The line is so small in the current view that all points are on the same pixel.
6422 if (halfDist === 0) {
6423 center = points[0];
6424 } else {
6425 for (i = 0, dist = 0; i < len - 1; i++) {
6426 p1 = points[i];
6427 p2 = points[i + 1];
6428 segDist = p1.distanceTo(p2);
6429 dist += segDist;
6430
6431 if (dist > halfDist) {
6432 ratio = (dist - halfDist) / segDist;
6433 center = [
6434 p2.x - ratio * (p2.x - p1.x),
6435 p2.y - ratio * (p2.y - p1.y)
6436 ];
6437 break;
6438 }
6439 }
6440 }
6441 return crs.unproject(toPoint(center));
6442 }
6443
6444 var LineUtil = {
6445 __proto__: null,
6446 simplify: simplify,
6447 pointToSegmentDistance: pointToSegmentDistance,
6448 closestPointOnSegment: closestPointOnSegment,
6449 clipSegment: clipSegment,
6450 _getEdgeIntersection: _getEdgeIntersection,
6451 _getBitCode: _getBitCode,
6452 _sqClosestPointOnSegment: _sqClosestPointOnSegment,
6453 isFlat: isFlat,
6454 _flat: _flat,
6455 polylineCenter: polylineCenter
6456 };
6457
6458 /*
6459 * @namespace PolyUtil
6460 * Various utility functions for polygon geometries.
6461 */
6462
6463 /* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[]
6464 * Clips the polygon geometry defined by the given `points` by the given bounds (using the [Sutherland-Hodgman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)).
6465 * Used by Leaflet to only show polygon points that are on the screen or near, increasing
6466 * performance. Note that polygon points needs different algorithm for clipping
6467 * than polyline, so there's a separate method for it.
6468 */
6469 function clipPolygon(points, bounds, round) {
6470 var clippedPoints,
6471 edges = [1, 4, 2, 8],
6472 i, j, k,
6473 a, b,
6474 len, edge, p;
6475
6476 for (i = 0, len = points.length; i < len; i++) {
6477 points[i]._code = _getBitCode(points[i], bounds);
6478 }
6479
6480 // for each edge (left, bottom, right, top)
6481 for (k = 0; k < 4; k++) {
6482 edge = edges[k];
6483 clippedPoints = [];
6484
6485 for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
6486 a = points[i];
6487 b = points[j];
6488
6489 // if a is inside the clip window
6490 if (!(a._code & edge)) {
6491 // if b is outside the clip window (a->b goes out of screen)
6492 if (b._code & edge) {
6493 p = _getEdgeIntersection(b, a, edge, bounds, round);
6494 p._code = _getBitCode(p, bounds);
6495 clippedPoints.push(p);
6496 }
6497 clippedPoints.push(a);
6498
6499 // else if b is inside the clip window (a->b enters the screen)
6500 } else if (!(b._code & edge)) {
6501 p = _getEdgeIntersection(b, a, edge, bounds, round);
6502 p._code = _getBitCode(p, bounds);
6503 clippedPoints.push(p);
6504 }
6505 }
6506 points = clippedPoints;
6507 }
6508
6509 return points;
6510 }
6511
6512 /* @function polygonCenter(latlngs: LatLng[] crs: CRS): LatLng
6513 * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the passed LatLngs (first ring) from a polygon.
6514 */
6515 function polygonCenter(latlngs, crs) {
6516 var i, j, p1, p2, f, area, x, y, center;
6517
6518 if (!latlngs || latlngs.length === 0) {
6519 throw new Error('latlngs not passed');
6520 }
6521
6522 if (!isFlat(latlngs)) {
6523 console.warn('latlngs are not flat! Only the first ring will be used');
6524 latlngs = latlngs[0];
6525 }
6526
6527 var points = [];
6528 for (var k in latlngs) {
6529 points.push(crs.project(toLatLng(latlngs[k])));
6530 }
6531
6532 var len = points.length;
6533 area = x = y = 0;
6534
6535 // polygon centroid algorithm;
6536 for (i = 0, j = len - 1; i < len; j = i++) {
6537 p1 = points[i];
6538 p2 = points[j];
6539
6540 f = p1.y * p2.x - p2.y * p1.x;
6541 x += (p1.x + p2.x) * f;
6542 y += (p1.y + p2.y) * f;
6543 area += f * 3;
6544 }
6545
6546 if (area === 0) {
6547 // Polygon is so small that all points are on same pixel.
6548 center = points[0];
6549 } else {
6550 center = [x / area, y / area];
6551 }
6552 return crs.unproject(toPoint(center));
6553 }
6554
6555 var PolyUtil = {
6556 __proto__: null,
6557 clipPolygon: clipPolygon,
6558 polygonCenter: polygonCenter
6559 };
6560
6561 /*
6562 * @namespace Projection
6563 * @section
6564 * Leaflet comes with a set of already defined Projections out of the box:
6565 *
6566 * @projection L.Projection.LonLat
6567 *
6568 * Equirectangular, or Plate Carree projection — the most simple projection,
6569 * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as
6570 * latitude. Also suitable for flat worlds, e.g. game maps. Used by the
6571 * `EPSG:4326` and `Simple` CRS.
6572 */
6573
6574 var LonLat = {
6575 project: function (latlng) {
6576 return new Point(latlng.lng, latlng.lat);
6577 },
6578
6579 unproject: function (point) {
6580 return new LatLng(point.y, point.x);
6581 },
6582
6583 bounds: new Bounds([-180, -90], [180, 90])
6584 };
6585
6586 /*
6587 * @namespace Projection
6588 * @projection L.Projection.Mercator
6589 *
6590 * Elliptical Mercator projection — more complex than Spherical Mercator. Assumes that Earth is an ellipsoid. Used by the EPSG:3395 CRS.
6591 */
6592
6593 var Mercator = {
6594 R: 6378137,
6595 R_MINOR: 6356752.314245179,
6596
6597 bounds: new Bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]),
6598
6599 project: function (latlng) {
6600 var d = Math.PI / 180,
6601 r = this.R,
6602 y = latlng.lat * d,
6603 tmp = this.R_MINOR / r,
6604 e = Math.sqrt(1 - tmp * tmp),
6605 con = e * Math.sin(y);
6606
6607 var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2);
6608 y = -r * Math.log(Math.max(ts, 1E-10));
6609
6610 return new Point(latlng.lng * d * r, y);
6611 },
6612
6613 unproject: function (point) {
6614 var d = 180 / Math.PI,
6615 r = this.R,
6616 tmp = this.R_MINOR / r,
6617 e = Math.sqrt(1 - tmp * tmp),
6618 ts = Math.exp(-point.y / r),
6619 phi = Math.PI / 2 - 2 * Math.atan(ts);
6620
6621 for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) {
6622 con = e * Math.sin(phi);
6623 con = Math.pow((1 - con) / (1 + con), e / 2);
6624 dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi;
6625 phi += dphi;
6626 }
6627
6628 return new LatLng(phi * d, point.x * d / r);
6629 }
6630 };
6631
6632 /*
6633 * @class Projection
6634
6635 * An object with methods for projecting geographical coordinates of the world onto
6636 * a flat surface (and back). See [Map projection](https://en.wikipedia.org/wiki/Map_projection).
6637
6638 * @property bounds: Bounds
6639 * The bounds (specified in CRS units) where the projection is valid
6640
6641 * @method project(latlng: LatLng): Point
6642 * Projects geographical coordinates into a 2D point.
6643 * Only accepts actual `L.LatLng` instances, not arrays.
6644
6645 * @method unproject(point: Point): LatLng
6646 * The inverse of `project`. Projects a 2D point into a geographical location.
6647 * Only accepts actual `L.Point` instances, not arrays.
6648
6649 * Note that the projection instances do not inherit from Leaflet's `Class` object,
6650 * and can't be instantiated. Also, new classes can't inherit from them,
6651 * and methods can't be added to them with the `include` function.
6652
6653 */
6654
6655 var index = {
6656 __proto__: null,
6657 LonLat: LonLat,
6658 Mercator: Mercator,
6659 SphericalMercator: SphericalMercator
6660 };
6661
6662 /*
6663 * @namespace CRS
6664 * @crs L.CRS.EPSG3395
6665 *
6666 * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection.
6667 */
6668 var EPSG3395 = extend({}, Earth, {
6669 code: 'EPSG:3395',
6670 projection: Mercator,
6671
6672 transformation: (function () {
6673 var scale = 0.5 / (Math.PI * Mercator.R);
6674 return toTransformation(scale, 0.5, -scale, 0.5);
6675 }())
6676 });
6677
6678 /*
6679 * @namespace CRS
6680 * @crs L.CRS.EPSG4326
6681 *
6682 * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection.
6683 *
6684 * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic),
6685 * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer`
6686 * with this CRS, ensure that there are two 256x256 pixel tiles covering the
6687 * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90),
6688 * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set.
6689 */
6690
6691 var EPSG4326 = extend({}, Earth, {
6692 code: 'EPSG:4326',
6693 projection: LonLat,
6694 transformation: toTransformation(1 / 180, 1, -1 / 180, 0.5)
6695 });
6696
6697 /*
6698 * @namespace CRS
6699 * @crs L.CRS.Simple
6700 *
6701 * A simple CRS that maps longitude and latitude into `x` and `y` directly.
6702 * May be used for maps of flat surfaces (e.g. game maps). Note that the `y`
6703 * axis should still be inverted (going from bottom to top). `distance()` returns
6704 * simple euclidean distance.
6705 */
6706
6707 var Simple = extend({}, CRS, {
6708 projection: LonLat,
6709 transformation: toTransformation(1, 0, -1, 0),
6710
6711 scale: function (zoom) {
6712 return Math.pow(2, zoom);
6713 },
6714
6715 zoom: function (scale) {
6716 return Math.log(scale) / Math.LN2;
6717 },
6718
6719 distance: function (latlng1, latlng2) {
6720 var dx = latlng2.lng - latlng1.lng,
6721 dy = latlng2.lat - latlng1.lat;
6722
6723 return Math.sqrt(dx * dx + dy * dy);
6724 },
6725
6726 infinite: true
6727 });
6728
6729 CRS.Earth = Earth;
6730 CRS.EPSG3395 = EPSG3395;
6731 CRS.EPSG3857 = EPSG3857;
6732 CRS.EPSG900913 = EPSG900913;
6733 CRS.EPSG4326 = EPSG4326;
6734 CRS.Simple = Simple;
6735
6736 /*
6737 * @class Layer
6738 * @inherits Evented
6739 * @aka L.Layer
6740 * @aka ILayer
6741 *
6742 * A set of methods from the Layer base class that all Leaflet layers use.
6743 * Inherits all methods, options and events from `L.Evented`.
6744 *
6745 * @example
6746 *
6747 * ```js
6748 * var layer = L.marker(latlng).addTo(map);
6749 * layer.addTo(map);
6750 * layer.remove();
6751 * ```
6752 *
6753 * @event add: Event
6754 * Fired after the layer is added to a map
6755 *
6756 * @event remove: Event
6757 * Fired after the layer is removed from a map
6758 */
6759
6760
6761 var Layer = Evented.extend({
6762
6763 // Classes extending `L.Layer` will inherit the following options:
6764 options: {
6765 // @option pane: String = 'overlayPane'
6766 // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.
6767 pane: 'overlayPane',
6768
6769 // @option attribution: String = null
6770 // String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers.
6771 attribution: null,
6772
6773 bubblingMouseEvents: true
6774 },
6775
6776 /* @section
6777 * Classes extending `L.Layer` will inherit the following methods:
6778 *
6779 * @method addTo(map: Map|LayerGroup): this
6780 * Adds the layer to the given map or layer group.
6781 */
6782 addTo: function (map) {
6783 map.addLayer(this);
6784 return this;
6785 },
6786
6787 // @method remove: this
6788 // Removes the layer from the map it is currently active on.
6789 remove: function () {
6790 return this.removeFrom(this._map || this._mapToAdd);
6791 },
6792
6793 // @method removeFrom(map: Map): this
6794 // Removes the layer from the given map
6795 //
6796 // @alternative
6797 // @method removeFrom(group: LayerGroup): this
6798 // Removes the layer from the given `LayerGroup`
6799 removeFrom: function (obj) {
6800 if (obj) {
6801 obj.removeLayer(this);
6802 }
6803 return this;
6804 },
6805
6806 // @method getPane(name? : String): HTMLElement
6807 // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer.
6808 getPane: function (name) {
6809 return this._map.getPane(name ? (this.options[name] || name) : this.options.pane);
6810 },
6811
6812 addInteractiveTarget: function (targetEl) {
6813 this._map._targets[stamp(targetEl)] = this;
6814 return this;
6815 },
6816
6817 removeInteractiveTarget: function (targetEl) {
6818 delete this._map._targets[stamp(targetEl)];
6819 return this;
6820 },
6821
6822 // @method getAttribution: String
6823 // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
6824 getAttribution: function () {
6825 return this.options.attribution;
6826 },
6827
6828 _layerAdd: function (e) {
6829 var map = e.target;
6830
6831 // check in case layer gets added and then removed before the map is ready
6832 if (!map.hasLayer(this)) { return; }
6833
6834 this._map = map;
6835 this._zoomAnimated = map._zoomAnimated;
6836
6837 if (this.getEvents) {
6838 var events = this.getEvents();
6839 map.on(events, this);
6840 this.once('remove', function () {
6841 map.off(events, this);
6842 }, this);
6843 }
6844
6845 this.onAdd(map);
6846
6847 this.fire('add');
6848 map.fire('layeradd', {layer: this});
6849 }
6850 });
6851
6852 /* @section Extension methods
6853 * @uninheritable
6854 *
6855 * Every layer should extend from `L.Layer` and (re-)implement the following methods.
6856 *
6857 * @method onAdd(map: Map): this
6858 * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer).
6859 *
6860 * @method onRemove(map: Map): this
6861 * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer).
6862 *
6863 * @method getEvents(): Object
6864 * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer.
6865 *
6866 * @method getAttribution(): String
6867 * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.
6868 *
6869 * @method beforeAdd(map: Map): this
6870 * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only.
6871 */
6872
6873
6874 /* @namespace Map
6875 * @section Layer events
6876 *
6877 * @event layeradd: LayerEvent
6878 * Fired when a new layer is added to the map.
6879 *
6880 * @event layerremove: LayerEvent
6881 * Fired when some layer is removed from the map
6882 *
6883 * @section Methods for Layers and Controls
6884 */
6885 Map.include({
6886 // @method addLayer(layer: Layer): this
6887 // Adds the given layer to the map
6888 addLayer: function (layer) {
6889 if (!layer._layerAdd) {
6890 throw new Error('The provided object is not a Layer.');
6891 }
6892
6893 var id = stamp(layer);
6894 if (this._layers[id]) { return this; }
6895 this._layers[id] = layer;
6896
6897 layer._mapToAdd = this;
6898
6899 if (layer.beforeAdd) {
6900 layer.beforeAdd(this);
6901 }
6902
6903 this.whenReady(layer._layerAdd, layer);
6904
6905 return this;
6906 },
6907
6908 // @method removeLayer(layer: Layer): this
6909 // Removes the given layer from the map.
6910 removeLayer: function (layer) {
6911 var id = stamp(layer);
6912
6913 if (!this._layers[id]) { return this; }
6914
6915 if (this._loaded) {
6916 layer.onRemove(this);
6917 }
6918
6919 delete this._layers[id];
6920
6921 if (this._loaded) {
6922 this.fire('layerremove', {layer: layer});
6923 layer.fire('remove');
6924 }
6925
6926 layer._map = layer._mapToAdd = null;
6927
6928 return this;
6929 },
6930
6931 // @method hasLayer(layer: Layer): Boolean
6932 // Returns `true` if the given layer is currently added to the map
6933 hasLayer: function (layer) {
6934 return stamp(layer) in this._layers;
6935 },
6936
6937 /* @method eachLayer(fn: Function, context?: Object): this
6938 * Iterates over the layers of the map, optionally specifying context of the iterator function.
6939 * ```
6940 * map.eachLayer(function(layer){
6941 * layer.bindPopup('Hello');
6942 * });
6943 * ```
6944 */
6945 eachLayer: function (method, context) {
6946 for (var i in this._layers) {
6947 method.call(context, this._layers[i]);
6948 }
6949 return this;
6950 },
6951
6952 _addLayers: function (layers) {
6953 layers = layers ? (isArray(layers) ? layers : [layers]) : [];
6954
6955 for (var i = 0, len = layers.length; i < len; i++) {
6956 this.addLayer(layers[i]);
6957 }
6958 },
6959
6960 _addZoomLimit: function (layer) {
6961 if (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {
6962 this._zoomBoundLayers[stamp(layer)] = layer;
6963 this._updateZoomLevels();
6964 }
6965 },
6966
6967 _removeZoomLimit: function (layer) {
6968 var id = stamp(layer);
6969
6970 if (this._zoomBoundLayers[id]) {
6971 delete this._zoomBoundLayers[id];
6972 this._updateZoomLevels();
6973 }
6974 },
6975
6976 _updateZoomLevels: function () {
6977 var minZoom = Infinity,
6978 maxZoom = -Infinity,
6979 oldZoomSpan = this._getZoomSpan();
6980
6981 for (var i in this._zoomBoundLayers) {
6982 var options = this._zoomBoundLayers[i].options;
6983
6984 minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom);
6985 maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom);
6986 }
6987
6988 this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom;
6989 this._layersMinZoom = minZoom === Infinity ? undefined : minZoom;
6990
6991 // @section Map state change events
6992 // @event zoomlevelschange: Event
6993 // Fired when the number of zoomlevels on the map is changed due
6994 // to adding or removing a layer.
6995 if (oldZoomSpan !== this._getZoomSpan()) {
6996 this.fire('zoomlevelschange');
6997 }
6998
6999 if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
7000 this.setZoom(this._layersMaxZoom);
7001 }
7002 if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
7003 this.setZoom(this._layersMinZoom);
7004 }
7005 }
7006 });
7007
7008 /*
7009 * @class LayerGroup
7010 * @aka L.LayerGroup
7011 * @inherits Interactive layer
7012 *
7013 * Used to group several layers and handle them as one. If you add it to the map,
7014 * any layers added or removed from the group will be added/removed on the map as
7015 * well. Extends `Layer`.
7016 *
7017 * @example
7018 *
7019 * ```js
7020 * L.layerGroup([marker1, marker2])
7021 * .addLayer(polyline)
7022 * .addTo(map);
7023 * ```
7024 */
7025
7026 var LayerGroup = Layer.extend({
7027
7028 initialize: function (layers, options) {
7029 setOptions(this, options);
7030
7031 this._layers = {};
7032
7033 var i, len;
7034
7035 if (layers) {
7036 for (i = 0, len = layers.length; i < len; i++) {
7037 this.addLayer(layers[i]);
7038 }
7039 }
7040 },
7041
7042 // @method addLayer(layer: Layer): this
7043 // Adds the given layer to the group.
7044 addLayer: function (layer) {
7045 var id = this.getLayerId(layer);
7046
7047 this._layers[id] = layer;
7048
7049 if (this._map) {
7050 this._map.addLayer(layer);
7051 }
7052
7053 return this;
7054 },
7055
7056 // @method removeLayer(layer: Layer): this
7057 // Removes the given layer from the group.
7058 // @alternative
7059 // @method removeLayer(id: Number): this
7060 // Removes the layer with the given internal ID from the group.
7061 removeLayer: function (layer) {
7062 var id = layer in this._layers ? layer : this.getLayerId(layer);
7063
7064 if (this._map && this._layers[id]) {
7065 this._map.removeLayer(this._layers[id]);
7066 }
7067
7068 delete this._layers[id];
7069
7070 return this;
7071 },
7072
7073 // @method hasLayer(layer: Layer): Boolean
7074 // Returns `true` if the given layer is currently added to the group.
7075 // @alternative
7076 // @method hasLayer(id: Number): Boolean
7077 // Returns `true` if the given internal ID is currently added to the group.
7078 hasLayer: function (layer) {
7079 var layerId = typeof layer === 'number' ? layer : this.getLayerId(layer);
7080 return layerId in this._layers;
7081 },
7082
7083 // @method clearLayers(): this
7084 // Removes all the layers from the group.
7085 clearLayers: function () {
7086 return this.eachLayer(this.removeLayer, this);
7087 },
7088
7089 // @method invoke(methodName: String, …): this
7090 // Calls `methodName` on every layer contained in this group, passing any
7091 // additional parameters. Has no effect if the layers contained do not
7092 // implement `methodName`.
7093 invoke: function (methodName) {
7094 var args = Array.prototype.slice.call(arguments, 1),
7095 i, layer;
7096
7097 for (i in this._layers) {
7098 layer = this._layers[i];
7099
7100 if (layer[methodName]) {
7101 layer[methodName].apply(layer, args);
7102 }
7103 }
7104
7105 return this;
7106 },
7107
7108 onAdd: function (map) {
7109 this.eachLayer(map.addLayer, map);
7110 },
7111
7112 onRemove: function (map) {
7113 this.eachLayer(map.removeLayer, map);
7114 },
7115
7116 // @method eachLayer(fn: Function, context?: Object): this
7117 // Iterates over the layers of the group, optionally specifying context of the iterator function.
7118 // ```js
7119 // group.eachLayer(function (layer) {
7120 // layer.bindPopup('Hello');
7121 // });
7122 // ```
7123 eachLayer: function (method, context) {
7124 for (var i in this._layers) {
7125 method.call(context, this._layers[i]);
7126 }
7127 return this;
7128 },
7129
7130 // @method getLayer(id: Number): Layer
7131 // Returns the layer with the given internal ID.
7132 getLayer: function (id) {
7133 return this._layers[id];
7134 },
7135
7136 // @method getLayers(): Layer[]
7137 // Returns an array of all the layers added to the group.
7138 getLayers: function () {
7139 var layers = [];
7140 this.eachLayer(layers.push, layers);
7141 return layers;
7142 },
7143
7144 // @method setZIndex(zIndex: Number): this
7145 // Calls `setZIndex` on every layer contained in this group, passing the z-index.
7146 setZIndex: function (zIndex) {
7147 return this.invoke('setZIndex', zIndex);
7148 },
7149
7150 // @method getLayerId(layer: Layer): Number
7151 // Returns the internal ID for a layer
7152 getLayerId: function (layer) {
7153 return stamp(layer);
7154 }
7155 });
7156
7157
7158 // @factory L.layerGroup(layers?: Layer[], options?: Object)
7159 // Create a layer group, optionally given an initial set of layers and an `options` object.
7160 var layerGroup = function (layers, options) {
7161 return new LayerGroup(layers, options);
7162 };
7163
7164 /*
7165 * @class FeatureGroup
7166 * @aka L.FeatureGroup
7167 * @inherits LayerGroup
7168 *
7169 * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers:
7170 * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip))
7171 * * Events are propagated to the `FeatureGroup`, so if the group has an event
7172 * handler, it will handle events from any of the layers. This includes mouse events
7173 * and custom events.
7174 * * Has `layeradd` and `layerremove` events
7175 *
7176 * @example
7177 *
7178 * ```js
7179 * L.featureGroup([marker1, marker2, polyline])
7180 * .bindPopup('Hello world!')
7181 * .on('click', function() { alert('Clicked on a member of the group!'); })
7182 * .addTo(map);
7183 * ```
7184 */
7185
7186 var FeatureGroup = LayerGroup.extend({
7187
7188 addLayer: function (layer) {
7189 if (this.hasLayer(layer)) {
7190 return this;
7191 }
7192
7193 layer.addEventParent(this);
7194
7195 LayerGroup.prototype.addLayer.call(this, layer);
7196
7197 // @event layeradd: LayerEvent
7198 // Fired when a layer is added to this `FeatureGroup`
7199 return this.fire('layeradd', {layer: layer});
7200 },
7201
7202 removeLayer: function (layer) {
7203 if (!this.hasLayer(layer)) {
7204 return this;
7205 }
7206 if (layer in this._layers) {
7207 layer = this._layers[layer];
7208 }
7209
7210 layer.removeEventParent(this);
7211
7212 LayerGroup.prototype.removeLayer.call(this, layer);
7213
7214 // @event layerremove: LayerEvent
7215 // Fired when a layer is removed from this `FeatureGroup`
7216 return this.fire('layerremove', {layer: layer});
7217 },
7218
7219 // @method setStyle(style: Path options): this
7220 // Sets the given path options to each layer of the group that has a `setStyle` method.
7221 setStyle: function (style) {
7222 return this.invoke('setStyle', style);
7223 },
7224
7225 // @method bringToFront(): this
7226 // Brings the layer group to the top of all other layers
7227 bringToFront: function () {
7228 return this.invoke('bringToFront');
7229 },
7230
7231 // @method bringToBack(): this
7232 // Brings the layer group to the back of all other layers
7233 bringToBack: function () {
7234 return this.invoke('bringToBack');
7235 },
7236
7237 // @method getBounds(): LatLngBounds
7238 // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children).
7239 getBounds: function () {
7240 var bounds = new LatLngBounds();
7241
7242 for (var id in this._layers) {
7243 var layer = this._layers[id];
7244 bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
7245 }
7246 return bounds;
7247 }
7248 });
7249
7250 // @factory L.featureGroup(layers?: Layer[], options?: Object)
7251 // Create a feature group, optionally given an initial set of layers and an `options` object.
7252 var featureGroup = function (layers, options) {
7253 return new FeatureGroup(layers, options);
7254 };
7255
7256 /*
7257 * @class Icon
7258 * @aka L.Icon
7259 *
7260 * Represents an icon to provide when creating a marker.
7261 *
7262 * @example
7263 *
7264 * ```js
7265 * var myIcon = L.icon({
7266 * iconUrl: 'my-icon.png',
7267 * iconRetinaUrl: 'my-icon@2x.png',
7268 * iconSize: [38, 95],
7269 * iconAnchor: [22, 94],
7270 * popupAnchor: [-3, -76],
7271 * shadowUrl: 'my-icon-shadow.png',
7272 * shadowRetinaUrl: 'my-icon-shadow@2x.png',
7273 * shadowSize: [68, 95],
7274 * shadowAnchor: [22, 94]
7275 * });
7276 *
7277 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
7278 * ```
7279 *
7280 * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
7281 *
7282 */
7283
7284 var Icon = Class.extend({
7285
7286 /* @section
7287 * @aka Icon options
7288 *
7289 * @option iconUrl: String = null
7290 * **(required)** The URL to the icon image (absolute or relative to your script path).
7291 *
7292 * @option iconRetinaUrl: String = null
7293 * The URL to a retina sized version of the icon image (absolute or relative to your
7294 * script path). Used for Retina screen devices.
7295 *
7296 * @option iconSize: Point = null
7297 * Size of the icon image in pixels.
7298 *
7299 * @option iconAnchor: Point = null
7300 * The coordinates of the "tip" of the icon (relative to its top left corner). The icon
7301 * will be aligned so that this point is at the marker's geographical location. Centered
7302 * by default if size is specified, also can be set in CSS with negative margins.
7303 *
7304 * @option popupAnchor: Point = [0, 0]
7305 * The coordinates of the point from which popups will "open", relative to the icon anchor.
7306 *
7307 * @option tooltipAnchor: Point = [0, 0]
7308 * The coordinates of the point from which tooltips will "open", relative to the icon anchor.
7309 *
7310 * @option shadowUrl: String = null
7311 * The URL to the icon shadow image. If not specified, no shadow image will be created.
7312 *
7313 * @option shadowRetinaUrl: String = null
7314 *
7315 * @option shadowSize: Point = null
7316 * Size of the shadow image in pixels.
7317 *
7318 * @option shadowAnchor: Point = null
7319 * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
7320 * as iconAnchor if not specified).
7321 *
7322 * @option className: String = ''
7323 * A custom class name to assign to both icon and shadow images. Empty by default.
7324 */
7325
7326 options: {
7327 popupAnchor: [0, 0],
7328 tooltipAnchor: [0, 0],
7329
7330 // @option crossOrigin: Boolean|String = false
7331 // Whether the crossOrigin attribute will be added to the tiles.
7332 // If a String is provided, all tiles will have their crossOrigin attribute set to the String provided. This is needed if you want to access tile pixel data.
7333 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
7334 crossOrigin: false
7335 },
7336
7337 initialize: function (options) {
7338 setOptions(this, options);
7339 },
7340
7341 // @method createIcon(oldIcon?: HTMLElement): HTMLElement
7342 // Called internally when the icon has to be shown, returns a `<img>` HTML element
7343 // styled according to the options.
7344 createIcon: function (oldIcon) {
7345 return this._createIcon('icon', oldIcon);
7346 },
7347
7348 // @method createShadow(oldIcon?: HTMLElement): HTMLElement
7349 // As `createIcon`, but for the shadow beneath it.
7350 createShadow: function (oldIcon) {
7351 return this._createIcon('shadow', oldIcon);
7352 },
7353
7354 _createIcon: function (name, oldIcon) {
7355 var src = this._getIconUrl(name);
7356
7357 if (!src) {
7358 if (name === 'icon') {
7359 throw new Error('iconUrl not set in Icon options (see the docs).');
7360 }
7361 return null;
7362 }
7363
7364 var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
7365 this._setIconStyles(img, name);
7366
7367 if (this.options.crossOrigin || this.options.crossOrigin === '') {
7368 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
7369 }
7370
7371 return img;
7372 },
7373
7374 _setIconStyles: function (img, name) {
7375 var options = this.options;
7376 var sizeOption = options[name + 'Size'];
7377
7378 if (typeof sizeOption === 'number') {
7379 sizeOption = [sizeOption, sizeOption];
7380 }
7381
7382 var size = toPoint(sizeOption),
7383 anchor = toPoint(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
7384 size && size.divideBy(2, true));
7385
7386 img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
7387
7388 if (anchor) {
7389 img.style.marginLeft = (-anchor.x) + 'px';
7390 img.style.marginTop = (-anchor.y) + 'px';
7391 }
7392
7393 if (size) {
7394 img.style.width = size.x + 'px';
7395 img.style.height = size.y + 'px';
7396 }
7397 },
7398
7399 _createImg: function (src, el) {
7400 el = el || document.createElement('img');
7401 el.src = src;
7402 return el;
7403 },
7404
7405 _getIconUrl: function (name) {
7406 return Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
7407 }
7408 });
7409
7410
7411 // @factory L.icon(options: Icon options)
7412 // Creates an icon instance with the given options.
7413 function icon(options) {
7414 return new Icon(options);
7415 }
7416
7417 /*
7418 * @miniclass Icon.Default (Icon)
7419 * @aka L.Icon.Default
7420 * @section
7421 *
7422 * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
7423 * no icon is specified. Points to the blue marker image distributed with Leaflet
7424 * releases.
7425 *
7426 * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options`
7427 * (which is a set of `Icon options`).
7428 *
7429 * If you want to _completely_ replace the default icon, override the
7430 * `L.Marker.prototype.options.icon` with your own icon instead.
7431 */
7432
7433 var IconDefault = Icon.extend({
7434
7435 options: {
7436 iconUrl: 'marker-icon.png',
7437 iconRetinaUrl: 'marker-icon-2x.png',
7438 shadowUrl: 'marker-shadow.png',
7439 iconSize: [25, 41],
7440 iconAnchor: [12, 41],
7441 popupAnchor: [1, -34],
7442 tooltipAnchor: [16, -28],
7443 shadowSize: [41, 41]
7444 },
7445
7446 _getIconUrl: function (name) {
7447 if (typeof IconDefault.imagePath !== 'string') { // Deprecated, backwards-compatibility only
7448 IconDefault.imagePath = this._detectIconPath();
7449 }
7450
7451 // @option imagePath: String
7452 // `Icon.Default` will try to auto-detect the location of the
7453 // blue icon images. If you are placing these images in a non-standard
7454 // way, set this option to point to the right path.
7455 return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name);
7456 },
7457
7458 _stripUrl: function (path) { // separate function to use in tests
7459 var strip = function (str, re, idx) {
7460 var match = re.exec(str);
7461 return match && match[idx];
7462 };
7463 path = strip(path, /^url\((['"])?(.+)\1\)$/, 2);
7464 return path && strip(path, /^(.*)marker-icon\.png$/, 1);
7465 },
7466
7467 _detectIconPath: function () {
7468 var el = create$1('div', 'leaflet-default-icon-path', document.body);
7469 var path = getStyle(el, 'background-image') ||
7470 getStyle(el, 'backgroundImage'); // IE8
7471
7472 document.body.removeChild(el);
7473 path = this._stripUrl(path);
7474 if (path) { return path; }
7475 var link = document.querySelector('link[href$="leaflet.css"]');
7476 if (!link) { return ''; }
7477 return link.href.substring(0, link.href.length - 'leaflet.css'.length - 1);
7478 }
7479 });
7480
7481 /*
7482 * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
7483 */
7484
7485
7486 /* @namespace Marker
7487 * @section Interaction handlers
7488 *
7489 * Interaction handlers are properties of a marker instance that allow you to control interaction behavior in runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example:
7490 *
7491 * ```js
7492 * marker.dragging.disable();
7493 * ```
7494 *
7495 * @property dragging: Handler
7496 * Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)).
7497 */
7498
7499 var MarkerDrag = Handler.extend({
7500 initialize: function (marker) {
7501 this._marker = marker;
7502 },
7503
7504 addHooks: function () {
7505 var icon = this._marker._icon;
7506
7507 if (!this._draggable) {
7508 this._draggable = new Draggable(icon, icon, true);
7509 }
7510
7511 this._draggable.on({
7512 dragstart: this._onDragStart,
7513 predrag: this._onPreDrag,
7514 drag: this._onDrag,
7515 dragend: this._onDragEnd
7516 }, this).enable();
7517
7518 addClass(icon, 'leaflet-marker-draggable');
7519 },
7520
7521 removeHooks: function () {
7522 this._draggable.off({
7523 dragstart: this._onDragStart,
7524 predrag: this._onPreDrag,
7525 drag: this._onDrag,
7526 dragend: this._onDragEnd
7527 }, this).disable();
7528
7529 if (this._marker._icon) {
7530 removeClass(this._marker._icon, 'leaflet-marker-draggable');
7531 }
7532 },
7533
7534 moved: function () {
7535 return this._draggable && this._draggable._moved;
7536 },
7537
7538 _adjustPan: function (e) {
7539 var marker = this._marker,
7540 map = marker._map,
7541 speed = this._marker.options.autoPanSpeed,
7542 padding = this._marker.options.autoPanPadding,
7543 iconPos = getPosition(marker._icon),
7544 bounds = map.getPixelBounds(),
7545 origin = map.getPixelOrigin();
7546
7547 var panBounds = toBounds(
7548 bounds.min._subtract(origin).add(padding),
7549 bounds.max._subtract(origin).subtract(padding)
7550 );
7551
7552 if (!panBounds.contains(iconPos)) {
7553 // Compute incremental movement
7554 var movement = toPoint(
7555 (Math.max(panBounds.max.x, iconPos.x) - panBounds.max.x) / (bounds.max.x - panBounds.max.x) -
7556 (Math.min(panBounds.min.x, iconPos.x) - panBounds.min.x) / (bounds.min.x - panBounds.min.x),
7557
7558 (Math.max(panBounds.max.y, iconPos.y) - panBounds.max.y) / (bounds.max.y - panBounds.max.y) -
7559 (Math.min(panBounds.min.y, iconPos.y) - panBounds.min.y) / (bounds.min.y - panBounds.min.y)
7560 ).multiplyBy(speed);
7561
7562 map.panBy(movement, {animate: false});
7563
7564 this._draggable._newPos._add(movement);
7565 this._draggable._startPos._add(movement);
7566
7567 setPosition(marker._icon, this._draggable._newPos);
7568 this._onDrag(e);
7569
7570 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
7571 }
7572 },
7573
7574 _onDragStart: function () {
7575 // @section Dragging events
7576 // @event dragstart: Event
7577 // Fired when the user starts dragging the marker.
7578
7579 // @event movestart: Event
7580 // Fired when the marker starts moving (because of dragging).
7581
7582 this._oldLatLng = this._marker.getLatLng();
7583
7584 // When using ES6 imports it could not be set when `Popup` was not imported as well
7585 this._marker.closePopup && this._marker.closePopup();
7586
7587 this._marker
7588 .fire('movestart')
7589 .fire('dragstart');
7590 },
7591
7592 _onPreDrag: function (e) {
7593 if (this._marker.options.autoPan) {
7594 cancelAnimFrame(this._panRequest);
7595 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
7596 }
7597 },
7598
7599 _onDrag: function (e) {
7600 var marker = this._marker,
7601 shadow = marker._shadow,
7602 iconPos = getPosition(marker._icon),
7603 latlng = marker._map.layerPointToLatLng(iconPos);
7604
7605 // update shadow position
7606 if (shadow) {
7607 setPosition(shadow, iconPos);
7608 }
7609
7610 marker._latlng = latlng;
7611 e.latlng = latlng;
7612 e.oldLatLng = this._oldLatLng;
7613
7614 // @event drag: Event
7615 // Fired repeatedly while the user drags the marker.
7616 marker
7617 .fire('move', e)
7618 .fire('drag', e);
7619 },
7620
7621 _onDragEnd: function (e) {
7622 // @event dragend: DragEndEvent
7623 // Fired when the user stops dragging the marker.
7624
7625 cancelAnimFrame(this._panRequest);
7626
7627 // @event moveend: Event
7628 // Fired when the marker stops moving (because of dragging).
7629 delete this._oldLatLng;
7630 this._marker
7631 .fire('moveend')
7632 .fire('dragend', e);
7633 }
7634 });
7635
7636 /*
7637 * @class Marker
7638 * @inherits Interactive layer
7639 * @aka L.Marker
7640 * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
7641 *
7642 * @example
7643 *
7644 * ```js
7645 * L.marker([50.5, 30.5]).addTo(map);
7646 * ```
7647 */
7648
7649 var Marker = Layer.extend({
7650
7651 // @section
7652 // @aka Marker options
7653 options: {
7654 // @option icon: Icon = *
7655 // Icon instance to use for rendering the marker.
7656 // See [Icon documentation](#L.Icon) for details on how to customize the marker icon.
7657 // If not specified, a common instance of `L.Icon.Default` is used.
7658 icon: new IconDefault(),
7659
7660 // Option inherited from "Interactive layer" abstract class
7661 interactive: true,
7662
7663 // @option keyboard: Boolean = true
7664 // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
7665 keyboard: true,
7666
7667 // @option title: String = ''
7668 // Text for the browser tooltip that appear on marker hover (no tooltip by default).
7669 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
7670 title: '',
7671
7672 // @option alt: String = 'Marker'
7673 // Text for the `alt` attribute of the icon image.
7674 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
7675 alt: 'Marker',
7676
7677 // @option zIndexOffset: Number = 0
7678 // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
7679 zIndexOffset: 0,
7680
7681 // @option opacity: Number = 1.0
7682 // The opacity of the marker.
7683 opacity: 1,
7684
7685 // @option riseOnHover: Boolean = false
7686 // If `true`, the marker will get on top of others when you hover the mouse over it.
7687 riseOnHover: false,
7688
7689 // @option riseOffset: Number = 250
7690 // The z-index offset used for the `riseOnHover` feature.
7691 riseOffset: 250,
7692
7693 // @option pane: String = 'markerPane'
7694 // `Map pane` where the markers icon will be added.
7695 pane: 'markerPane',
7696
7697 // @option shadowPane: String = 'shadowPane'
7698 // `Map pane` where the markers shadow will be added.
7699 shadowPane: 'shadowPane',
7700
7701 // @option bubblingMouseEvents: Boolean = false
7702 // When `true`, a mouse event on this marker will trigger the same event on the map
7703 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
7704 bubblingMouseEvents: false,
7705
7706 // @option autoPanOnFocus: Boolean = true
7707 // When `true`, the map will pan whenever the marker is focused (via
7708 // e.g. pressing `tab` on the keyboard) to ensure the marker is
7709 // visible within the map's bounds
7710 autoPanOnFocus: true,
7711
7712 // @section Draggable marker options
7713 // @option draggable: Boolean = false
7714 // Whether the marker is draggable with mouse/touch or not.
7715 draggable: false,
7716
7717 // @option autoPan: Boolean = false
7718 // Whether to pan the map when dragging this marker near its edge or not.
7719 autoPan: false,
7720
7721 // @option autoPanPadding: Point = Point(50, 50)
7722 // Distance (in pixels to the left/right and to the top/bottom) of the
7723 // map edge to start panning the map.
7724 autoPanPadding: [50, 50],
7725
7726 // @option autoPanSpeed: Number = 10
7727 // Number of pixels the map should pan by.
7728 autoPanSpeed: 10
7729 },
7730
7731 /* @section
7732 *
7733 * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
7734 */
7735
7736 initialize: function (latlng, options) {
7737 setOptions(this, options);
7738 this._latlng = toLatLng(latlng);
7739 },
7740
7741 onAdd: function (map) {
7742 this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
7743
7744 if (this._zoomAnimated) {
7745 map.on('zoomanim', this._animateZoom, this);
7746 }
7747
7748 this._initIcon();
7749 this.update();
7750 },
7751
7752 onRemove: function (map) {
7753 if (this.dragging && this.dragging.enabled()) {
7754 this.options.draggable = true;
7755 this.dragging.removeHooks();
7756 }
7757 delete this.dragging;
7758
7759 if (this._zoomAnimated) {
7760 map.off('zoomanim', this._animateZoom, this);
7761 }
7762
7763 this._removeIcon();
7764 this._removeShadow();
7765 },
7766
7767 getEvents: function () {
7768 return {
7769 zoom: this.update,
7770 viewreset: this.update
7771 };
7772 },
7773
7774 // @method getLatLng: LatLng
7775 // Returns the current geographical position of the marker.
7776 getLatLng: function () {
7777 return this._latlng;
7778 },
7779
7780 // @method setLatLng(latlng: LatLng): this
7781 // Changes the marker position to the given point.
7782 setLatLng: function (latlng) {
7783 var oldLatLng = this._latlng;
7784 this._latlng = toLatLng(latlng);
7785 this.update();
7786
7787 // @event move: Event
7788 // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
7789 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
7790 },
7791
7792 // @method setZIndexOffset(offset: Number): this
7793 // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
7794 setZIndexOffset: function (offset) {
7795 this.options.zIndexOffset = offset;
7796 return this.update();
7797 },
7798
7799 // @method getIcon: Icon
7800 // Returns the current icon used by the marker
7801 getIcon: function () {
7802 return this.options.icon;
7803 },
7804
7805 // @method setIcon(icon: Icon): this
7806 // Changes the marker icon.
7807 setIcon: function (icon) {
7808
7809 this.options.icon = icon;
7810
7811 if (this._map) {
7812 this._initIcon();
7813 this.update();
7814 }
7815
7816 if (this._popup) {
7817 this.bindPopup(this._popup, this._popup.options);
7818 }
7819
7820 return this;
7821 },
7822
7823 getElement: function () {
7824 return this._icon;
7825 },
7826
7827 update: function () {
7828
7829 if (this._icon && this._map) {
7830 var pos = this._map.latLngToLayerPoint(this._latlng).round();
7831 this._setPos(pos);
7832 }
7833
7834 return this;
7835 },
7836
7837 _initIcon: function () {
7838 var options = this.options,
7839 classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
7840
7841 var icon = options.icon.createIcon(this._icon),
7842 addIcon = false;
7843
7844 // if we're not reusing the icon, remove the old one and init new one
7845 if (icon !== this._icon) {
7846 if (this._icon) {
7847 this._removeIcon();
7848 }
7849 addIcon = true;
7850
7851 if (options.title) {
7852 icon.title = options.title;
7853 }
7854
7855 if (icon.tagName === 'IMG') {
7856 icon.alt = options.alt || '';
7857 }
7858 }
7859
7860 addClass(icon, classToAdd);
7861
7862 if (options.keyboard) {
7863 icon.tabIndex = '0';
7864 icon.setAttribute('role', 'button');
7865 }
7866
7867 this._icon = icon;
7868
7869 if (options.riseOnHover) {
7870 this.on({
7871 mouseover: this._bringToFront,
7872 mouseout: this._resetZIndex
7873 });
7874 }
7875
7876 if (this.options.autoPanOnFocus) {
7877 on(icon, 'focus', this._panOnFocus, this);
7878 }
7879
7880 var newShadow = options.icon.createShadow(this._shadow),
7881 addShadow = false;
7882
7883 if (newShadow !== this._shadow) {
7884 this._removeShadow();
7885 addShadow = true;
7886 }
7887
7888 if (newShadow) {
7889 addClass(newShadow, classToAdd);
7890 newShadow.alt = '';
7891 }
7892 this._shadow = newShadow;
7893
7894
7895 if (options.opacity < 1) {
7896 this._updateOpacity();
7897 }
7898
7899
7900 if (addIcon) {
7901 this.getPane().appendChild(this._icon);
7902 }
7903 this._initInteraction();
7904 if (newShadow && addShadow) {
7905 this.getPane(options.shadowPane).appendChild(this._shadow);
7906 }
7907 },
7908
7909 _removeIcon: function () {
7910 if (this.options.riseOnHover) {
7911 this.off({
7912 mouseover: this._bringToFront,
7913 mouseout: this._resetZIndex
7914 });
7915 }
7916
7917 if (this.options.autoPanOnFocus) {
7918 off(this._icon, 'focus', this._panOnFocus, this);
7919 }
7920
7921 remove(this._icon);
7922 this.removeInteractiveTarget(this._icon);
7923
7924 this._icon = null;
7925 },
7926
7927 _removeShadow: function () {
7928 if (this._shadow) {
7929 remove(this._shadow);
7930 }
7931 this._shadow = null;
7932 },
7933
7934 _setPos: function (pos) {
7935
7936 if (this._icon) {
7937 setPosition(this._icon, pos);
7938 }
7939
7940 if (this._shadow) {
7941 setPosition(this._shadow, pos);
7942 }
7943
7944 this._zIndex = pos.y + this.options.zIndexOffset;
7945
7946 this._resetZIndex();
7947 },
7948
7949 _updateZIndex: function (offset) {
7950 if (this._icon) {
7951 this._icon.style.zIndex = this._zIndex + offset;
7952 }
7953 },
7954
7955 _animateZoom: function (opt) {
7956 var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
7957
7958 this._setPos(pos);
7959 },
7960
7961 _initInteraction: function () {
7962
7963 if (!this.options.interactive) { return; }
7964
7965 addClass(this._icon, 'leaflet-interactive');
7966
7967 this.addInteractiveTarget(this._icon);
7968
7969 if (MarkerDrag) {
7970 var draggable = this.options.draggable;
7971 if (this.dragging) {
7972 draggable = this.dragging.enabled();
7973 this.dragging.disable();
7974 }
7975
7976 this.dragging = new MarkerDrag(this);
7977
7978 if (draggable) {
7979 this.dragging.enable();
7980 }
7981 }
7982 },
7983
7984 // @method setOpacity(opacity: Number): this
7985 // Changes the opacity of the marker.
7986 setOpacity: function (opacity) {
7987 this.options.opacity = opacity;
7988 if (this._map) {
7989 this._updateOpacity();
7990 }
7991
7992 return this;
7993 },
7994
7995 _updateOpacity: function () {
7996 var opacity = this.options.opacity;
7997
7998 if (this._icon) {
7999 setOpacity(this._icon, opacity);
8000 }
8001
8002 if (this._shadow) {
8003 setOpacity(this._shadow, opacity);
8004 }
8005 },
8006
8007 _bringToFront: function () {
8008 this._updateZIndex(this.options.riseOffset);
8009 },
8010
8011 _resetZIndex: function () {
8012 this._updateZIndex(0);
8013 },
8014
8015 _panOnFocus: function () {
8016 var map = this._map;
8017 if (!map) { return; }
8018
8019 var iconOpts = this.options.icon.options;
8020 var size = iconOpts.iconSize ? toPoint(iconOpts.iconSize) : toPoint(0, 0);
8021 var anchor = iconOpts.iconAnchor ? toPoint(iconOpts.iconAnchor) : toPoint(0, 0);
8022
8023 map.panInside(this._latlng, {
8024 paddingTopLeft: anchor,
8025 paddingBottomRight: size.subtract(anchor)
8026 });
8027 },
8028
8029 _getPopupAnchor: function () {
8030 return this.options.icon.options.popupAnchor;
8031 },
8032
8033 _getTooltipAnchor: function () {
8034 return this.options.icon.options.tooltipAnchor;
8035 }
8036 });
8037
8038
8039 // factory L.marker(latlng: LatLng, options? : Marker options)
8040
8041 // @factory L.marker(latlng: LatLng, options? : Marker options)
8042 // Instantiates a Marker object given a geographical point and optionally an options object.
8043 function marker(latlng, options) {
8044 return new Marker(latlng, options);
8045 }
8046
8047 /*
8048 * @class Path
8049 * @aka L.Path
8050 * @inherits Interactive layer
8051 *
8052 * An abstract class that contains options and constants shared between vector
8053 * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`.
8054 */
8055
8056 var Path = Layer.extend({
8057
8058 // @section
8059 // @aka Path options
8060 options: {
8061 // @option stroke: Boolean = true
8062 // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles.
8063 stroke: true,
8064
8065 // @option color: String = '#3388ff'
8066 // Stroke color
8067 color: '#3388ff',
8068
8069 // @option weight: Number = 3
8070 // Stroke width in pixels
8071 weight: 3,
8072
8073 // @option opacity: Number = 1.0
8074 // Stroke opacity
8075 opacity: 1,
8076
8077 // @option lineCap: String= 'round'
8078 // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke.
8079 lineCap: 'round',
8080
8081 // @option lineJoin: String = 'round'
8082 // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke.
8083 lineJoin: 'round',
8084
8085 // @option dashArray: String = null
8086 // A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
8087 dashArray: null,
8088
8089 // @option dashOffset: String = null
8090 // A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
8091 dashOffset: null,
8092
8093 // @option fill: Boolean = depends
8094 // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles.
8095 fill: false,
8096
8097 // @option fillColor: String = *
8098 // Fill color. Defaults to the value of the [`color`](#path-color) option
8099 fillColor: null,
8100
8101 // @option fillOpacity: Number = 0.2
8102 // Fill opacity.
8103 fillOpacity: 0.2,
8104
8105 // @option fillRule: String = 'evenodd'
8106 // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined.
8107 fillRule: 'evenodd',
8108
8109 // className: '',
8110
8111 // Option inherited from "Interactive layer" abstract class
8112 interactive: true,
8113
8114 // @option bubblingMouseEvents: Boolean = true
8115 // When `true`, a mouse event on this path will trigger the same event on the map
8116 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
8117 bubblingMouseEvents: true
8118 },
8119
8120 beforeAdd: function (map) {
8121 // Renderer is set here because we need to call renderer.getEvents
8122 // before this.getEvents.
8123 this._renderer = map.getRenderer(this);
8124 },
8125
8126 onAdd: function () {
8127 this._renderer._initPath(this);
8128 this._reset();
8129 this._renderer._addPath(this);
8130 },
8131
8132 onRemove: function () {
8133 this._renderer._removePath(this);
8134 },
8135
8136 // @method redraw(): this
8137 // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses.
8138 redraw: function () {
8139 if (this._map) {
8140 this._renderer._updatePath(this);
8141 }
8142 return this;
8143 },
8144
8145 // @method setStyle(style: Path options): this
8146 // Changes the appearance of a Path based on the options in the `Path options` object.
8147 setStyle: function (style) {
8148 setOptions(this, style);
8149 if (this._renderer) {
8150 this._renderer._updateStyle(this);
8151 if (this.options.stroke && style && Object.prototype.hasOwnProperty.call(style, 'weight')) {
8152 this._updateBounds();
8153 }
8154 }
8155 return this;
8156 },
8157
8158 // @method bringToFront(): this
8159 // Brings the layer to the top of all path layers.
8160 bringToFront: function () {
8161 if (this._renderer) {
8162 this._renderer._bringToFront(this);
8163 }
8164 return this;
8165 },
8166
8167 // @method bringToBack(): this
8168 // Brings the layer to the bottom of all path layers.
8169 bringToBack: function () {
8170 if (this._renderer) {
8171 this._renderer._bringToBack(this);
8172 }
8173 return this;
8174 },
8175
8176 getElement: function () {
8177 return this._path;
8178 },
8179
8180 _reset: function () {
8181 // defined in child classes
8182 this._project();
8183 this._update();
8184 },
8185
8186 _clickTolerance: function () {
8187 // used when doing hit detection for Canvas layers
8188 return (this.options.stroke ? this.options.weight / 2 : 0) +
8189 (this._renderer.options.tolerance || 0);
8190 }
8191 });
8192
8193 /*
8194 * @class CircleMarker
8195 * @aka L.CircleMarker
8196 * @inherits Path
8197 *
8198 * A circle of a fixed size with radius specified in pixels. Extends `Path`.
8199 */
8200
8201 var CircleMarker = Path.extend({
8202
8203 // @section
8204 // @aka CircleMarker options
8205 options: {
8206 fill: true,
8207
8208 // @option radius: Number = 10
8209 // Radius of the circle marker, in pixels
8210 radius: 10
8211 },
8212
8213 initialize: function (latlng, options) {
8214 setOptions(this, options);
8215 this._latlng = toLatLng(latlng);
8216 this._radius = this.options.radius;
8217 },
8218
8219 // @method setLatLng(latLng: LatLng): this
8220 // Sets the position of a circle marker to a new location.
8221 setLatLng: function (latlng) {
8222 var oldLatLng = this._latlng;
8223 this._latlng = toLatLng(latlng);
8224 this.redraw();
8225
8226 // @event move: Event
8227 // Fired when the marker is moved via [`setLatLng`](#circlemarker-setlatlng). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
8228 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
8229 },
8230
8231 // @method getLatLng(): LatLng
8232 // Returns the current geographical position of the circle marker
8233 getLatLng: function () {
8234 return this._latlng;
8235 },
8236
8237 // @method setRadius(radius: Number): this
8238 // Sets the radius of a circle marker. Units are in pixels.
8239 setRadius: function (radius) {
8240 this.options.radius = this._radius = radius;
8241 return this.redraw();
8242 },
8243
8244 // @method getRadius(): Number
8245 // Returns the current radius of the circle
8246 getRadius: function () {
8247 return this._radius;
8248 },
8249
8250 setStyle : function (options) {
8251 var radius = options && options.radius || this._radius;
8252 Path.prototype.setStyle.call(this, options);
8253 this.setRadius(radius);
8254 return this;
8255 },
8256
8257 _project: function () {
8258 this._point = this._map.latLngToLayerPoint(this._latlng);
8259 this._updateBounds();
8260 },
8261
8262 _updateBounds: function () {
8263 var r = this._radius,
8264 r2 = this._radiusY || r,
8265 w = this._clickTolerance(),
8266 p = [r + w, r2 + w];
8267 this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p));
8268 },
8269
8270 _update: function () {
8271 if (this._map) {
8272 this._updatePath();
8273 }
8274 },
8275
8276 _updatePath: function () {
8277 this._renderer._updateCircle(this);
8278 },
8279
8280 _empty: function () {
8281 return this._radius && !this._renderer._bounds.intersects(this._pxBounds);
8282 },
8283
8284 // Needed by the `Canvas` renderer for interactivity
8285 _containsPoint: function (p) {
8286 return p.distanceTo(this._point) <= this._radius + this._clickTolerance();
8287 }
8288 });
8289
8290
8291 // @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options)
8292 // Instantiates a circle marker object given a geographical point, and an optional options object.
8293 function circleMarker(latlng, options) {
8294 return new CircleMarker(latlng, options);
8295 }
8296
8297 /*
8298 * @class Circle
8299 * @aka L.Circle
8300 * @inherits CircleMarker
8301 *
8302 * A class for drawing circle overlays on a map. Extends `CircleMarker`.
8303 *
8304 * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion).
8305 *
8306 * @example
8307 *
8308 * ```js
8309 * L.circle([50.5, 30.5], {radius: 200}).addTo(map);
8310 * ```
8311 */
8312
8313 var Circle = CircleMarker.extend({
8314
8315 initialize: function (latlng, options, legacyOptions) {
8316 if (typeof options === 'number') {
8317 // Backwards compatibility with 0.7.x factory (latlng, radius, options?)
8318 options = extend({}, legacyOptions, {radius: options});
8319 }
8320 setOptions(this, options);
8321 this._latlng = toLatLng(latlng);
8322
8323 if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); }
8324
8325 // @section
8326 // @aka Circle options
8327 // @option radius: Number; Radius of the circle, in meters.
8328 this._mRadius = this.options.radius;
8329 },
8330
8331 // @method setRadius(radius: Number): this
8332 // Sets the radius of a circle. Units are in meters.
8333 setRadius: function (radius) {
8334 this._mRadius = radius;
8335 return this.redraw();
8336 },
8337
8338 // @method getRadius(): Number
8339 // Returns the current radius of a circle. Units are in meters.
8340 getRadius: function () {
8341 return this._mRadius;
8342 },
8343
8344 // @method getBounds(): LatLngBounds
8345 // Returns the `LatLngBounds` of the path.
8346 getBounds: function () {
8347 var half = [this._radius, this._radiusY || this._radius];
8348
8349 return new LatLngBounds(
8350 this._map.layerPointToLatLng(this._point.subtract(half)),
8351 this._map.layerPointToLatLng(this._point.add(half)));
8352 },
8353
8354 setStyle: Path.prototype.setStyle,
8355
8356 _project: function () {
8357
8358 var lng = this._latlng.lng,
8359 lat = this._latlng.lat,
8360 map = this._map,
8361 crs = map.options.crs;
8362
8363 if (crs.distance === Earth.distance) {
8364 var d = Math.PI / 180,
8365 latR = (this._mRadius / Earth.R) / d,
8366 top = map.project([lat + latR, lng]),
8367 bottom = map.project([lat - latR, lng]),
8368 p = top.add(bottom).divideBy(2),
8369 lat2 = map.unproject(p).lat,
8370 lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) /
8371 (Math.cos(lat * d) * Math.cos(lat2 * d))) / d;
8372
8373 if (isNaN(lngR) || lngR === 0) {
8374 lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425
8375 }
8376
8377 this._point = p.subtract(map.getPixelOrigin());
8378 this._radius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x;
8379 this._radiusY = p.y - top.y;
8380
8381 } else {
8382 var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0]));
8383
8384 this._point = map.latLngToLayerPoint(this._latlng);
8385 this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x;
8386 }
8387
8388 this._updateBounds();
8389 }
8390 });
8391
8392 // @factory L.circle(latlng: LatLng, options?: Circle options)
8393 // Instantiates a circle object given a geographical point, and an options object
8394 // which contains the circle radius.
8395 // @alternative
8396 // @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options)
8397 // Obsolete way of instantiating a circle, for compatibility with 0.7.x code.
8398 // Do not use in new applications or plugins.
8399 function circle(latlng, options, legacyOptions) {
8400 return new Circle(latlng, options, legacyOptions);
8401 }
8402
8403 /*
8404 * @class Polyline
8405 * @aka L.Polyline
8406 * @inherits Path
8407 *
8408 * A class for drawing polyline overlays on a map. Extends `Path`.
8409 *
8410 * @example
8411 *
8412 * ```js
8413 * // create a red polyline from an array of LatLng points
8414 * var latlngs = [
8415 * [45.51, -122.68],
8416 * [37.77, -122.43],
8417 * [34.04, -118.2]
8418 * ];
8419 *
8420 * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
8421 *
8422 * // zoom the map to the polyline
8423 * map.fitBounds(polyline.getBounds());
8424 * ```
8425 *
8426 * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape:
8427 *
8428 * ```js
8429 * // create a red polyline from an array of arrays of LatLng points
8430 * var latlngs = [
8431 * [[45.51, -122.68],
8432 * [37.77, -122.43],
8433 * [34.04, -118.2]],
8434 * [[40.78, -73.91],
8435 * [41.83, -87.62],
8436 * [32.76, -96.72]]
8437 * ];
8438 * ```
8439 */
8440
8441
8442 var Polyline = Path.extend({
8443
8444 // @section
8445 // @aka Polyline options
8446 options: {
8447 // @option smoothFactor: Number = 1.0
8448 // How much to simplify the polyline on each zoom level. More means
8449 // better performance and smoother look, and less means more accurate representation.
8450 smoothFactor: 1.0,
8451
8452 // @option noClip: Boolean = false
8453 // Disable polyline clipping.
8454 noClip: false
8455 },
8456
8457 initialize: function (latlngs, options) {
8458 setOptions(this, options);
8459 this._setLatLngs(latlngs);
8460 },
8461
8462 // @method getLatLngs(): LatLng[]
8463 // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
8464 getLatLngs: function () {
8465 return this._latlngs;
8466 },
8467
8468 // @method setLatLngs(latlngs: LatLng[]): this
8469 // Replaces all the points in the polyline with the given array of geographical points.
8470 setLatLngs: function (latlngs) {
8471 this._setLatLngs(latlngs);
8472 return this.redraw();
8473 },
8474
8475 // @method isEmpty(): Boolean
8476 // Returns `true` if the Polyline has no LatLngs.
8477 isEmpty: function () {
8478 return !this._latlngs.length;
8479 },
8480
8481 // @method closestLayerPoint(p: Point): Point
8482 // Returns the point closest to `p` on the Polyline.
8483 closestLayerPoint: function (p) {
8484 var minDistance = Infinity,
8485 minPoint = null,
8486 closest = _sqClosestPointOnSegment,
8487 p1, p2;
8488
8489 for (var j = 0, jLen = this._parts.length; j < jLen; j++) {
8490 var points = this._parts[j];
8491
8492 for (var i = 1, len = points.length; i < len; i++) {
8493 p1 = points[i - 1];
8494 p2 = points[i];
8495
8496 var sqDist = closest(p, p1, p2, true);
8497
8498 if (sqDist < minDistance) {
8499 minDistance = sqDist;
8500 minPoint = closest(p, p1, p2);
8501 }
8502 }
8503 }
8504 if (minPoint) {
8505 minPoint.distance = Math.sqrt(minDistance);
8506 }
8507 return minPoint;
8508 },
8509
8510 // @method getCenter(): LatLng
8511 // Returns the center ([centroid](https://en.wikipedia.org/wiki/Centroid)) of the polyline.
8512 getCenter: function () {
8513 // throws error when not yet added to map as this center calculation requires projected coordinates
8514 if (!this._map) {
8515 throw new Error('Must add layer to map before using getCenter()');
8516 }
8517 return polylineCenter(this._defaultShape(), this._map.options.crs);
8518 },
8519
8520 // @method getBounds(): LatLngBounds
8521 // Returns the `LatLngBounds` of the path.
8522 getBounds: function () {
8523 return this._bounds;
8524 },
8525
8526 // @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this
8527 // Adds a given point to the polyline. By default, adds to the first ring of
8528 // the polyline in case of a multi-polyline, but can be overridden by passing
8529 // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)).
8530 addLatLng: function (latlng, latlngs) {
8531 latlngs = latlngs || this._defaultShape();
8532 latlng = toLatLng(latlng);
8533 latlngs.push(latlng);
8534 this._bounds.extend(latlng);
8535 return this.redraw();
8536 },
8537
8538 _setLatLngs: function (latlngs) {
8539 this._bounds = new LatLngBounds();
8540 this._latlngs = this._convertLatLngs(latlngs);
8541 },
8542
8543 _defaultShape: function () {
8544 return isFlat(this._latlngs) ? this._latlngs : this._latlngs[0];
8545 },
8546
8547 // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way
8548 _convertLatLngs: function (latlngs) {
8549 var result = [],
8550 flat = isFlat(latlngs);
8551
8552 for (var i = 0, len = latlngs.length; i < len; i++) {
8553 if (flat) {
8554 result[i] = toLatLng(latlngs[i]);
8555 this._bounds.extend(result[i]);
8556 } else {
8557 result[i] = this._convertLatLngs(latlngs[i]);
8558 }
8559 }
8560
8561 return result;
8562 },
8563
8564 _project: function () {
8565 var pxBounds = new Bounds();
8566 this._rings = [];
8567 this._projectLatlngs(this._latlngs, this._rings, pxBounds);
8568
8569 if (this._bounds.isValid() && pxBounds.isValid()) {
8570 this._rawPxBounds = pxBounds;
8571 this._updateBounds();
8572 }
8573 },
8574
8575 _updateBounds: function () {
8576 var w = this._clickTolerance(),
8577 p = new Point(w, w);
8578
8579 if (!this._rawPxBounds) {
8580 return;
8581 }
8582
8583 this._pxBounds = new Bounds([
8584 this._rawPxBounds.min.subtract(p),
8585 this._rawPxBounds.max.add(p)
8586 ]);
8587 },
8588
8589 // recursively turns latlngs into a set of rings with projected coordinates
8590 _projectLatlngs: function (latlngs, result, projectedBounds) {
8591 var flat = latlngs[0] instanceof LatLng,
8592 len = latlngs.length,
8593 i, ring;
8594
8595 if (flat) {
8596 ring = [];
8597 for (i = 0; i < len; i++) {
8598 ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
8599 projectedBounds.extend(ring[i]);
8600 }
8601 result.push(ring);
8602 } else {
8603 for (i = 0; i < len; i++) {
8604 this._projectLatlngs(latlngs[i], result, projectedBounds);
8605 }
8606 }
8607 },
8608
8609 // clip polyline by renderer bounds so that we have less to render for performance
8610 _clipPoints: function () {
8611 var bounds = this._renderer._bounds;
8612
8613 this._parts = [];
8614 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
8615 return;
8616 }
8617
8618 if (this.options.noClip) {
8619 this._parts = this._rings;
8620 return;
8621 }
8622
8623 var parts = this._parts,
8624 i, j, k, len, len2, segment, points;
8625
8626 for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
8627 points = this._rings[i];
8628
8629 for (j = 0, len2 = points.length; j < len2 - 1; j++) {
8630 segment = clipSegment(points[j], points[j + 1], bounds, j, true);
8631
8632 if (!segment) { continue; }
8633
8634 parts[k] = parts[k] || [];
8635 parts[k].push(segment[0]);
8636
8637 // if segment goes out of screen, or it's the last one, it's the end of the line part
8638 if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
8639 parts[k].push(segment[1]);
8640 k++;
8641 }
8642 }
8643 }
8644 },
8645
8646 // simplify each clipped part of the polyline for performance
8647 _simplifyPoints: function () {
8648 var parts = this._parts,
8649 tolerance = this.options.smoothFactor;
8650
8651 for (var i = 0, len = parts.length; i < len; i++) {
8652 parts[i] = simplify(parts[i], tolerance);
8653 }
8654 },
8655
8656 _update: function () {
8657 if (!this._map) { return; }
8658
8659 this._clipPoints();
8660 this._simplifyPoints();
8661 this._updatePath();
8662 },
8663
8664 _updatePath: function () {
8665 this._renderer._updatePoly(this);
8666 },
8667
8668 // Needed by the `Canvas` renderer for interactivity
8669 _containsPoint: function (p, closed) {
8670 var i, j, k, len, len2, part,
8671 w = this._clickTolerance();
8672
8673 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
8674
8675 // hit detection for polylines
8676 for (i = 0, len = this._parts.length; i < len; i++) {
8677 part = this._parts[i];
8678
8679 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
8680 if (!closed && (j === 0)) { continue; }
8681
8682 if (pointToSegmentDistance(p, part[k], part[j]) <= w) {
8683 return true;
8684 }
8685 }
8686 }
8687 return false;
8688 }
8689 });
8690
8691 // @factory L.polyline(latlngs: LatLng[], options?: Polyline options)
8692 // Instantiates a polyline object given an array of geographical points and
8693 // optionally an options object. You can create a `Polyline` object with
8694 // multiple separate lines (`MultiPolyline`) by passing an array of arrays
8695 // of geographic points.
8696 function polyline(latlngs, options) {
8697 return new Polyline(latlngs, options);
8698 }
8699
8700 // Retrocompat. Allow plugins to support Leaflet versions before and after 1.1.
8701 Polyline._flat = _flat;
8702
8703 /*
8704 * @class Polygon
8705 * @aka L.Polygon
8706 * @inherits Polyline
8707 *
8708 * A class for drawing polygon overlays on a map. Extends `Polyline`.
8709 *
8710 * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points.
8711 *
8712 *
8713 * @example
8714 *
8715 * ```js
8716 * // create a red polygon from an array of LatLng points
8717 * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
8718 *
8719 * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
8720 *
8721 * // zoom the map to the polygon
8722 * map.fitBounds(polygon.getBounds());
8723 * ```
8724 *
8725 * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape:
8726 *
8727 * ```js
8728 * var latlngs = [
8729 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
8730 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
8731 * ];
8732 * ```
8733 *
8734 * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape.
8735 *
8736 * ```js
8737 * var latlngs = [
8738 * [ // first polygon
8739 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
8740 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
8741 * ],
8742 * [ // second polygon
8743 * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
8744 * ]
8745 * ];
8746 * ```
8747 */
8748
8749 var Polygon = Polyline.extend({
8750
8751 options: {
8752 fill: true
8753 },
8754
8755 isEmpty: function () {
8756 return !this._latlngs.length || !this._latlngs[0].length;
8757 },
8758
8759 // @method getCenter(): LatLng
8760 // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the Polygon.
8761 getCenter: function () {
8762 // throws error when not yet added to map as this center calculation requires projected coordinates
8763 if (!this._map) {
8764 throw new Error('Must add layer to map before using getCenter()');
8765 }
8766 return polygonCenter(this._defaultShape(), this._map.options.crs);
8767 },
8768
8769 _convertLatLngs: function (latlngs) {
8770 var result = Polyline.prototype._convertLatLngs.call(this, latlngs),
8771 len = result.length;
8772
8773 // remove last point if it equals first one
8774 if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) {
8775 result.pop();
8776 }
8777 return result;
8778 },
8779
8780 _setLatLngs: function (latlngs) {
8781 Polyline.prototype._setLatLngs.call(this, latlngs);
8782 if (isFlat(this._latlngs)) {
8783 this._latlngs = [this._latlngs];
8784 }
8785 },
8786
8787 _defaultShape: function () {
8788 return isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0];
8789 },
8790
8791 _clipPoints: function () {
8792 // polygons need a different clipping algorithm so we redefine that
8793
8794 var bounds = this._renderer._bounds,
8795 w = this.options.weight,
8796 p = new Point(w, w);
8797
8798 // increase clip padding by stroke width to avoid stroke on clip edges
8799 bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p));
8800
8801 this._parts = [];
8802 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
8803 return;
8804 }
8805
8806 if (this.options.noClip) {
8807 this._parts = this._rings;
8808 return;
8809 }
8810
8811 for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
8812 clipped = clipPolygon(this._rings[i], bounds, true);
8813 if (clipped.length) {
8814 this._parts.push(clipped);
8815 }
8816 }
8817 },
8818
8819 _updatePath: function () {
8820 this._renderer._updatePoly(this, true);
8821 },
8822
8823 // Needed by the `Canvas` renderer for interactivity
8824 _containsPoint: function (p) {
8825 var inside = false,
8826 part, p1, p2, i, j, k, len, len2;
8827
8828 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
8829
8830 // ray casting algorithm for detecting if point is in polygon
8831 for (i = 0, len = this._parts.length; i < len; i++) {
8832 part = this._parts[i];
8833
8834 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
8835 p1 = part[j];
8836 p2 = part[k];
8837
8838 if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
8839 inside = !inside;
8840 }
8841 }
8842 }
8843
8844 // also check if it's on polygon stroke
8845 return inside || Polyline.prototype._containsPoint.call(this, p, true);
8846 }
8847
8848 });
8849
8850
8851 // @factory L.polygon(latlngs: LatLng[], options?: Polyline options)
8852 function polygon(latlngs, options) {
8853 return new Polygon(latlngs, options);
8854 }
8855
8856 /*
8857 * @class GeoJSON
8858 * @aka L.GeoJSON
8859 * @inherits FeatureGroup
8860 *
8861 * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse
8862 * GeoJSON data and display it on the map. Extends `FeatureGroup`.
8863 *
8864 * @example
8865 *
8866 * ```js
8867 * L.geoJSON(data, {
8868 * style: function (feature) {
8869 * return {color: feature.properties.color};
8870 * }
8871 * }).bindPopup(function (layer) {
8872 * return layer.feature.properties.description;
8873 * }).addTo(map);
8874 * ```
8875 */
8876
8877 var GeoJSON = FeatureGroup.extend({
8878
8879 /* @section
8880 * @aka GeoJSON options
8881 *
8882 * @option pointToLayer: Function = *
8883 * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally
8884 * called when data is added, passing the GeoJSON point feature and its `LatLng`.
8885 * The default is to spawn a default `Marker`:
8886 * ```js
8887 * function(geoJsonPoint, latlng) {
8888 * return L.marker(latlng);
8889 * }
8890 * ```
8891 *
8892 * @option style: Function = *
8893 * A `Function` defining the `Path options` for styling GeoJSON lines and polygons,
8894 * called internally when data is added.
8895 * The default value is to not override any defaults:
8896 * ```js
8897 * function (geoJsonFeature) {
8898 * return {}
8899 * }
8900 * ```
8901 *
8902 * @option onEachFeature: Function = *
8903 * A `Function` that will be called once for each created `Feature`, after it has
8904 * been created and styled. Useful for attaching events and popups to features.
8905 * The default is to do nothing with the newly created layers:
8906 * ```js
8907 * function (feature, layer) {}
8908 * ```
8909 *
8910 * @option filter: Function = *
8911 * A `Function` that will be used to decide whether to include a feature or not.
8912 * The default is to include all features:
8913 * ```js
8914 * function (geoJsonFeature) {
8915 * return true;
8916 * }
8917 * ```
8918 * Note: dynamically changing the `filter` option will have effect only on newly
8919 * added data. It will _not_ re-evaluate already included features.
8920 *
8921 * @option coordsToLatLng: Function = *
8922 * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s.
8923 * The default is the `coordsToLatLng` static method.
8924 *
8925 * @option markersInheritOptions: Boolean = false
8926 * Whether default Markers for "Point" type Features inherit from group options.
8927 */
8928
8929 initialize: function (geojson, options) {
8930 setOptions(this, options);
8931
8932 this._layers = {};
8933
8934 if (geojson) {
8935 this.addData(geojson);
8936 }
8937 },
8938
8939 // @method addData( <GeoJSON> data ): this
8940 // Adds a GeoJSON object to the layer.
8941 addData: function (geojson) {
8942 var features = isArray(geojson) ? geojson : geojson.features,
8943 i, len, feature;
8944
8945 if (features) {
8946 for (i = 0, len = features.length; i < len; i++) {
8947 // only add this if geometry or geometries are set and not null
8948 feature = features[i];
8949 if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
8950 this.addData(feature);
8951 }
8952 }
8953 return this;
8954 }
8955
8956 var options = this.options;
8957
8958 if (options.filter && !options.filter(geojson)) { return this; }
8959
8960 var layer = geometryToLayer(geojson, options);
8961 if (!layer) {
8962 return this;
8963 }
8964 layer.feature = asFeature(geojson);
8965
8966 layer.defaultOptions = layer.options;
8967 this.resetStyle(layer);
8968
8969 if (options.onEachFeature) {
8970 options.onEachFeature(geojson, layer);
8971 }
8972
8973 return this.addLayer(layer);
8974 },
8975
8976 // @method resetStyle( <Path> layer? ): this
8977 // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
8978 // If `layer` is omitted, the style of all features in the current layer is reset.
8979 resetStyle: function (layer) {
8980 if (layer === undefined) {
8981 return this.eachLayer(this.resetStyle, this);
8982 }
8983 // reset any custom styles
8984 layer.options = extend({}, layer.defaultOptions);
8985 this._setLayerStyle(layer, this.options.style);
8986 return this;
8987 },
8988
8989 // @method setStyle( <Function> style ): this
8990 // Changes styles of GeoJSON vector layers with the given style function.
8991 setStyle: function (style) {
8992 return this.eachLayer(function (layer) {
8993 this._setLayerStyle(layer, style);
8994 }, this);
8995 },
8996
8997 _setLayerStyle: function (layer, style) {
8998 if (layer.setStyle) {
8999 if (typeof style === 'function') {
9000 style = style(layer.feature);
9001 }
9002 layer.setStyle(style);
9003 }
9004 }
9005 });
9006
9007 // @section
9008 // There are several static functions which can be called without instantiating L.GeoJSON:
9009
9010 // @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer
9011 // Creates a `Layer` from a given GeoJSON feature. Can use a custom
9012 // [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng)
9013 // functions if provided as options.
9014 function geometryToLayer(geojson, options) {
9015
9016 var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
9017 coords = geometry ? geometry.coordinates : null,
9018 layers = [],
9019 pointToLayer = options && options.pointToLayer,
9020 _coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng,
9021 latlng, latlngs, i, len;
9022
9023 if (!coords && !geometry) {
9024 return null;
9025 }
9026
9027 switch (geometry.type) {
9028 case 'Point':
9029 latlng = _coordsToLatLng(coords);
9030 return _pointToLayer(pointToLayer, geojson, latlng, options);
9031
9032 case 'MultiPoint':
9033 for (i = 0, len = coords.length; i < len; i++) {
9034 latlng = _coordsToLatLng(coords[i]);
9035 layers.push(_pointToLayer(pointToLayer, geojson, latlng, options));
9036 }
9037 return new FeatureGroup(layers);
9038
9039 case 'LineString':
9040 case 'MultiLineString':
9041 latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng);
9042 return new Polyline(latlngs, options);
9043
9044 case 'Polygon':
9045 case 'MultiPolygon':
9046 latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng);
9047 return new Polygon(latlngs, options);
9048
9049 case 'GeometryCollection':
9050 for (i = 0, len = geometry.geometries.length; i < len; i++) {
9051 var geoLayer = geometryToLayer({
9052 geometry: geometry.geometries[i],
9053 type: 'Feature',
9054 properties: geojson.properties
9055 }, options);
9056
9057 if (geoLayer) {
9058 layers.push(geoLayer);
9059 }
9060 }
9061 return new FeatureGroup(layers);
9062
9063 case 'FeatureCollection':
9064 for (i = 0, len = geometry.features.length; i < len; i++) {
9065 var featureLayer = geometryToLayer(geometry.features[i], options);
9066
9067 if (featureLayer) {
9068 layers.push(featureLayer);
9069 }
9070 }
9071 return new FeatureGroup(layers);
9072
9073 default:
9074 throw new Error('Invalid GeoJSON object.');
9075 }
9076 }
9077
9078 function _pointToLayer(pointToLayerFn, geojson, latlng, options) {
9079 return pointToLayerFn ?
9080 pointToLayerFn(geojson, latlng) :
9081 new Marker(latlng, options && options.markersInheritOptions && options);
9082 }
9083
9084 // @function coordsToLatLng(coords: Array): LatLng
9085 // Creates a `LatLng` object from an array of 2 numbers (longitude, latitude)
9086 // or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points.
9087 function coordsToLatLng(coords) {
9088 return new LatLng(coords[1], coords[0], coords[2]);
9089 }
9090
9091 // @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array
9092 // Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array.
9093 // `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default).
9094 // Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function.
9095 function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) {
9096 var latlngs = [];
9097
9098 for (var i = 0, len = coords.length, latlng; i < len; i++) {
9099 latlng = levelsDeep ?
9100 coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) :
9101 (_coordsToLatLng || coordsToLatLng)(coords[i]);
9102
9103 latlngs.push(latlng);
9104 }
9105
9106 return latlngs;
9107 }
9108
9109 // @function latLngToCoords(latlng: LatLng, precision?: Number|false): Array
9110 // Reverse of [`coordsToLatLng`](#geojson-coordstolatlng)
9111 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
9112 function latLngToCoords(latlng, precision) {
9113 latlng = toLatLng(latlng);
9114 return latlng.alt !== undefined ?
9115 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision), formatNum(latlng.alt, precision)] :
9116 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision)];
9117 }
9118
9119 // @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean, precision?: Number|false): Array
9120 // Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs)
9121 // `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default.
9122 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
9123 function latLngsToCoords(latlngs, levelsDeep, closed, precision) {
9124 var coords = [];
9125
9126 for (var i = 0, len = latlngs.length; i < len; i++) {
9127 // Check for flat arrays required to ensure unbalanced arrays are correctly converted in recursion
9128 coords.push(levelsDeep ?
9129 latLngsToCoords(latlngs[i], isFlat(latlngs[i]) ? 0 : levelsDeep - 1, closed, precision) :
9130 latLngToCoords(latlngs[i], precision));
9131 }
9132
9133 if (!levelsDeep && closed) {
9134 coords.push(coords[0].slice());
9135 }
9136
9137 return coords;
9138 }
9139
9140 function getFeature(layer, newGeometry) {
9141 return layer.feature ?
9142 extend({}, layer.feature, {geometry: newGeometry}) :
9143 asFeature(newGeometry);
9144 }
9145
9146 // @function asFeature(geojson: Object): Object
9147 // Normalize GeoJSON geometries/features into GeoJSON features.
9148 function asFeature(geojson) {
9149 if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') {
9150 return geojson;
9151 }
9152
9153 return {
9154 type: 'Feature',
9155 properties: {},
9156 geometry: geojson
9157 };
9158 }
9159
9160 var PointToGeoJSON = {
9161 toGeoJSON: function (precision) {
9162 return getFeature(this, {
9163 type: 'Point',
9164 coordinates: latLngToCoords(this.getLatLng(), precision)
9165 });
9166 }
9167 };
9168
9169 // @namespace Marker
9170 // @section Other methods
9171 // @method toGeoJSON(precision?: Number|false): Object
9172 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9173 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature).
9174 Marker.include(PointToGeoJSON);
9175
9176 // @namespace CircleMarker
9177 // @method toGeoJSON(precision?: Number|false): Object
9178 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9179 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature).
9180 Circle.include(PointToGeoJSON);
9181 CircleMarker.include(PointToGeoJSON);
9182
9183
9184 // @namespace Polyline
9185 // @method toGeoJSON(precision?: Number|false): Object
9186 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9187 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature).
9188 Polyline.include({
9189 toGeoJSON: function (precision) {
9190 var multi = !isFlat(this._latlngs);
9191
9192 var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision);
9193
9194 return getFeature(this, {
9195 type: (multi ? 'Multi' : '') + 'LineString',
9196 coordinates: coords
9197 });
9198 }
9199 });
9200
9201 // @namespace Polygon
9202 // @method toGeoJSON(precision?: Number|false): Object
9203 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9204 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature).
9205 Polygon.include({
9206 toGeoJSON: function (precision) {
9207 var holes = !isFlat(this._latlngs),
9208 multi = holes && !isFlat(this._latlngs[0]);
9209
9210 var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision);
9211
9212 if (!holes) {
9213 coords = [coords];
9214 }
9215
9216 return getFeature(this, {
9217 type: (multi ? 'Multi' : '') + 'Polygon',
9218 coordinates: coords
9219 });
9220 }
9221 });
9222
9223
9224 // @namespace LayerGroup
9225 LayerGroup.include({
9226 toMultiPoint: function (precision) {
9227 var coords = [];
9228
9229 this.eachLayer(function (layer) {
9230 coords.push(layer.toGeoJSON(precision).geometry.coordinates);
9231 });
9232
9233 return getFeature(this, {
9234 type: 'MultiPoint',
9235 coordinates: coords
9236 });
9237 },
9238
9239 // @method toGeoJSON(precision?: Number|false): Object
9240 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9241 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`).
9242 toGeoJSON: function (precision) {
9243
9244 var type = this.feature && this.feature.geometry && this.feature.geometry.type;
9245
9246 if (type === 'MultiPoint') {
9247 return this.toMultiPoint(precision);
9248 }
9249
9250 var isGeometryCollection = type === 'GeometryCollection',
9251 jsons = [];
9252
9253 this.eachLayer(function (layer) {
9254 if (layer.toGeoJSON) {
9255 var json = layer.toGeoJSON(precision);
9256 if (isGeometryCollection) {
9257 jsons.push(json.geometry);
9258 } else {
9259 var feature = asFeature(json);
9260 // Squash nested feature collections
9261 if (feature.type === 'FeatureCollection') {
9262 jsons.push.apply(jsons, feature.features);
9263 } else {
9264 jsons.push(feature);
9265 }
9266 }
9267 }
9268 });
9269
9270 if (isGeometryCollection) {
9271 return getFeature(this, {
9272 geometries: jsons,
9273 type: 'GeometryCollection'
9274 });
9275 }
9276
9277 return {
9278 type: 'FeatureCollection',
9279 features: jsons
9280 };
9281 }
9282 });
9283
9284 // @namespace GeoJSON
9285 // @factory L.geoJSON(geojson?: Object, options?: GeoJSON options)
9286 // Creates a GeoJSON layer. Optionally accepts an object in
9287 // [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map
9288 // (you can alternatively add it later with `addData` method) and an `options` object.
9289 function geoJSON(geojson, options) {
9290 return new GeoJSON(geojson, options);
9291 }
9292
9293 // Backward compatibility.
9294 var geoJson = geoJSON;
9295
9296 /*
9297 * @class ImageOverlay
9298 * @aka L.ImageOverlay
9299 * @inherits Interactive layer
9300 *
9301 * Used to load and display a single image over specific bounds of the map. Extends `Layer`.
9302 *
9303 * @example
9304 *
9305 * ```js
9306 * var imageUrl = 'https://maps.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
9307 * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
9308 * L.imageOverlay(imageUrl, imageBounds).addTo(map);
9309 * ```
9310 */
9311
9312 var ImageOverlay = Layer.extend({
9313
9314 // @section
9315 // @aka ImageOverlay options
9316 options: {
9317 // @option opacity: Number = 1.0
9318 // The opacity of the image overlay.
9319 opacity: 1,
9320
9321 // @option alt: String = ''
9322 // Text for the `alt` attribute of the image (useful for accessibility).
9323 alt: '',
9324
9325 // @option interactive: Boolean = false
9326 // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
9327 interactive: false,
9328
9329 // @option crossOrigin: Boolean|String = false
9330 // Whether the crossOrigin attribute will be added to the image.
9331 // If a String is provided, the image will have its crossOrigin attribute set to the String provided. This is needed if you want to access image pixel data.
9332 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
9333 crossOrigin: false,
9334
9335 // @option errorOverlayUrl: String = ''
9336 // URL to the overlay image to show in place of the overlay that failed to load.
9337 errorOverlayUrl: '',
9338
9339 // @option zIndex: Number = 1
9340 // The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the overlay layer.
9341 zIndex: 1,
9342
9343 // @option className: String = ''
9344 // A custom class name to assign to the image. Empty by default.
9345 className: ''
9346 },
9347
9348 initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
9349 this._url = url;
9350 this._bounds = toLatLngBounds(bounds);
9351
9352 setOptions(this, options);
9353 },
9354
9355 onAdd: function () {
9356 if (!this._image) {
9357 this._initImage();
9358
9359 if (this.options.opacity < 1) {
9360 this._updateOpacity();
9361 }
9362 }
9363
9364 if (this.options.interactive) {
9365 addClass(this._image, 'leaflet-interactive');
9366 this.addInteractiveTarget(this._image);
9367 }
9368
9369 this.getPane().appendChild(this._image);
9370 this._reset();
9371 },
9372
9373 onRemove: function () {
9374 remove(this._image);
9375 if (this.options.interactive) {
9376 this.removeInteractiveTarget(this._image);
9377 }
9378 },
9379
9380 // @method setOpacity(opacity: Number): this
9381 // Sets the opacity of the overlay.
9382 setOpacity: function (opacity) {
9383 this.options.opacity = opacity;
9384
9385 if (this._image) {
9386 this._updateOpacity();
9387 }
9388 return this;
9389 },
9390
9391 setStyle: function (styleOpts) {
9392 if (styleOpts.opacity) {
9393 this.setOpacity(styleOpts.opacity);
9394 }
9395 return this;
9396 },
9397
9398 // @method bringToFront(): this
9399 // Brings the layer to the top of all overlays.
9400 bringToFront: function () {
9401 if (this._map) {
9402 toFront(this._image);
9403 }
9404 return this;
9405 },
9406
9407 // @method bringToBack(): this
9408 // Brings the layer to the bottom of all overlays.
9409 bringToBack: function () {
9410 if (this._map) {
9411 toBack(this._image);
9412 }
9413 return this;
9414 },
9415
9416 // @method setUrl(url: String): this
9417 // Changes the URL of the image.
9418 setUrl: function (url) {
9419 this._url = url;
9420
9421 if (this._image) {
9422 this._image.src = url;
9423 }
9424 return this;
9425 },
9426
9427 // @method setBounds(bounds: LatLngBounds): this
9428 // Update the bounds that this ImageOverlay covers
9429 setBounds: function (bounds) {
9430 this._bounds = toLatLngBounds(bounds);
9431
9432 if (this._map) {
9433 this._reset();
9434 }
9435 return this;
9436 },
9437
9438 getEvents: function () {
9439 var events = {
9440 zoom: this._reset,
9441 viewreset: this._reset
9442 };
9443
9444 if (this._zoomAnimated) {
9445 events.zoomanim = this._animateZoom;
9446 }
9447
9448 return events;
9449 },
9450
9451 // @method setZIndex(value: Number): this
9452 // Changes the [zIndex](#imageoverlay-zindex) of the image overlay.
9453 setZIndex: function (value) {
9454 this.options.zIndex = value;
9455 this._updateZIndex();
9456 return this;
9457 },
9458
9459 // @method getBounds(): LatLngBounds
9460 // Get the bounds that this ImageOverlay covers
9461 getBounds: function () {
9462 return this._bounds;
9463 },
9464
9465 // @method getElement(): HTMLElement
9466 // Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement)
9467 // used by this overlay.
9468 getElement: function () {
9469 return this._image;
9470 },
9471
9472 _initImage: function () {
9473 var wasElementSupplied = this._url.tagName === 'IMG';
9474 var img = this._image = wasElementSupplied ? this._url : create$1('img');
9475
9476 addClass(img, 'leaflet-image-layer');
9477 if (this._zoomAnimated) { addClass(img, 'leaflet-zoom-animated'); }
9478 if (this.options.className) { addClass(img, this.options.className); }
9479
9480 img.onselectstart = falseFn;
9481 img.onmousemove = falseFn;
9482
9483 // @event load: Event
9484 // Fired when the ImageOverlay layer has loaded its image
9485 img.onload = bind(this.fire, this, 'load');
9486 img.onerror = bind(this._overlayOnError, this, 'error');
9487
9488 if (this.options.crossOrigin || this.options.crossOrigin === '') {
9489 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
9490 }
9491
9492 if (this.options.zIndex) {
9493 this._updateZIndex();
9494 }
9495
9496 if (wasElementSupplied) {
9497 this._url = img.src;
9498 return;
9499 }
9500
9501 img.src = this._url;
9502 img.alt = this.options.alt;
9503 },
9504
9505 _animateZoom: function (e) {
9506 var scale = this._map.getZoomScale(e.zoom),
9507 offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min;
9508
9509 setTransform(this._image, offset, scale);
9510 },
9511
9512 _reset: function () {
9513 var image = this._image,
9514 bounds = new Bounds(
9515 this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
9516 this._map.latLngToLayerPoint(this._bounds.getSouthEast())),
9517 size = bounds.getSize();
9518
9519 setPosition(image, bounds.min);
9520
9521 image.style.width = size.x + 'px';
9522 image.style.height = size.y + 'px';
9523 },
9524
9525 _updateOpacity: function () {
9526 setOpacity(this._image, this.options.opacity);
9527 },
9528
9529 _updateZIndex: function () {
9530 if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) {
9531 this._image.style.zIndex = this.options.zIndex;
9532 }
9533 },
9534
9535 _overlayOnError: function () {
9536 // @event error: Event
9537 // Fired when the ImageOverlay layer fails to load its image
9538 this.fire('error');
9539
9540 var errorUrl = this.options.errorOverlayUrl;
9541 if (errorUrl && this._url !== errorUrl) {
9542 this._url = errorUrl;
9543 this._image.src = errorUrl;
9544 }
9545 },
9546
9547 // @method getCenter(): LatLng
9548 // Returns the center of the ImageOverlay.
9549 getCenter: function () {
9550 return this._bounds.getCenter();
9551 }
9552 });
9553
9554 // @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
9555 // Instantiates an image overlay object given the URL of the image and the
9556 // geographical bounds it is tied to.
9557 var imageOverlay = function (url, bounds, options) {
9558 return new ImageOverlay(url, bounds, options);
9559 };
9560
9561 /*
9562 * @class VideoOverlay
9563 * @aka L.VideoOverlay
9564 * @inherits ImageOverlay
9565 *
9566 * Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`.
9567 *
9568 * A video overlay uses the [`<video>`](https://developer.mozilla.org/docs/Web/HTML/Element/video)
9569 * HTML5 element.
9570 *
9571 * @example
9572 *
9573 * ```js
9574 * var videoUrl = 'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
9575 * videoBounds = [[ 32, -130], [ 13, -100]];
9576 * L.videoOverlay(videoUrl, videoBounds ).addTo(map);
9577 * ```
9578 */
9579
9580 var VideoOverlay = ImageOverlay.extend({
9581
9582 // @section
9583 // @aka VideoOverlay options
9584 options: {
9585 // @option autoplay: Boolean = true
9586 // Whether the video starts playing automatically when loaded.
9587 // On some browsers autoplay will only work with `muted: true`
9588 autoplay: true,
9589
9590 // @option loop: Boolean = true
9591 // Whether the video will loop back to the beginning when played.
9592 loop: true,
9593
9594 // @option keepAspectRatio: Boolean = true
9595 // Whether the video will save aspect ratio after the projection.
9596 // Relevant for supported browsers. See [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
9597 keepAspectRatio: true,
9598
9599 // @option muted: Boolean = false
9600 // Whether the video starts on mute when loaded.
9601 muted: false,
9602
9603 // @option playsInline: Boolean = true
9604 // Mobile browsers will play the video right where it is instead of open it up in fullscreen mode.
9605 playsInline: true
9606 },
9607
9608 _initImage: function () {
9609 var wasElementSupplied = this._url.tagName === 'VIDEO';
9610 var vid = this._image = wasElementSupplied ? this._url : create$1('video');
9611
9612 addClass(vid, 'leaflet-image-layer');
9613 if (this._zoomAnimated) { addClass(vid, 'leaflet-zoom-animated'); }
9614 if (this.options.className) { addClass(vid, this.options.className); }
9615
9616 vid.onselectstart = falseFn;
9617 vid.onmousemove = falseFn;
9618
9619 // @event load: Event
9620 // Fired when the video has finished loading the first frame
9621 vid.onloadeddata = bind(this.fire, this, 'load');
9622
9623 if (wasElementSupplied) {
9624 var sourceElements = vid.getElementsByTagName('source');
9625 var sources = [];
9626 for (var j = 0; j < sourceElements.length; j++) {
9627 sources.push(sourceElements[j].src);
9628 }
9629
9630 this._url = (sourceElements.length > 0) ? sources : [vid.src];
9631 return;
9632 }
9633
9634 if (!isArray(this._url)) { this._url = [this._url]; }
9635
9636 if (!this.options.keepAspectRatio && Object.prototype.hasOwnProperty.call(vid.style, 'objectFit')) {
9637 vid.style['objectFit'] = 'fill';
9638 }
9639 vid.autoplay = !!this.options.autoplay;
9640 vid.loop = !!this.options.loop;
9641 vid.muted = !!this.options.muted;
9642 vid.playsInline = !!this.options.playsInline;
9643 for (var i = 0; i < this._url.length; i++) {
9644 var source = create$1('source');
9645 source.src = this._url[i];
9646 vid.appendChild(source);
9647 }
9648 }
9649
9650 // @method getElement(): HTMLVideoElement
9651 // Returns the instance of [`HTMLVideoElement`](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement)
9652 // used by this overlay.
9653 });
9654
9655
9656 // @factory L.videoOverlay(video: String|Array|HTMLVideoElement, bounds: LatLngBounds, options?: VideoOverlay options)
9657 // Instantiates an image overlay object given the URL of the video (or array of URLs, or even a video element) and the
9658 // geographical bounds it is tied to.
9659
9660 function videoOverlay(video, bounds, options) {
9661 return new VideoOverlay(video, bounds, options);
9662 }
9663
9664 /*
9665 * @class SVGOverlay
9666 * @aka L.SVGOverlay
9667 * @inherits ImageOverlay
9668 *
9669 * Used to load, display and provide DOM access to an SVG file over specific bounds of the map. Extends `ImageOverlay`.
9670 *
9671 * An SVG overlay uses the [`<svg>`](https://developer.mozilla.org/docs/Web/SVG/Element/svg) element.
9672 *
9673 * @example
9674 *
9675 * ```js
9676 * var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
9677 * svgElement.setAttribute('xmlns', "http://www.w3.org/2000/svg");
9678 * svgElement.setAttribute('viewBox', "0 0 200 200");
9679 * svgElement.innerHTML = '<rect width="200" height="200"/><rect x="75" y="23" width="50" height="50" style="fill:red"/><rect x="75" y="123" width="50" height="50" style="fill:#0013ff"/>';
9680 * var svgElementBounds = [ [ 32, -130 ], [ 13, -100 ] ];
9681 * L.svgOverlay(svgElement, svgElementBounds).addTo(map);
9682 * ```
9683 */
9684
9685 var SVGOverlay = ImageOverlay.extend({
9686 _initImage: function () {
9687 var el = this._image = this._url;
9688
9689 addClass(el, 'leaflet-image-layer');
9690 if (this._zoomAnimated) { addClass(el, 'leaflet-zoom-animated'); }
9691 if (this.options.className) { addClass(el, this.options.className); }
9692
9693 el.onselectstart = falseFn;
9694 el.onmousemove = falseFn;
9695 }
9696
9697 // @method getElement(): SVGElement
9698 // Returns the instance of [`SVGElement`](https://developer.mozilla.org/docs/Web/API/SVGElement)
9699 // used by this overlay.
9700 });
9701
9702
9703 // @factory L.svgOverlay(svg: String|SVGElement, bounds: LatLngBounds, options?: SVGOverlay options)
9704 // Instantiates an image overlay object given an SVG element and the geographical bounds it is tied to.
9705 // A viewBox attribute is required on the SVG element to zoom in and out properly.
9706
9707 function svgOverlay(el, bounds, options) {
9708 return new SVGOverlay(el, bounds, options);
9709 }
9710
9711 /*
9712 * @class DivOverlay
9713 * @inherits Interactive layer
9714 * @aka L.DivOverlay
9715 * Base model for L.Popup and L.Tooltip. Inherit from it for custom overlays like plugins.
9716 */
9717
9718 // @namespace DivOverlay
9719 var DivOverlay = Layer.extend({
9720
9721 // @section
9722 // @aka DivOverlay options
9723 options: {
9724 // @option interactive: Boolean = false
9725 // If true, the popup/tooltip will listen to the mouse events.
9726 interactive: false,
9727
9728 // @option offset: Point = Point(0, 0)
9729 // The offset of the overlay position.
9730 offset: [0, 0],
9731
9732 // @option className: String = ''
9733 // A custom CSS class name to assign to the overlay.
9734 className: '',
9735
9736 // @option pane: String = undefined
9737 // `Map pane` where the overlay will be added.
9738 pane: undefined,
9739
9740 // @option content: String|HTMLElement|Function = ''
9741 // Sets the HTML content of the overlay while initializing. If a function is passed the source layer will be
9742 // passed to the function. The function should return a `String` or `HTMLElement` to be used in the overlay.
9743 content: ''
9744 },
9745
9746 initialize: function (options, source) {
9747 if (options && (options instanceof LatLng || isArray(options))) {
9748 this._latlng = toLatLng(options);
9749 setOptions(this, source);
9750 } else {
9751 setOptions(this, options);
9752 this._source = source;
9753 }
9754 if (this.options.content) {
9755 this._content = this.options.content;
9756 }
9757 },
9758
9759 // @method openOn(map: Map): this
9760 // Adds the overlay to the map.
9761 // Alternative to `map.openPopup(popup)`/`.openTooltip(tooltip)`.
9762 openOn: function (map) {
9763 map = arguments.length ? map : this._source._map; // experimental, not the part of public api
9764 if (!map.hasLayer(this)) {
9765 map.addLayer(this);
9766 }
9767 return this;
9768 },
9769
9770 // @method close(): this
9771 // Closes the overlay.
9772 // Alternative to `map.closePopup(popup)`/`.closeTooltip(tooltip)`
9773 // and `layer.closePopup()`/`.closeTooltip()`.
9774 close: function () {
9775 if (this._map) {
9776 this._map.removeLayer(this);
9777 }
9778 return this;
9779 },
9780
9781 // @method toggle(layer?: Layer): this
9782 // Opens or closes the overlay bound to layer depending on its current state.
9783 // Argument may be omitted only for overlay bound to layer.
9784 // Alternative to `layer.togglePopup()`/`.toggleTooltip()`.
9785 toggle: function (layer) {
9786 if (this._map) {
9787 this.close();
9788 } else {
9789 if (arguments.length) {
9790 this._source = layer;
9791 } else {
9792 layer = this._source;
9793 }
9794 this._prepareOpen();
9795
9796 // open the overlay on the map
9797 this.openOn(layer._map);
9798 }
9799 return this;
9800 },
9801
9802 onAdd: function (map) {
9803 this._zoomAnimated = map._zoomAnimated;
9804
9805 if (!this._container) {
9806 this._initLayout();
9807 }
9808
9809 if (map._fadeAnimated) {
9810 setOpacity(this._container, 0);
9811 }
9812
9813 clearTimeout(this._removeTimeout);
9814 this.getPane().appendChild(this._container);
9815 this.update();
9816
9817 if (map._fadeAnimated) {
9818 setOpacity(this._container, 1);
9819 }
9820
9821 this.bringToFront();
9822
9823 if (this.options.interactive) {
9824 addClass(this._container, 'leaflet-interactive');
9825 this.addInteractiveTarget(this._container);
9826 }
9827 },
9828
9829 onRemove: function (map) {
9830 if (map._fadeAnimated) {
9831 setOpacity(this._container, 0);
9832 this._removeTimeout = setTimeout(bind(remove, undefined, this._container), 200);
9833 } else {
9834 remove(this._container);
9835 }
9836
9837 if (this.options.interactive) {
9838 removeClass(this._container, 'leaflet-interactive');
9839 this.removeInteractiveTarget(this._container);
9840 }
9841 },
9842
9843 // @namespace DivOverlay
9844 // @method getLatLng: LatLng
9845 // Returns the geographical point of the overlay.
9846 getLatLng: function () {
9847 return this._latlng;
9848 },
9849
9850 // @method setLatLng(latlng: LatLng): this
9851 // Sets the geographical point where the overlay will open.
9852 setLatLng: function (latlng) {
9853 this._latlng = toLatLng(latlng);
9854 if (this._map) {
9855 this._updatePosition();
9856 this._adjustPan();
9857 }
9858 return this;
9859 },
9860
9861 // @method getContent: String|HTMLElement
9862 // Returns the content of the overlay.
9863 getContent: function () {
9864 return this._content;
9865 },
9866
9867 // @method setContent(htmlContent: String|HTMLElement|Function): this
9868 // Sets the HTML content of the overlay. If a function is passed the source layer will be passed to the function.
9869 // The function should return a `String` or `HTMLElement` to be used in the overlay.
9870 setContent: function (content) {
9871 this._content = content;
9872 this.update();
9873 return this;
9874 },
9875
9876 // @method getElement: String|HTMLElement
9877 // Returns the HTML container of the overlay.
9878 getElement: function () {
9879 return this._container;
9880 },
9881
9882 // @method update: null
9883 // Updates the overlay content, layout and position. Useful for updating the overlay after something inside changed, e.g. image loaded.
9884 update: function () {
9885 if (!this._map) { return; }
9886
9887 this._container.style.visibility = 'hidden';
9888
9889 this._updateContent();
9890 this._updateLayout();
9891 this._updatePosition();
9892
9893 this._container.style.visibility = '';
9894
9895 this._adjustPan();
9896 },
9897
9898 getEvents: function () {
9899 var events = {
9900 zoom: this._updatePosition,
9901 viewreset: this._updatePosition
9902 };
9903
9904 if (this._zoomAnimated) {
9905 events.zoomanim = this._animateZoom;
9906 }
9907 return events;
9908 },
9909
9910 // @method isOpen: Boolean
9911 // Returns `true` when the overlay is visible on the map.
9912 isOpen: function () {
9913 return !!this._map && this._map.hasLayer(this);
9914 },
9915
9916 // @method bringToFront: this
9917 // Brings this overlay in front of other overlays (in the same map pane).
9918 bringToFront: function () {
9919 if (this._map) {
9920 toFront(this._container);
9921 }
9922 return this;
9923 },
9924
9925 // @method bringToBack: this
9926 // Brings this overlay to the back of other overlays (in the same map pane).
9927 bringToBack: function () {
9928 if (this._map) {
9929 toBack(this._container);
9930 }
9931 return this;
9932 },
9933
9934 // prepare bound overlay to open: update latlng pos / content source (for FeatureGroup)
9935 _prepareOpen: function (latlng) {
9936 var source = this._source;
9937 if (!source._map) { return false; }
9938
9939 if (source instanceof FeatureGroup) {
9940 source = null;
9941 var layers = this._source._layers;
9942 for (var id in layers) {
9943 if (layers[id]._map) {
9944 source = layers[id];
9945 break;
9946 }
9947 }
9948 if (!source) { return false; } // Unable to get source layer.
9949
9950 // set overlay source to this layer
9951 this._source = source;
9952 }
9953
9954 if (!latlng) {
9955 if (source.getCenter) {
9956 latlng = source.getCenter();
9957 } else if (source.getLatLng) {
9958 latlng = source.getLatLng();
9959 } else if (source.getBounds) {
9960 latlng = source.getBounds().getCenter();
9961 } else {
9962 throw new Error('Unable to get source layer LatLng.');
9963 }
9964 }
9965 this.setLatLng(latlng);
9966
9967 if (this._map) {
9968 // update the overlay (content, layout, etc...)
9969 this.update();
9970 }
9971
9972 return true;
9973 },
9974
9975 _updateContent: function () {
9976 if (!this._content) { return; }
9977
9978 var node = this._contentNode;
9979 var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
9980
9981 if (typeof content === 'string') {
9982 node.innerHTML = content;
9983 } else {
9984 while (node.hasChildNodes()) {
9985 node.removeChild(node.firstChild);
9986 }
9987 node.appendChild(content);
9988 }
9989
9990 // @namespace DivOverlay
9991 // @section DivOverlay events
9992 // @event contentupdate: Event
9993 // Fired when the content of the overlay is updated
9994 this.fire('contentupdate');
9995 },
9996
9997 _updatePosition: function () {
9998 if (!this._map) { return; }
9999
10000 var pos = this._map.latLngToLayerPoint(this._latlng),
10001 offset = toPoint(this.options.offset),
10002 anchor = this._getAnchor();
10003
10004 if (this._zoomAnimated) {
10005 setPosition(this._container, pos.add(anchor));
10006 } else {
10007 offset = offset.add(pos).add(anchor);
10008 }
10009
10010 var bottom = this._containerBottom = -offset.y,
10011 left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x;
10012
10013 // bottom position the overlay in case the height of the overlay changes (images loading etc)
10014 this._container.style.bottom = bottom + 'px';
10015 this._container.style.left = left + 'px';
10016 },
10017
10018 _getAnchor: function () {
10019 return [0, 0];
10020 }
10021
10022 });
10023
10024 Map.include({
10025 _initOverlay: function (OverlayClass, content, latlng, options) {
10026 var overlay = content;
10027 if (!(overlay instanceof OverlayClass)) {
10028 overlay = new OverlayClass(options).setContent(content);
10029 }
10030 if (latlng) {
10031 overlay.setLatLng(latlng);
10032 }
10033 return overlay;
10034 }
10035 });
10036
10037
10038 Layer.include({
10039 _initOverlay: function (OverlayClass, old, content, options) {
10040 var overlay = content;
10041 if (overlay instanceof OverlayClass) {
10042 setOptions(overlay, options);
10043 overlay._source = this;
10044 } else {
10045 overlay = (old && !options) ? old : new OverlayClass(options, this);
10046 overlay.setContent(content);
10047 }
10048 return overlay;
10049 }
10050 });
10051
10052 /*
10053 * @class Popup
10054 * @inherits DivOverlay
10055 * @aka L.Popup
10056 * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
10057 * open popups while making sure that only one popup is open at one time
10058 * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
10059 *
10060 * @example
10061 *
10062 * If you want to just bind a popup to marker click and then open it, it's really easy:
10063 *
10064 * ```js
10065 * marker.bindPopup(popupContent).openPopup();
10066 * ```
10067 * Path overlays like polylines also have a `bindPopup` method.
10068 *
10069 * A popup can be also standalone:
10070 *
10071 * ```js
10072 * var popup = L.popup()
10073 * .setLatLng(latlng)
10074 * .setContent('<p>Hello world!<br />This is a nice popup.</p>')
10075 * .openOn(map);
10076 * ```
10077 * or
10078 * ```js
10079 * var popup = L.popup(latlng, {content: '<p>Hello world!<br />This is a nice popup.</p>')
10080 * .openOn(map);
10081 * ```
10082 */
10083
10084
10085 // @namespace Popup
10086 var Popup = DivOverlay.extend({
10087
10088 // @section
10089 // @aka Popup options
10090 options: {
10091 // @option pane: String = 'popupPane'
10092 // `Map pane` where the popup will be added.
10093 pane: 'popupPane',
10094
10095 // @option offset: Point = Point(0, 7)
10096 // The offset of the popup position.
10097 offset: [0, 7],
10098
10099 // @option maxWidth: Number = 300
10100 // Max width of the popup, in pixels.
10101 maxWidth: 300,
10102
10103 // @option minWidth: Number = 50
10104 // Min width of the popup, in pixels.
10105 minWidth: 50,
10106
10107 // @option maxHeight: Number = null
10108 // If set, creates a scrollable container of the given height
10109 // inside a popup if its content exceeds it.
10110 // The scrollable container can be styled using the
10111 // `leaflet-popup-scrolled` CSS class selector.
10112 maxHeight: null,
10113
10114 // @option autoPan: Boolean = true
10115 // Set it to `false` if you don't want the map to do panning animation
10116 // to fit the opened popup.
10117 autoPan: true,
10118
10119 // @option autoPanPaddingTopLeft: Point = null
10120 // The margin between the popup and the top left corner of the map
10121 // view after autopanning was performed.
10122 autoPanPaddingTopLeft: null,
10123
10124 // @option autoPanPaddingBottomRight: Point = null
10125 // The margin between the popup and the bottom right corner of the map
10126 // view after autopanning was performed.
10127 autoPanPaddingBottomRight: null,
10128
10129 // @option autoPanPadding: Point = Point(5, 5)
10130 // Equivalent of setting both top left and bottom right autopan padding to the same value.
10131 autoPanPadding: [5, 5],
10132
10133 // @option keepInView: Boolean = false
10134 // Set it to `true` if you want to prevent users from panning the popup
10135 // off of the screen while it is open.
10136 keepInView: false,
10137
10138 // @option closeButton: Boolean = true
10139 // Controls the presence of a close button in the popup.
10140 closeButton: true,
10141
10142 // @option autoClose: Boolean = true
10143 // Set it to `false` if you want to override the default behavior of
10144 // the popup closing when another popup is opened.
10145 autoClose: true,
10146
10147 // @option closeOnEscapeKey: Boolean = true
10148 // Set it to `false` if you want to override the default behavior of
10149 // the ESC key for closing of the popup.
10150 closeOnEscapeKey: true,
10151
10152 // @option closeOnClick: Boolean = *
10153 // Set it if you want to override the default behavior of the popup closing when user clicks
10154 // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option.
10155
10156 // @option className: String = ''
10157 // A custom CSS class name to assign to the popup.
10158 className: ''
10159 },
10160
10161 // @namespace Popup
10162 // @method openOn(map: Map): this
10163 // Alternative to `map.openPopup(popup)`.
10164 // Adds the popup to the map and closes the previous one.
10165 openOn: function (map) {
10166 map = arguments.length ? map : this._source._map; // experimental, not the part of public api
10167
10168 if (!map.hasLayer(this) && map._popup && map._popup.options.autoClose) {
10169 map.removeLayer(map._popup);
10170 }
10171 map._popup = this;
10172
10173 return DivOverlay.prototype.openOn.call(this, map);
10174 },
10175
10176 onAdd: function (map) {
10177 DivOverlay.prototype.onAdd.call(this, map);
10178
10179 // @namespace Map
10180 // @section Popup events
10181 // @event popupopen: PopupEvent
10182 // Fired when a popup is opened in the map
10183 map.fire('popupopen', {popup: this});
10184
10185 if (this._source) {
10186 // @namespace Layer
10187 // @section Popup events
10188 // @event popupopen: PopupEvent
10189 // Fired when a popup bound to this layer is opened
10190 this._source.fire('popupopen', {popup: this}, true);
10191 // For non-path layers, we toggle the popup when clicking
10192 // again the layer, so prevent the map to reopen it.
10193 if (!(this._source instanceof Path)) {
10194 this._source.on('preclick', stopPropagation);
10195 }
10196 }
10197 },
10198
10199 onRemove: function (map) {
10200 DivOverlay.prototype.onRemove.call(this, map);
10201
10202 // @namespace Map
10203 // @section Popup events
10204 // @event popupclose: PopupEvent
10205 // Fired when a popup in the map is closed
10206 map.fire('popupclose', {popup: this});
10207
10208 if (this._source) {
10209 // @namespace Layer
10210 // @section Popup events
10211 // @event popupclose: PopupEvent
10212 // Fired when a popup bound to this layer is closed
10213 this._source.fire('popupclose', {popup: this}, true);
10214 if (!(this._source instanceof Path)) {
10215 this._source.off('preclick', stopPropagation);
10216 }
10217 }
10218 },
10219
10220 getEvents: function () {
10221 var events = DivOverlay.prototype.getEvents.call(this);
10222
10223 if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
10224 events.preclick = this.close;
10225 }
10226
10227 if (this.options.keepInView) {
10228 events.moveend = this._adjustPan;
10229 }
10230
10231 return events;
10232 },
10233
10234 _initLayout: function () {
10235 var prefix = 'leaflet-popup',
10236 container = this._container = create$1('div',
10237 prefix + ' ' + (this.options.className || '') +
10238 ' leaflet-zoom-animated');
10239
10240 var wrapper = this._wrapper = create$1('div', prefix + '-content-wrapper', container);
10241 this._contentNode = create$1('div', prefix + '-content', wrapper);
10242
10243 disableClickPropagation(container);
10244 disableScrollPropagation(this._contentNode);
10245 on(container, 'contextmenu', stopPropagation);
10246
10247 this._tipContainer = create$1('div', prefix + '-tip-container', container);
10248 this._tip = create$1('div', prefix + '-tip', this._tipContainer);
10249
10250 if (this.options.closeButton) {
10251 var closeButton = this._closeButton = create$1('a', prefix + '-close-button', container);
10252 closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
10253 closeButton.setAttribute('aria-label', 'Close popup');
10254 closeButton.href = '#close';
10255 closeButton.innerHTML = '<span aria-hidden="true">&#215;</span>';
10256
10257 on(closeButton, 'click', function (ev) {
10258 preventDefault(ev);
10259 this.close();
10260 }, this);
10261 }
10262 },
10263
10264 _updateLayout: function () {
10265 var container = this._contentNode,
10266 style = container.style;
10267
10268 style.width = '';
10269 style.whiteSpace = 'nowrap';
10270
10271 var width = container.offsetWidth;
10272 width = Math.min(width, this.options.maxWidth);
10273 width = Math.max(width, this.options.minWidth);
10274
10275 style.width = (width + 1) + 'px';
10276 style.whiteSpace = '';
10277
10278 style.height = '';
10279
10280 var height = container.offsetHeight,
10281 maxHeight = this.options.maxHeight,
10282 scrolledClass = 'leaflet-popup-scrolled';
10283
10284 if (maxHeight && height > maxHeight) {
10285 style.height = maxHeight + 'px';
10286 addClass(container, scrolledClass);
10287 } else {
10288 removeClass(container, scrolledClass);
10289 }
10290
10291 this._containerWidth = this._container.offsetWidth;
10292 },
10293
10294 _animateZoom: function (e) {
10295 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
10296 anchor = this._getAnchor();
10297 setPosition(this._container, pos.add(anchor));
10298 },
10299
10300 _adjustPan: function () {
10301 if (!this.options.autoPan) { return; }
10302 if (this._map._panAnim) { this._map._panAnim.stop(); }
10303
10304 // We can endlessly recurse if keepInView is set and the view resets.
10305 // Let's guard against that by exiting early if we're responding to our own autopan.
10306 if (this._autopanning) {
10307 this._autopanning = false;
10308 return;
10309 }
10310
10311 var map = this._map,
10312 marginBottom = parseInt(getStyle(this._container, 'marginBottom'), 10) || 0,
10313 containerHeight = this._container.offsetHeight + marginBottom,
10314 containerWidth = this._containerWidth,
10315 layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom);
10316
10317 layerPos._add(getPosition(this._container));
10318
10319 var containerPos = map.layerPointToContainerPoint(layerPos),
10320 padding = toPoint(this.options.autoPanPadding),
10321 paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding),
10322 paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding),
10323 size = map.getSize(),
10324 dx = 0,
10325 dy = 0;
10326
10327 if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
10328 dx = containerPos.x + containerWidth - size.x + paddingBR.x;
10329 }
10330 if (containerPos.x - dx - paddingTL.x < 0) { // left
10331 dx = containerPos.x - paddingTL.x;
10332 }
10333 if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
10334 dy = containerPos.y + containerHeight - size.y + paddingBR.y;
10335 }
10336 if (containerPos.y - dy - paddingTL.y < 0) { // top
10337 dy = containerPos.y - paddingTL.y;
10338 }
10339
10340 // @namespace Map
10341 // @section Popup events
10342 // @event autopanstart: Event
10343 // Fired when the map starts autopanning when opening a popup.
10344 if (dx || dy) {
10345 // Track that we're autopanning, as this function will be re-ran on moveend
10346 if (this.options.keepInView) {
10347 this._autopanning = true;
10348 }
10349
10350 map
10351 .fire('autopanstart')
10352 .panBy([dx, dy]);
10353 }
10354 },
10355
10356 _getAnchor: function () {
10357 // Where should we anchor the popup on the source layer?
10358 return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
10359 }
10360
10361 });
10362
10363 // @namespace Popup
10364 // @factory L.popup(options?: Popup options, source?: Layer)
10365 // Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers.
10366 // @alternative
10367 // @factory L.popup(latlng: LatLng, options?: Popup options)
10368 // Instantiates a `Popup` object given `latlng` where the popup will open and an optional `options` object that describes its appearance and location.
10369 var popup = function (options, source) {
10370 return new Popup(options, source);
10371 };
10372
10373
10374 /* @namespace Map
10375 * @section Interaction Options
10376 * @option closePopupOnClick: Boolean = true
10377 * Set it to `false` if you don't want popups to close when user clicks the map.
10378 */
10379 Map.mergeOptions({
10380 closePopupOnClick: true
10381 });
10382
10383
10384 // @namespace Map
10385 // @section Methods for Layers and Controls
10386 Map.include({
10387 // @method openPopup(popup: Popup): this
10388 // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
10389 // @alternative
10390 // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
10391 // Creates a popup with the specified content and options and opens it in the given point on a map.
10392 openPopup: function (popup, latlng, options) {
10393 this._initOverlay(Popup, popup, latlng, options)
10394 .openOn(this);
10395
10396 return this;
10397 },
10398
10399 // @method closePopup(popup?: Popup): this
10400 // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
10401 closePopup: function (popup) {
10402 popup = arguments.length ? popup : this._popup;
10403 if (popup) {
10404 popup.close();
10405 }
10406 return this;
10407 }
10408 });
10409
10410 /*
10411 * @namespace Layer
10412 * @section Popup methods example
10413 *
10414 * All layers share a set of methods convenient for binding popups to it.
10415 *
10416 * ```js
10417 * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
10418 * layer.openPopup();
10419 * layer.closePopup();
10420 * ```
10421 *
10422 * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened.
10423 */
10424
10425 // @section Popup methods
10426 Layer.include({
10427
10428 // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
10429 // Binds a popup to the layer with the passed `content` and sets up the
10430 // necessary event listeners. If a `Function` is passed it will receive
10431 // the layer as the first argument and should return a `String` or `HTMLElement`.
10432 bindPopup: function (content, options) {
10433 this._popup = this._initOverlay(Popup, this._popup, content, options);
10434 if (!this._popupHandlersAdded) {
10435 this.on({
10436 click: this._openPopup,
10437 keypress: this._onKeyPress,
10438 remove: this.closePopup,
10439 move: this._movePopup
10440 });
10441 this._popupHandlersAdded = true;
10442 }
10443
10444 return this;
10445 },
10446
10447 // @method unbindPopup(): this
10448 // Removes the popup previously bound with `bindPopup`.
10449 unbindPopup: function () {
10450 if (this._popup) {
10451 this.off({
10452 click: this._openPopup,
10453 keypress: this._onKeyPress,
10454 remove: this.closePopup,
10455 move: this._movePopup
10456 });
10457 this._popupHandlersAdded = false;
10458 this._popup = null;
10459 }
10460 return this;
10461 },
10462
10463 // @method openPopup(latlng?: LatLng): this
10464 // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed.
10465 openPopup: function (latlng) {
10466 if (this._popup) {
10467 if (!(this instanceof FeatureGroup)) {
10468 this._popup._source = this;
10469 }
10470 if (this._popup._prepareOpen(latlng || this._latlng)) {
10471 // open the popup on the map
10472 this._popup.openOn(this._map);
10473 }
10474 }
10475 return this;
10476 },
10477
10478 // @method closePopup(): this
10479 // Closes the popup bound to this layer if it is open.
10480 closePopup: function () {
10481 if (this._popup) {
10482 this._popup.close();
10483 }
10484 return this;
10485 },
10486
10487 // @method togglePopup(): this
10488 // Opens or closes the popup bound to this layer depending on its current state.
10489 togglePopup: function () {
10490 if (this._popup) {
10491 this._popup.toggle(this);
10492 }
10493 return this;
10494 },
10495
10496 // @method isPopupOpen(): boolean
10497 // Returns `true` if the popup bound to this layer is currently open.
10498 isPopupOpen: function () {
10499 return (this._popup ? this._popup.isOpen() : false);
10500 },
10501
10502 // @method setPopupContent(content: String|HTMLElement|Popup): this
10503 // Sets the content of the popup bound to this layer.
10504 setPopupContent: function (content) {
10505 if (this._popup) {
10506 this._popup.setContent(content);
10507 }
10508 return this;
10509 },
10510
10511 // @method getPopup(): Popup
10512 // Returns the popup bound to this layer.
10513 getPopup: function () {
10514 return this._popup;
10515 },
10516
10517 _openPopup: function (e) {
10518 if (!this._popup || !this._map) {
10519 return;
10520 }
10521 // prevent map click
10522 stop(e);
10523
10524 var target = e.layer || e.target;
10525 if (this._popup._source === target && !(target instanceof Path)) {
10526 // treat it like a marker and figure out
10527 // if we should toggle it open/closed
10528 if (this._map.hasLayer(this._popup)) {
10529 this.closePopup();
10530 } else {
10531 this.openPopup(e.latlng);
10532 }
10533 return;
10534 }
10535 this._popup._source = target;
10536 this.openPopup(e.latlng);
10537 },
10538
10539 _movePopup: function (e) {
10540 this._popup.setLatLng(e.latlng);
10541 },
10542
10543 _onKeyPress: function (e) {
10544 if (e.originalEvent.keyCode === 13) {
10545 this._openPopup(e);
10546 }
10547 }
10548 });
10549
10550 /*
10551 * @class Tooltip
10552 * @inherits DivOverlay
10553 * @aka L.Tooltip
10554 * Used to display small texts on top of map layers.
10555 *
10556 * @example
10557 * If you want to just bind a tooltip to marker:
10558 *
10559 * ```js
10560 * marker.bindTooltip("my tooltip text").openTooltip();
10561 * ```
10562 * Path overlays like polylines also have a `bindTooltip` method.
10563 *
10564 * A tooltip can be also standalone:
10565 *
10566 * ```js
10567 * var tooltip = L.tooltip()
10568 * .setLatLng(latlng)
10569 * .setContent('Hello world!<br />This is a nice tooltip.')
10570 * .addTo(map);
10571 * ```
10572 * or
10573 * ```js
10574 * var tooltip = L.tooltip(latlng, {content: 'Hello world!<br />This is a nice tooltip.'})
10575 * .addTo(map);
10576 * ```
10577 *
10578 *
10579 * Note about tooltip offset. Leaflet takes two options in consideration
10580 * for computing tooltip offsetting:
10581 * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
10582 * Add a positive x offset to move the tooltip to the right, and a positive y offset to
10583 * move it to the bottom. Negatives will move to the left and top.
10584 * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
10585 * should adapt this value if you use a custom icon.
10586 */
10587
10588
10589 // @namespace Tooltip
10590 var Tooltip = DivOverlay.extend({
10591
10592 // @section
10593 // @aka Tooltip options
10594 options: {
10595 // @option pane: String = 'tooltipPane'
10596 // `Map pane` where the tooltip will be added.
10597 pane: 'tooltipPane',
10598
10599 // @option offset: Point = Point(0, 0)
10600 // Optional offset of the tooltip position.
10601 offset: [0, 0],
10602
10603 // @option direction: String = 'auto'
10604 // Direction where to open the tooltip. Possible values are: `right`, `left`,
10605 // `top`, `bottom`, `center`, `auto`.
10606 // `auto` will dynamically switch between `right` and `left` according to the tooltip
10607 // position on the map.
10608 direction: 'auto',
10609
10610 // @option permanent: Boolean = false
10611 // Whether to open the tooltip permanently or only on mouseover.
10612 permanent: false,
10613
10614 // @option sticky: Boolean = false
10615 // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
10616 sticky: false,
10617
10618 // @option opacity: Number = 0.9
10619 // Tooltip container opacity.
10620 opacity: 0.9
10621 },
10622
10623 onAdd: function (map) {
10624 DivOverlay.prototype.onAdd.call(this, map);
10625 this.setOpacity(this.options.opacity);
10626
10627 // @namespace Map
10628 // @section Tooltip events
10629 // @event tooltipopen: TooltipEvent
10630 // Fired when a tooltip is opened in the map.
10631 map.fire('tooltipopen', {tooltip: this});
10632
10633 if (this._source) {
10634 this.addEventParent(this._source);
10635
10636 // @namespace Layer
10637 // @section Tooltip events
10638 // @event tooltipopen: TooltipEvent
10639 // Fired when a tooltip bound to this layer is opened.
10640 this._source.fire('tooltipopen', {tooltip: this}, true);
10641 }
10642 },
10643
10644 onRemove: function (map) {
10645 DivOverlay.prototype.onRemove.call(this, map);
10646
10647 // @namespace Map
10648 // @section Tooltip events
10649 // @event tooltipclose: TooltipEvent
10650 // Fired when a tooltip in the map is closed.
10651 map.fire('tooltipclose', {tooltip: this});
10652
10653 if (this._source) {
10654 this.removeEventParent(this._source);
10655
10656 // @namespace Layer
10657 // @section Tooltip events
10658 // @event tooltipclose: TooltipEvent
10659 // Fired when a tooltip bound to this layer is closed.
10660 this._source.fire('tooltipclose', {tooltip: this}, true);
10661 }
10662 },
10663
10664 getEvents: function () {
10665 var events = DivOverlay.prototype.getEvents.call(this);
10666
10667 if (!this.options.permanent) {
10668 events.preclick = this.close;
10669 }
10670
10671 return events;
10672 },
10673
10674 _initLayout: function () {
10675 var prefix = 'leaflet-tooltip',
10676 className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
10677
10678 this._contentNode = this._container = create$1('div', className);
10679
10680 this._container.setAttribute('role', 'tooltip');
10681 this._container.setAttribute('id', 'leaflet-tooltip-' + stamp(this));
10682 },
10683
10684 _updateLayout: function () {},
10685
10686 _adjustPan: function () {},
10687
10688 _setPosition: function (pos) {
10689 var subX, subY,
10690 map = this._map,
10691 container = this._container,
10692 centerPoint = map.latLngToContainerPoint(map.getCenter()),
10693 tooltipPoint = map.layerPointToContainerPoint(pos),
10694 direction = this.options.direction,
10695 tooltipWidth = container.offsetWidth,
10696 tooltipHeight = container.offsetHeight,
10697 offset = toPoint(this.options.offset),
10698 anchor = this._getAnchor();
10699
10700 if (direction === 'top') {
10701 subX = tooltipWidth / 2;
10702 subY = tooltipHeight;
10703 } else if (direction === 'bottom') {
10704 subX = tooltipWidth / 2;
10705 subY = 0;
10706 } else if (direction === 'center') {
10707 subX = tooltipWidth / 2;
10708 subY = tooltipHeight / 2;
10709 } else if (direction === 'right') {
10710 subX = 0;
10711 subY = tooltipHeight / 2;
10712 } else if (direction === 'left') {
10713 subX = tooltipWidth;
10714 subY = tooltipHeight / 2;
10715 } else if (tooltipPoint.x < centerPoint.x) {
10716 direction = 'right';
10717 subX = 0;
10718 subY = tooltipHeight / 2;
10719 } else {
10720 direction = 'left';
10721 subX = tooltipWidth + (offset.x + anchor.x) * 2;
10722 subY = tooltipHeight / 2;
10723 }
10724
10725 pos = pos.subtract(toPoint(subX, subY, true)).add(offset).add(anchor);
10726
10727 removeClass(container, 'leaflet-tooltip-right');
10728 removeClass(container, 'leaflet-tooltip-left');
10729 removeClass(container, 'leaflet-tooltip-top');
10730 removeClass(container, 'leaflet-tooltip-bottom');
10731 addClass(container, 'leaflet-tooltip-' + direction);
10732 setPosition(container, pos);
10733 },
10734
10735 _updatePosition: function () {
10736 var pos = this._map.latLngToLayerPoint(this._latlng);
10737 this._setPosition(pos);
10738 },
10739
10740 setOpacity: function (opacity) {
10741 this.options.opacity = opacity;
10742
10743 if (this._container) {
10744 setOpacity(this._container, opacity);
10745 }
10746 },
10747
10748 _animateZoom: function (e) {
10749 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
10750 this._setPosition(pos);
10751 },
10752
10753 _getAnchor: function () {
10754 // Where should we anchor the tooltip on the source layer?
10755 return toPoint(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
10756 }
10757
10758 });
10759
10760 // @namespace Tooltip
10761 // @factory L.tooltip(options?: Tooltip options, source?: Layer)
10762 // Instantiates a `Tooltip` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers.
10763 // @alternative
10764 // @factory L.tooltip(latlng: LatLng, options?: Tooltip options)
10765 // Instantiates a `Tooltip` object given `latlng` where the tooltip will open and an optional `options` object that describes its appearance and location.
10766 var tooltip = function (options, source) {
10767 return new Tooltip(options, source);
10768 };
10769
10770 // @namespace Map
10771 // @section Methods for Layers and Controls
10772 Map.include({
10773
10774 // @method openTooltip(tooltip: Tooltip): this
10775 // Opens the specified tooltip.
10776 // @alternative
10777 // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
10778 // Creates a tooltip with the specified content and options and open it.
10779 openTooltip: function (tooltip, latlng, options) {
10780 this._initOverlay(Tooltip, tooltip, latlng, options)
10781 .openOn(this);
10782
10783 return this;
10784 },
10785
10786 // @method closeTooltip(tooltip: Tooltip): this
10787 // Closes the tooltip given as parameter.
10788 closeTooltip: function (tooltip) {
10789 tooltip.close();
10790 return this;
10791 }
10792
10793 });
10794
10795 /*
10796 * @namespace Layer
10797 * @section Tooltip methods example
10798 *
10799 * All layers share a set of methods convenient for binding tooltips to it.
10800 *
10801 * ```js
10802 * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
10803 * layer.openTooltip();
10804 * layer.closeTooltip();
10805 * ```
10806 */
10807
10808 // @section Tooltip methods
10809 Layer.include({
10810
10811 // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
10812 // Binds a tooltip to the layer with the passed `content` and sets up the
10813 // necessary event listeners. If a `Function` is passed it will receive
10814 // the layer as the first argument and should return a `String` or `HTMLElement`.
10815 bindTooltip: function (content, options) {
10816
10817 if (this._tooltip && this.isTooltipOpen()) {
10818 this.unbindTooltip();
10819 }
10820
10821 this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options);
10822 this._initTooltipInteractions();
10823
10824 if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
10825 this.openTooltip();
10826 }
10827
10828 return this;
10829 },
10830
10831 // @method unbindTooltip(): this
10832 // Removes the tooltip previously bound with `bindTooltip`.
10833 unbindTooltip: function () {
10834 if (this._tooltip) {
10835 this._initTooltipInteractions(true);
10836 this.closeTooltip();
10837 this._tooltip = null;
10838 }
10839 return this;
10840 },
10841
10842 _initTooltipInteractions: function (remove) {
10843 if (!remove && this._tooltipHandlersAdded) { return; }
10844 var onOff = remove ? 'off' : 'on',
10845 events = {
10846 remove: this.closeTooltip,
10847 move: this._moveTooltip
10848 };
10849 if (!this._tooltip.options.permanent) {
10850 events.mouseover = this._openTooltip;
10851 events.mouseout = this.closeTooltip;
10852 events.click = this._openTooltip;
10853 if (this._map) {
10854 this._addFocusListeners();
10855 } else {
10856 events.add = this._addFocusListeners;
10857 }
10858 } else {
10859 events.add = this._openTooltip;
10860 }
10861 if (this._tooltip.options.sticky) {
10862 events.mousemove = this._moveTooltip;
10863 }
10864 this[onOff](events);
10865 this._tooltipHandlersAdded = !remove;
10866 },
10867
10868 // @method openTooltip(latlng?: LatLng): this
10869 // Opens the bound tooltip at the specified `latlng` or at the default tooltip anchor if no `latlng` is passed.
10870 openTooltip: function (latlng) {
10871 if (this._tooltip) {
10872 if (!(this instanceof FeatureGroup)) {
10873 this._tooltip._source = this;
10874 }
10875 if (this._tooltip._prepareOpen(latlng)) {
10876 // open the tooltip on the map
10877 this._tooltip.openOn(this._map);
10878
10879 if (this.getElement) {
10880 this._setAriaDescribedByOnLayer(this);
10881 } else if (this.eachLayer) {
10882 this.eachLayer(this._setAriaDescribedByOnLayer, this);
10883 }
10884 }
10885 }
10886 return this;
10887 },
10888
10889 // @method closeTooltip(): this
10890 // Closes the tooltip bound to this layer if it is open.
10891 closeTooltip: function () {
10892 if (this._tooltip) {
10893 return this._tooltip.close();
10894 }
10895 },
10896
10897 // @method toggleTooltip(): this
10898 // Opens or closes the tooltip bound to this layer depending on its current state.
10899 toggleTooltip: function () {
10900 if (this._tooltip) {
10901 this._tooltip.toggle(this);
10902 }
10903 return this;
10904 },
10905
10906 // @method isTooltipOpen(): boolean
10907 // Returns `true` if the tooltip bound to this layer is currently open.
10908 isTooltipOpen: function () {
10909 return this._tooltip.isOpen();
10910 },
10911
10912 // @method setTooltipContent(content: String|HTMLElement|Tooltip): this
10913 // Sets the content of the tooltip bound to this layer.
10914 setTooltipContent: function (content) {
10915 if (this._tooltip) {
10916 this._tooltip.setContent(content);
10917 }
10918 return this;
10919 },
10920
10921 // @method getTooltip(): Tooltip
10922 // Returns the tooltip bound to this layer.
10923 getTooltip: function () {
10924 return this._tooltip;
10925 },
10926
10927 _addFocusListeners: function () {
10928 if (this.getElement) {
10929 this._addFocusListenersOnLayer(this);
10930 } else if (this.eachLayer) {
10931 this.eachLayer(this._addFocusListenersOnLayer, this);
10932 }
10933 },
10934
10935 _addFocusListenersOnLayer: function (layer) {
10936 var el = layer.getElement();
10937 if (el) {
10938 on(el, 'focus', function () {
10939 this._tooltip._source = layer;
10940 this.openTooltip();
10941 }, this);
10942 on(el, 'blur', this.closeTooltip, this);
10943 }
10944 },
10945
10946 _setAriaDescribedByOnLayer: function (layer) {
10947 var el = layer.getElement();
10948 if (el) {
10949 el.setAttribute('aria-describedby', this._tooltip._container.id);
10950 }
10951 },
10952
10953
10954 _openTooltip: function (e) {
10955 if (!this._tooltip || !this._map || (this._map.dragging && this._map.dragging.moving())) {
10956 return;
10957 }
10958 this._tooltip._source = e.layer || e.target;
10959
10960 this.openTooltip(this._tooltip.options.sticky ? e.latlng : undefined);
10961 },
10962
10963 _moveTooltip: function (e) {
10964 var latlng = e.latlng, containerPoint, layerPoint;
10965 if (this._tooltip.options.sticky && e.originalEvent) {
10966 containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
10967 layerPoint = this._map.containerPointToLayerPoint(containerPoint);
10968 latlng = this._map.layerPointToLatLng(layerPoint);
10969 }
10970 this._tooltip.setLatLng(latlng);
10971 }
10972 });
10973
10974 /*
10975 * @class DivIcon
10976 * @aka L.DivIcon
10977 * @inherits Icon
10978 *
10979 * Represents a lightweight icon for markers that uses a simple `<div>`
10980 * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
10981 *
10982 * @example
10983 * ```js
10984 * var myIcon = L.divIcon({className: 'my-div-icon'});
10985 * // you can set .my-div-icon styles in CSS
10986 *
10987 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
10988 * ```
10989 *
10990 * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
10991 */
10992
10993 var DivIcon = Icon.extend({
10994 options: {
10995 // @section
10996 // @aka DivIcon options
10997 iconSize: [12, 12], // also can be set through CSS
10998
10999 // iconAnchor: (Point),
11000 // popupAnchor: (Point),
11001
11002 // @option html: String|HTMLElement = ''
11003 // Custom HTML code to put inside the div element, empty by default. Alternatively,
11004 // an instance of `HTMLElement`.
11005 html: false,
11006
11007 // @option bgPos: Point = [0, 0]
11008 // Optional relative position of the background, in pixels
11009 bgPos: null,
11010
11011 className: 'leaflet-div-icon'
11012 },
11013
11014 createIcon: function (oldIcon) {
11015 var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
11016 options = this.options;
11017
11018 if (options.html instanceof Element) {
11019 empty(div);
11020 div.appendChild(options.html);
11021 } else {
11022 div.innerHTML = options.html !== false ? options.html : '';
11023 }
11024
11025 if (options.bgPos) {
11026 var bgPos = toPoint(options.bgPos);
11027 div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
11028 }
11029 this._setIconStyles(div, 'icon');
11030
11031 return div;
11032 },
11033
11034 createShadow: function () {
11035 return null;
11036 }
11037 });
11038
11039 // @factory L.divIcon(options: DivIcon options)
11040 // Creates a `DivIcon` instance with the given options.
11041 function divIcon(options) {
11042 return new DivIcon(options);
11043 }
11044
11045 Icon.Default = IconDefault;
11046
11047 /*
11048 * @class GridLayer
11049 * @inherits Layer
11050 * @aka L.GridLayer
11051 *
11052 * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
11053 * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you.
11054 *
11055 *
11056 * @section Synchronous usage
11057 * @example
11058 *
11059 * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile.
11060 *
11061 * ```js
11062 * var CanvasLayer = L.GridLayer.extend({
11063 * createTile: function(coords){
11064 * // create a <canvas> element for drawing
11065 * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
11066 *
11067 * // setup tile width and height according to the options
11068 * var size = this.getTileSize();
11069 * tile.width = size.x;
11070 * tile.height = size.y;
11071 *
11072 * // get a canvas context and draw something on it using coords.x, coords.y and coords.z
11073 * var ctx = tile.getContext('2d');
11074 *
11075 * // return the tile so it can be rendered on screen
11076 * return tile;
11077 * }
11078 * });
11079 * ```
11080 *
11081 * @section Asynchronous usage
11082 * @example
11083 *
11084 * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback.
11085 *
11086 * ```js
11087 * var CanvasLayer = L.GridLayer.extend({
11088 * createTile: function(coords, done){
11089 * var error;
11090 *
11091 * // create a <canvas> element for drawing
11092 * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
11093 *
11094 * // setup tile width and height according to the options
11095 * var size = this.getTileSize();
11096 * tile.width = size.x;
11097 * tile.height = size.y;
11098 *
11099 * // draw something asynchronously and pass the tile to the done() callback
11100 * setTimeout(function() {
11101 * done(error, tile);
11102 * }, 1000);
11103 *
11104 * return tile;
11105 * }
11106 * });
11107 * ```
11108 *
11109 * @section
11110 */
11111
11112
11113 var GridLayer = Layer.extend({
11114
11115 // @section
11116 // @aka GridLayer options
11117 options: {
11118 // @option tileSize: Number|Point = 256
11119 // Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
11120 tileSize: 256,
11121
11122 // @option opacity: Number = 1.0
11123 // Opacity of the tiles. Can be used in the `createTile()` function.
11124 opacity: 1,
11125
11126 // @option updateWhenIdle: Boolean = (depends)
11127 // Load new tiles only when panning ends.
11128 // `true` by default on mobile browsers, in order to avoid too many requests and keep smooth navigation.
11129 // `false` otherwise in order to display new tiles _during_ panning, since it is easy to pan outside the
11130 // [`keepBuffer`](#gridlayer-keepbuffer) option in desktop browsers.
11131 updateWhenIdle: Browser.mobile,
11132
11133 // @option updateWhenZooming: Boolean = true
11134 // By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends.
11135 updateWhenZooming: true,
11136
11137 // @option updateInterval: Number = 200
11138 // Tiles will not update more than once every `updateInterval` milliseconds when panning.
11139 updateInterval: 200,
11140
11141 // @option zIndex: Number = 1
11142 // The explicit zIndex of the tile layer.
11143 zIndex: 1,
11144
11145 // @option bounds: LatLngBounds = undefined
11146 // If set, tiles will only be loaded inside the set `LatLngBounds`.
11147 bounds: null,
11148
11149 // @option minZoom: Number = 0
11150 // The minimum zoom level down to which this layer will be displayed (inclusive).
11151 minZoom: 0,
11152
11153 // @option maxZoom: Number = undefined
11154 // The maximum zoom level up to which this layer will be displayed (inclusive).
11155 maxZoom: undefined,
11156
11157 // @option maxNativeZoom: Number = undefined
11158 // Maximum zoom number the tile source has available. If it is specified,
11159 // the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
11160 // from `maxNativeZoom` level and auto-scaled.
11161 maxNativeZoom: undefined,
11162
11163 // @option minNativeZoom: Number = undefined
11164 // Minimum zoom number the tile source has available. If it is specified,
11165 // the tiles on all zoom levels lower than `minNativeZoom` will be loaded
11166 // from `minNativeZoom` level and auto-scaled.
11167 minNativeZoom: undefined,
11168
11169 // @option noWrap: Boolean = false
11170 // Whether the layer is wrapped around the antimeridian. If `true`, the
11171 // GridLayer will only be displayed once at low zoom levels. Has no
11172 // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
11173 // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
11174 // tiles outside the CRS limits.
11175 noWrap: false,
11176
11177 // @option pane: String = 'tilePane'
11178 // `Map pane` where the grid layer will be added.
11179 pane: 'tilePane',
11180
11181 // @option className: String = ''
11182 // A custom class name to assign to the tile layer. Empty by default.
11183 className: '',
11184
11185 // @option keepBuffer: Number = 2
11186 // When panning the map, keep this many rows and columns of tiles before unloading them.
11187 keepBuffer: 2
11188 },
11189
11190 initialize: function (options) {
11191 setOptions(this, options);
11192 },
11193
11194 onAdd: function () {
11195 this._initContainer();
11196
11197 this._levels = {};
11198 this._tiles = {};
11199
11200 this._resetView(); // implicit _update() call
11201 },
11202
11203 beforeAdd: function (map) {
11204 map._addZoomLimit(this);
11205 },
11206
11207 onRemove: function (map) {
11208 this._removeAllTiles();
11209 remove(this._container);
11210 map._removeZoomLimit(this);
11211 this._container = null;
11212 this._tileZoom = undefined;
11213 },
11214
11215 // @method bringToFront: this
11216 // Brings the tile layer to the top of all tile layers.
11217 bringToFront: function () {
11218 if (this._map) {
11219 toFront(this._container);
11220 this._setAutoZIndex(Math.max);
11221 }
11222 return this;
11223 },
11224
11225 // @method bringToBack: this
11226 // Brings the tile layer to the bottom of all tile layers.
11227 bringToBack: function () {
11228 if (this._map) {
11229 toBack(this._container);
11230 this._setAutoZIndex(Math.min);
11231 }
11232 return this;
11233 },
11234
11235 // @method getContainer: HTMLElement
11236 // Returns the HTML element that contains the tiles for this layer.
11237 getContainer: function () {
11238 return this._container;
11239 },
11240
11241 // @method setOpacity(opacity: Number): this
11242 // Changes the [opacity](#gridlayer-opacity) of the grid layer.
11243 setOpacity: function (opacity) {
11244 this.options.opacity = opacity;
11245 this._updateOpacity();
11246 return this;
11247 },
11248
11249 // @method setZIndex(zIndex: Number): this
11250 // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
11251 setZIndex: function (zIndex) {
11252 this.options.zIndex = zIndex;
11253 this._updateZIndex();
11254
11255 return this;
11256 },
11257
11258 // @method isLoading: Boolean
11259 // Returns `true` if any tile in the grid layer has not finished loading.
11260 isLoading: function () {
11261 return this._loading;
11262 },
11263
11264 // @method redraw: this
11265 // Causes the layer to clear all the tiles and request them again.
11266 redraw: function () {
11267 if (this._map) {
11268 this._removeAllTiles();
11269 var tileZoom = this._clampZoom(this._map.getZoom());
11270 if (tileZoom !== this._tileZoom) {
11271 this._tileZoom = tileZoom;
11272 this._updateLevels();
11273 }
11274 this._update();
11275 }
11276 return this;
11277 },
11278
11279 getEvents: function () {
11280 var events = {
11281 viewprereset: this._invalidateAll,
11282 viewreset: this._resetView,
11283 zoom: this._resetView,
11284 moveend: this._onMoveEnd
11285 };
11286
11287 if (!this.options.updateWhenIdle) {
11288 // update tiles on move, but not more often than once per given interval
11289 if (!this._onMove) {
11290 this._onMove = throttle(this._onMoveEnd, this.options.updateInterval, this);
11291 }
11292
11293 events.move = this._onMove;
11294 }
11295
11296 if (this._zoomAnimated) {
11297 events.zoomanim = this._animateZoom;
11298 }
11299
11300 return events;
11301 },
11302
11303 // @section Extension methods
11304 // Layers extending `GridLayer` shall reimplement the following method.
11305 // @method createTile(coords: Object, done?: Function): HTMLElement
11306 // Called only internally, must be overridden by classes extending `GridLayer`.
11307 // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
11308 // is specified, it must be called when the tile has finished loading and drawing.
11309 createTile: function () {
11310 return document.createElement('div');
11311 },
11312
11313 // @section
11314 // @method getTileSize: Point
11315 // Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
11316 getTileSize: function () {
11317 var s = this.options.tileSize;
11318 return s instanceof Point ? s : new Point(s, s);
11319 },
11320
11321 _updateZIndex: function () {
11322 if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
11323 this._container.style.zIndex = this.options.zIndex;
11324 }
11325 },
11326
11327 _setAutoZIndex: function (compare) {
11328 // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
11329
11330 var layers = this.getPane().children,
11331 edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
11332
11333 for (var i = 0, len = layers.length, zIndex; i < len; i++) {
11334
11335 zIndex = layers[i].style.zIndex;
11336
11337 if (layers[i] !== this._container && zIndex) {
11338 edgeZIndex = compare(edgeZIndex, +zIndex);
11339 }
11340 }
11341
11342 if (isFinite(edgeZIndex)) {
11343 this.options.zIndex = edgeZIndex + compare(-1, 1);
11344 this._updateZIndex();
11345 }
11346 },
11347
11348 _updateOpacity: function () {
11349 if (!this._map) { return; }
11350
11351 // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
11352 if (Browser.ielt9) { return; }
11353
11354 setOpacity(this._container, this.options.opacity);
11355
11356 var now = +new Date(),
11357 nextFrame = false,
11358 willPrune = false;
11359
11360 for (var key in this._tiles) {
11361 var tile = this._tiles[key];
11362 if (!tile.current || !tile.loaded) { continue; }
11363
11364 var fade = Math.min(1, (now - tile.loaded) / 200);
11365
11366 setOpacity(tile.el, fade);
11367 if (fade < 1) {
11368 nextFrame = true;
11369 } else {
11370 if (tile.active) {
11371 willPrune = true;
11372 } else {
11373 this._onOpaqueTile(tile);
11374 }
11375 tile.active = true;
11376 }
11377 }
11378
11379 if (willPrune && !this._noPrune) { this._pruneTiles(); }
11380
11381 if (nextFrame) {
11382 cancelAnimFrame(this._fadeFrame);
11383 this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
11384 }
11385 },
11386
11387 _onOpaqueTile: falseFn,
11388
11389 _initContainer: function () {
11390 if (this._container) { return; }
11391
11392 this._container = create$1('div', 'leaflet-layer ' + (this.options.className || ''));
11393 this._updateZIndex();
11394
11395 if (this.options.opacity < 1) {
11396 this._updateOpacity();
11397 }
11398
11399 this.getPane().appendChild(this._container);
11400 },
11401
11402 _updateLevels: function () {
11403
11404 var zoom = this._tileZoom,
11405 maxZoom = this.options.maxZoom;
11406
11407 if (zoom === undefined) { return undefined; }
11408
11409 for (var z in this._levels) {
11410 z = Number(z);
11411 if (this._levels[z].el.children.length || z === zoom) {
11412 this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
11413 this._onUpdateLevel(z);
11414 } else {
11415 remove(this._levels[z].el);
11416 this._removeTilesAtZoom(z);
11417 this._onRemoveLevel(z);
11418 delete this._levels[z];
11419 }
11420 }
11421
11422 var level = this._levels[zoom],
11423 map = this._map;
11424
11425 if (!level) {
11426 level = this._levels[zoom] = {};
11427
11428 level.el = create$1('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
11429 level.el.style.zIndex = maxZoom;
11430
11431 level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
11432 level.zoom = zoom;
11433
11434 this._setZoomTransform(level, map.getCenter(), map.getZoom());
11435
11436 // force the browser to consider the newly added element for transition
11437 falseFn(level.el.offsetWidth);
11438
11439 this._onCreateLevel(level);
11440 }
11441
11442 this._level = level;
11443
11444 return level;
11445 },
11446
11447 _onUpdateLevel: falseFn,
11448
11449 _onRemoveLevel: falseFn,
11450
11451 _onCreateLevel: falseFn,
11452
11453 _pruneTiles: function () {
11454 if (!this._map) {
11455 return;
11456 }
11457
11458 var key, tile;
11459
11460 var zoom = this._map.getZoom();
11461 if (zoom > this.options.maxZoom ||
11462 zoom < this.options.minZoom) {
11463 this._removeAllTiles();
11464 return;
11465 }
11466
11467 for (key in this._tiles) {
11468 tile = this._tiles[key];
11469 tile.retain = tile.current;
11470 }
11471
11472 for (key in this._tiles) {
11473 tile = this._tiles[key];
11474 if (tile.current && !tile.active) {
11475 var coords = tile.coords;
11476 if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
11477 this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
11478 }
11479 }
11480 }
11481
11482 for (key in this._tiles) {
11483 if (!this._tiles[key].retain) {
11484 this._removeTile(key);
11485 }
11486 }
11487 },
11488
11489 _removeTilesAtZoom: function (zoom) {
11490 for (var key in this._tiles) {
11491 if (this._tiles[key].coords.z !== zoom) {
11492 continue;
11493 }
11494 this._removeTile(key);
11495 }
11496 },
11497
11498 _removeAllTiles: function () {
11499 for (var key in this._tiles) {
11500 this._removeTile(key);
11501 }
11502 },
11503
11504 _invalidateAll: function () {
11505 for (var z in this._levels) {
11506 remove(this._levels[z].el);
11507 this._onRemoveLevel(Number(z));
11508 delete this._levels[z];
11509 }
11510 this._removeAllTiles();
11511
11512 this._tileZoom = undefined;
11513 },
11514
11515 _retainParent: function (x, y, z, minZoom) {
11516 var x2 = Math.floor(x / 2),
11517 y2 = Math.floor(y / 2),
11518 z2 = z - 1,
11519 coords2 = new Point(+x2, +y2);
11520 coords2.z = +z2;
11521
11522 var key = this._tileCoordsToKey(coords2),
11523 tile = this._tiles[key];
11524
11525 if (tile && tile.active) {
11526 tile.retain = true;
11527 return true;
11528
11529 } else if (tile && tile.loaded) {
11530 tile.retain = true;
11531 }
11532
11533 if (z2 > minZoom) {
11534 return this._retainParent(x2, y2, z2, minZoom);
11535 }
11536
11537 return false;
11538 },
11539
11540 _retainChildren: function (x, y, z, maxZoom) {
11541
11542 for (var i = 2 * x; i < 2 * x + 2; i++) {
11543 for (var j = 2 * y; j < 2 * y + 2; j++) {
11544
11545 var coords = new Point(i, j);
11546 coords.z = z + 1;
11547
11548 var key = this._tileCoordsToKey(coords),
11549 tile = this._tiles[key];
11550
11551 if (tile && tile.active) {
11552 tile.retain = true;
11553 continue;
11554
11555 } else if (tile && tile.loaded) {
11556 tile.retain = true;
11557 }
11558
11559 if (z + 1 < maxZoom) {
11560 this._retainChildren(i, j, z + 1, maxZoom);
11561 }
11562 }
11563 }
11564 },
11565
11566 _resetView: function (e) {
11567 var animating = e && (e.pinch || e.flyTo);
11568 this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
11569 },
11570
11571 _animateZoom: function (e) {
11572 this._setView(e.center, e.zoom, true, e.noUpdate);
11573 },
11574
11575 _clampZoom: function (zoom) {
11576 var options = this.options;
11577
11578 if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) {
11579 return options.minNativeZoom;
11580 }
11581
11582 if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) {
11583 return options.maxNativeZoom;
11584 }
11585
11586 return zoom;
11587 },
11588
11589 _setView: function (center, zoom, noPrune, noUpdate) {
11590 var tileZoom = Math.round(zoom);
11591 if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
11592 (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
11593 tileZoom = undefined;
11594 } else {
11595 tileZoom = this._clampZoom(tileZoom);
11596 }
11597
11598 var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);
11599
11600 if (!noUpdate || tileZoomChanged) {
11601
11602 this._tileZoom = tileZoom;
11603
11604 if (this._abortLoading) {
11605 this._abortLoading();
11606 }
11607
11608 this._updateLevels();
11609 this._resetGrid();
11610
11611 if (tileZoom !== undefined) {
11612 this._update(center);
11613 }
11614
11615 if (!noPrune) {
11616 this._pruneTiles();
11617 }
11618
11619 // Flag to prevent _updateOpacity from pruning tiles during
11620 // a zoom anim or a pinch gesture
11621 this._noPrune = !!noPrune;
11622 }
11623
11624 this._setZoomTransforms(center, zoom);
11625 },
11626
11627 _setZoomTransforms: function (center, zoom) {
11628 for (var i in this._levels) {
11629 this._setZoomTransform(this._levels[i], center, zoom);
11630 }
11631 },
11632
11633 _setZoomTransform: function (level, center, zoom) {
11634 var scale = this._map.getZoomScale(zoom, level.zoom),
11635 translate = level.origin.multiplyBy(scale)
11636 .subtract(this._map._getNewPixelOrigin(center, zoom)).round();
11637
11638 if (Browser.any3d) {
11639 setTransform(level.el, translate, scale);
11640 } else {
11641 setPosition(level.el, translate);
11642 }
11643 },
11644
11645 _resetGrid: function () {
11646 var map = this._map,
11647 crs = map.options.crs,
11648 tileSize = this._tileSize = this.getTileSize(),
11649 tileZoom = this._tileZoom;
11650
11651 var bounds = this._map.getPixelWorldBounds(this._tileZoom);
11652 if (bounds) {
11653 this._globalTileRange = this._pxBoundsToTileRange(bounds);
11654 }
11655
11656 this._wrapX = crs.wrapLng && !this.options.noWrap && [
11657 Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
11658 Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
11659 ];
11660 this._wrapY = crs.wrapLat && !this.options.noWrap && [
11661 Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
11662 Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
11663 ];
11664 },
11665
11666 _onMoveEnd: function () {
11667 if (!this._map || this._map._animatingZoom) { return; }
11668
11669 this._update();
11670 },
11671
11672 _getTiledPixelBounds: function (center) {
11673 var map = this._map,
11674 mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
11675 scale = map.getZoomScale(mapZoom, this._tileZoom),
11676 pixelCenter = map.project(center, this._tileZoom).floor(),
11677 halfSize = map.getSize().divideBy(scale * 2);
11678
11679 return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
11680 },
11681
11682 // Private method to load tiles in the grid's active zoom level according to map bounds
11683 _update: function (center) {
11684 var map = this._map;
11685 if (!map) { return; }
11686 var zoom = this._clampZoom(map.getZoom());
11687
11688 if (center === undefined) { center = map.getCenter(); }
11689 if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom
11690
11691 var pixelBounds = this._getTiledPixelBounds(center),
11692 tileRange = this._pxBoundsToTileRange(pixelBounds),
11693 tileCenter = tileRange.getCenter(),
11694 queue = [],
11695 margin = this.options.keepBuffer,
11696 noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
11697 tileRange.getTopRight().add([margin, -margin]));
11698
11699 // Sanity check: panic if the tile range contains Infinity somewhere.
11700 if (!(isFinite(tileRange.min.x) &&
11701 isFinite(tileRange.min.y) &&
11702 isFinite(tileRange.max.x) &&
11703 isFinite(tileRange.max.y))) { throw new Error('Attempted to load an infinite number of tiles'); }
11704
11705 for (var key in this._tiles) {
11706 var c = this._tiles[key].coords;
11707 if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) {
11708 this._tiles[key].current = false;
11709 }
11710 }
11711
11712 // _update just loads more tiles. If the tile zoom level differs too much
11713 // from the map's, let _setView reset levels and prune old tiles.
11714 if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }
11715
11716 // create a queue of coordinates to load tiles from
11717 for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
11718 for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
11719 var coords = new Point(i, j);
11720 coords.z = this._tileZoom;
11721
11722 if (!this._isValidTile(coords)) { continue; }
11723
11724 var tile = this._tiles[this._tileCoordsToKey(coords)];
11725 if (tile) {
11726 tile.current = true;
11727 } else {
11728 queue.push(coords);
11729 }
11730 }
11731 }
11732
11733 // sort tile queue to load tiles in order of their distance to center
11734 queue.sort(function (a, b) {
11735 return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
11736 });
11737
11738 if (queue.length !== 0) {
11739 // if it's the first batch of tiles to load
11740 if (!this._loading) {
11741 this._loading = true;
11742 // @event loading: Event
11743 // Fired when the grid layer starts loading tiles.
11744 this.fire('loading');
11745 }
11746
11747 // create DOM fragment to append tiles in one batch
11748 var fragment = document.createDocumentFragment();
11749
11750 for (i = 0; i < queue.length; i++) {
11751 this._addTile(queue[i], fragment);
11752 }
11753
11754 this._level.el.appendChild(fragment);
11755 }
11756 },
11757
11758 _isValidTile: function (coords) {
11759 var crs = this._map.options.crs;
11760
11761 if (!crs.infinite) {
11762 // don't load tile if it's out of bounds and not wrapped
11763 var bounds = this._globalTileRange;
11764 if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
11765 (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
11766 }
11767
11768 if (!this.options.bounds) { return true; }
11769
11770 // don't load tile if it doesn't intersect the bounds in options
11771 var tileBounds = this._tileCoordsToBounds(coords);
11772 return toLatLngBounds(this.options.bounds).overlaps(tileBounds);
11773 },
11774
11775 _keyToBounds: function (key) {
11776 return this._tileCoordsToBounds(this._keyToTileCoords(key));
11777 },
11778
11779 _tileCoordsToNwSe: function (coords) {
11780 var map = this._map,
11781 tileSize = this.getTileSize(),
11782 nwPoint = coords.scaleBy(tileSize),
11783 sePoint = nwPoint.add(tileSize),
11784 nw = map.unproject(nwPoint, coords.z),
11785 se = map.unproject(sePoint, coords.z);
11786 return [nw, se];
11787 },
11788
11789 // converts tile coordinates to its geographical bounds
11790 _tileCoordsToBounds: function (coords) {
11791 var bp = this._tileCoordsToNwSe(coords),
11792 bounds = new LatLngBounds(bp[0], bp[1]);
11793
11794 if (!this.options.noWrap) {
11795 bounds = this._map.wrapLatLngBounds(bounds);
11796 }
11797 return bounds;
11798 },
11799 // converts tile coordinates to key for the tile cache
11800 _tileCoordsToKey: function (coords) {
11801 return coords.x + ':' + coords.y + ':' + coords.z;
11802 },
11803
11804 // converts tile cache key to coordinates
11805 _keyToTileCoords: function (key) {
11806 var k = key.split(':'),
11807 coords = new Point(+k[0], +k[1]);
11808 coords.z = +k[2];
11809 return coords;
11810 },
11811
11812 _removeTile: function (key) {
11813 var tile = this._tiles[key];
11814 if (!tile) { return; }
11815
11816 remove(tile.el);
11817
11818 delete this._tiles[key];
11819
11820 // @event tileunload: TileEvent
11821 // Fired when a tile is removed (e.g. when a tile goes off the screen).
11822 this.fire('tileunload', {
11823 tile: tile.el,
11824 coords: this._keyToTileCoords(key)
11825 });
11826 },
11827
11828 _initTile: function (tile) {
11829 addClass(tile, 'leaflet-tile');
11830
11831 var tileSize = this.getTileSize();
11832 tile.style.width = tileSize.x + 'px';
11833 tile.style.height = tileSize.y + 'px';
11834
11835 tile.onselectstart = falseFn;
11836 tile.onmousemove = falseFn;
11837
11838 // update opacity on tiles in IE7-8 because of filter inheritance problems
11839 if (Browser.ielt9 && this.options.opacity < 1) {
11840 setOpacity(tile, this.options.opacity);
11841 }
11842 },
11843
11844 _addTile: function (coords, container) {
11845 var tilePos = this._getTilePos(coords),
11846 key = this._tileCoordsToKey(coords);
11847
11848 var tile = this.createTile(this._wrapCoords(coords), bind(this._tileReady, this, coords));
11849
11850 this._initTile(tile);
11851
11852 // if createTile is defined with a second argument ("done" callback),
11853 // we know that tile is async and will be ready later; otherwise
11854 if (this.createTile.length < 2) {
11855 // mark tile as ready, but delay one frame for opacity animation to happen
11856 requestAnimFrame(bind(this._tileReady, this, coords, null, tile));
11857 }
11858
11859 setPosition(tile, tilePos);
11860
11861 // save tile in cache
11862 this._tiles[key] = {
11863 el: tile,
11864 coords: coords,
11865 current: true
11866 };
11867
11868 container.appendChild(tile);
11869 // @event tileloadstart: TileEvent
11870 // Fired when a tile is requested and starts loading.
11871 this.fire('tileloadstart', {
11872 tile: tile,
11873 coords: coords
11874 });
11875 },
11876
11877 _tileReady: function (coords, err, tile) {
11878 if (err) {
11879 // @event tileerror: TileErrorEvent
11880 // Fired when there is an error loading a tile.
11881 this.fire('tileerror', {
11882 error: err,
11883 tile: tile,
11884 coords: coords
11885 });
11886 }
11887
11888 var key = this._tileCoordsToKey(coords);
11889
11890 tile = this._tiles[key];
11891 if (!tile) { return; }
11892
11893 tile.loaded = +new Date();
11894 if (this._map._fadeAnimated) {
11895 setOpacity(tile.el, 0);
11896 cancelAnimFrame(this._fadeFrame);
11897 this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
11898 } else {
11899 tile.active = true;
11900 this._pruneTiles();
11901 }
11902
11903 if (!err) {
11904 addClass(tile.el, 'leaflet-tile-loaded');
11905
11906 // @event tileload: TileEvent
11907 // Fired when a tile loads.
11908 this.fire('tileload', {
11909 tile: tile.el,
11910 coords: coords
11911 });
11912 }
11913
11914 if (this._noTilesToLoad()) {
11915 this._loading = false;
11916 // @event load: Event
11917 // Fired when the grid layer loaded all visible tiles.
11918 this.fire('load');
11919
11920 if (Browser.ielt9 || !this._map._fadeAnimated) {
11921 requestAnimFrame(this._pruneTiles, this);
11922 } else {
11923 // Wait a bit more than 0.2 secs (the duration of the tile fade-in)
11924 // to trigger a pruning.
11925 setTimeout(bind(this._pruneTiles, this), 250);
11926 }
11927 }
11928 },
11929
11930 _getTilePos: function (coords) {
11931 return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
11932 },
11933
11934 _wrapCoords: function (coords) {
11935 var newCoords = new Point(
11936 this._wrapX ? wrapNum(coords.x, this._wrapX) : coords.x,
11937 this._wrapY ? wrapNum(coords.y, this._wrapY) : coords.y);
11938 newCoords.z = coords.z;
11939 return newCoords;
11940 },
11941
11942 _pxBoundsToTileRange: function (bounds) {
11943 var tileSize = this.getTileSize();
11944 return new Bounds(
11945 bounds.min.unscaleBy(tileSize).floor(),
11946 bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
11947 },
11948
11949 _noTilesToLoad: function () {
11950 for (var key in this._tiles) {
11951 if (!this._tiles[key].loaded) { return false; }
11952 }
11953 return true;
11954 }
11955 });
11956
11957 // @factory L.gridLayer(options?: GridLayer options)
11958 // Creates a new instance of GridLayer with the supplied options.
11959 function gridLayer(options) {
11960 return new GridLayer(options);
11961 }
11962
11963 /*
11964 * @class TileLayer
11965 * @inherits GridLayer
11966 * @aka L.TileLayer
11967 * Used to load and display tile layers on the map. Note that most tile servers require attribution, which you can set under `Layer`. Extends `GridLayer`.
11968 *
11969 * @example
11970 *
11971 * ```js
11972 * L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);
11973 * ```
11974 *
11975 * @section URL template
11976 * @example
11977 *
11978 * A string of the following form:
11979 *
11980 * ```
11981 * 'https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
11982 * ```
11983 *
11984 * `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add "&commat;2x" to the URL to load retina tiles.
11985 *
11986 * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
11987 *
11988 * ```
11989 * L.tileLayer('https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
11990 * ```
11991 */
11992
11993
11994 var TileLayer = GridLayer.extend({
11995
11996 // @section
11997 // @aka TileLayer options
11998 options: {
11999 // @option minZoom: Number = 0
12000 // The minimum zoom level down to which this layer will be displayed (inclusive).
12001 minZoom: 0,
12002
12003 // @option maxZoom: Number = 18
12004 // The maximum zoom level up to which this layer will be displayed (inclusive).
12005 maxZoom: 18,
12006
12007 // @option subdomains: String|String[] = 'abc'
12008 // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings.
12009 subdomains: 'abc',
12010
12011 // @option errorTileUrl: String = ''
12012 // URL to the tile image to show in place of the tile that failed to load.
12013 errorTileUrl: '',
12014
12015 // @option zoomOffset: Number = 0
12016 // The zoom number used in tile URLs will be offset with this value.
12017 zoomOffset: 0,
12018
12019 // @option tms: Boolean = false
12020 // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
12021 tms: false,
12022
12023 // @option zoomReverse: Boolean = false
12024 // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
12025 zoomReverse: false,
12026
12027 // @option detectRetina: Boolean = false
12028 // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.
12029 detectRetina: false,
12030
12031 // @option crossOrigin: Boolean|String = false
12032 // Whether the crossOrigin attribute will be added to the tiles.
12033 // If a String is provided, all tiles will have their crossOrigin attribute set to the String provided. This is needed if you want to access tile pixel data.
12034 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
12035 crossOrigin: false,
12036
12037 // @option referrerPolicy: Boolean|String = false
12038 // Whether the referrerPolicy attribute will be added to the tiles.
12039 // If a String is provided, all tiles will have their referrerPolicy attribute set to the String provided.
12040 // This may be needed if your map's rendering context has a strict default but your tile provider expects a valid referrer
12041 // (e.g. to validate an API token).
12042 // Refer to [HTMLImageElement.referrerPolicy](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/referrerPolicy) for valid String values.
12043 referrerPolicy: false
12044 },
12045
12046 initialize: function (url, options) {
12047
12048 this._url = url;
12049
12050 options = setOptions(this, options);
12051
12052 // detecting retina displays, adjusting tileSize and zoom levels
12053 if (options.detectRetina && Browser.retina && options.maxZoom > 0) {
12054
12055 options.tileSize = Math.floor(options.tileSize / 2);
12056
12057 if (!options.zoomReverse) {
12058 options.zoomOffset++;
12059 options.maxZoom = Math.max(options.minZoom, options.maxZoom - 1);
12060 } else {
12061 options.zoomOffset--;
12062 options.minZoom = Math.min(options.maxZoom, options.minZoom + 1);
12063 }
12064
12065 options.minZoom = Math.max(0, options.minZoom);
12066 } else if (!options.zoomReverse) {
12067 // make sure maxZoom is gte minZoom
12068 options.maxZoom = Math.max(options.minZoom, options.maxZoom);
12069 } else {
12070 // make sure minZoom is lte maxZoom
12071 options.minZoom = Math.min(options.maxZoom, options.minZoom);
12072 }
12073
12074 if (typeof options.subdomains === 'string') {
12075 options.subdomains = options.subdomains.split('');
12076 }
12077
12078 this.on('tileunload', this._onTileRemove);
12079 },
12080
12081 // @method setUrl(url: String, noRedraw?: Boolean): this
12082 // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
12083 // If the URL does not change, the layer will not be redrawn unless
12084 // the noRedraw parameter is set to false.
12085 setUrl: function (url, noRedraw) {
12086 if (this._url === url && noRedraw === undefined) {
12087 noRedraw = true;
12088 }
12089
12090 this._url = url;
12091
12092 if (!noRedraw) {
12093 this.redraw();
12094 }
12095 return this;
12096 },
12097
12098 // @method createTile(coords: Object, done?: Function): HTMLElement
12099 // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
12100 // to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done`
12101 // callback is called when the tile has been loaded.
12102 createTile: function (coords, done) {
12103 var tile = document.createElement('img');
12104
12105 on(tile, 'load', bind(this._tileOnLoad, this, done, tile));
12106 on(tile, 'error', bind(this._tileOnError, this, done, tile));
12107
12108 if (this.options.crossOrigin || this.options.crossOrigin === '') {
12109 tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
12110 }
12111
12112 // for this new option we follow the documented behavior
12113 // more closely by only setting the property when string
12114 if (typeof this.options.referrerPolicy === 'string') {
12115 tile.referrerPolicy = this.options.referrerPolicy;
12116 }
12117
12118 // The alt attribute is set to the empty string,
12119 // allowing screen readers to ignore the decorative image tiles.
12120 // https://www.w3.org/WAI/tutorials/images/decorative/
12121 // https://www.w3.org/TR/html-aria/#el-img-empty-alt
12122 tile.alt = '';
12123
12124 tile.src = this.getTileUrl(coords);
12125
12126 return tile;
12127 },
12128
12129 // @section Extension methods
12130 // @uninheritable
12131 // Layers extending `TileLayer` might reimplement the following method.
12132 // @method getTileUrl(coords: Object): String
12133 // Called only internally, returns the URL for a tile given its coordinates.
12134 // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
12135 getTileUrl: function (coords) {
12136 var data = {
12137 r: Browser.retina ? '@2x' : '',
12138 s: this._getSubdomain(coords),
12139 x: coords.x,
12140 y: coords.y,
12141 z: this._getZoomForUrl()
12142 };
12143 if (this._map && !this._map.options.crs.infinite) {
12144 var invertedY = this._globalTileRange.max.y - coords.y;
12145 if (this.options.tms) {
12146 data['y'] = invertedY;
12147 }
12148 data['-y'] = invertedY;
12149 }
12150
12151 return template(this._url, extend(data, this.options));
12152 },
12153
12154 _tileOnLoad: function (done, tile) {
12155 // For https://github.com/Leaflet/Leaflet/issues/3332
12156 if (Browser.ielt9) {
12157 setTimeout(bind(done, this, null, tile), 0);
12158 } else {
12159 done(null, tile);
12160 }
12161 },
12162
12163 _tileOnError: function (done, tile, e) {
12164 var errorUrl = this.options.errorTileUrl;
12165 if (errorUrl && tile.getAttribute('src') !== errorUrl) {
12166 tile.src = errorUrl;
12167 }
12168 done(e, tile);
12169 },
12170
12171 _onTileRemove: function (e) {
12172 e.tile.onload = null;
12173 },
12174
12175 _getZoomForUrl: function () {
12176 var zoom = this._tileZoom,
12177 maxZoom = this.options.maxZoom,
12178 zoomReverse = this.options.zoomReverse,
12179 zoomOffset = this.options.zoomOffset;
12180
12181 if (zoomReverse) {
12182 zoom = maxZoom - zoom;
12183 }
12184
12185 return zoom + zoomOffset;
12186 },
12187
12188 _getSubdomain: function (tilePoint) {
12189 var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
12190 return this.options.subdomains[index];
12191 },
12192
12193 // stops loading all tiles in the background layer
12194 _abortLoading: function () {
12195 var i, tile;
12196 for (i in this._tiles) {
12197 if (this._tiles[i].coords.z !== this._tileZoom) {
12198 tile = this._tiles[i].el;
12199
12200 tile.onload = falseFn;
12201 tile.onerror = falseFn;
12202
12203 if (!tile.complete) {
12204 tile.src = emptyImageUrl;
12205 var coords = this._tiles[i].coords;
12206 remove(tile);
12207 delete this._tiles[i];
12208 // @event tileabort: TileEvent
12209 // Fired when a tile was loading but is now not wanted.
12210 this.fire('tileabort', {
12211 tile: tile,
12212 coords: coords
12213 });
12214 }
12215 }
12216 }
12217 },
12218
12219 _removeTile: function (key) {
12220 var tile = this._tiles[key];
12221 if (!tile) { return; }
12222
12223 // Cancels any pending http requests associated with the tile
12224 tile.el.setAttribute('src', emptyImageUrl);
12225
12226 return GridLayer.prototype._removeTile.call(this, key);
12227 },
12228
12229 _tileReady: function (coords, err, tile) {
12230 if (!this._map || (tile && tile.getAttribute('src') === emptyImageUrl)) {
12231 return;
12232 }
12233
12234 return GridLayer.prototype._tileReady.call(this, coords, err, tile);
12235 }
12236 });
12237
12238
12239 // @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
12240 // Instantiates a tile layer object given a `URL template` and optionally an options object.
12241
12242 function tileLayer(url, options) {
12243 return new TileLayer(url, options);
12244 }
12245
12246 /*
12247 * @class TileLayer.WMS
12248 * @inherits TileLayer
12249 * @aka L.TileLayer.WMS
12250 * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
12251 *
12252 * @example
12253 *
12254 * ```js
12255 * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
12256 * layers: 'nexrad-n0r-900913',
12257 * format: 'image/png',
12258 * transparent: true,
12259 * attribution: "Weather data © 2012 IEM Nexrad"
12260 * });
12261 * ```
12262 */
12263
12264 var TileLayerWMS = TileLayer.extend({
12265
12266 // @section
12267 // @aka TileLayer.WMS options
12268 // If any custom options not documented here are used, they will be sent to the
12269 // WMS server as extra parameters in each request URL. This can be useful for
12270 // [non-standard vendor WMS parameters](https://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
12271 defaultWmsParams: {
12272 service: 'WMS',
12273 request: 'GetMap',
12274
12275 // @option layers: String = ''
12276 // **(required)** Comma-separated list of WMS layers to show.
12277 layers: '',
12278
12279 // @option styles: String = ''
12280 // Comma-separated list of WMS styles.
12281 styles: '',
12282
12283 // @option format: String = 'image/jpeg'
12284 // WMS image format (use `'image/png'` for layers with transparency).
12285 format: 'image/jpeg',
12286
12287 // @option transparent: Boolean = false
12288 // If `true`, the WMS service will return images with transparency.
12289 transparent: false,
12290
12291 // @option version: String = '1.1.1'
12292 // Version of the WMS service to use
12293 version: '1.1.1'
12294 },
12295
12296 options: {
12297 // @option crs: CRS = null
12298 // Coordinate Reference System to use for the WMS requests, defaults to
12299 // map CRS. Don't change this if you're not sure what it means.
12300 crs: null,
12301
12302 // @option uppercase: Boolean = false
12303 // If `true`, WMS request parameter keys will be uppercase.
12304 uppercase: false
12305 },
12306
12307 initialize: function (url, options) {
12308
12309 this._url = url;
12310
12311 var wmsParams = extend({}, this.defaultWmsParams);
12312
12313 // all keys that are not TileLayer options go to WMS params
12314 for (var i in options) {
12315 if (!(i in this.options)) {
12316 wmsParams[i] = options[i];
12317 }
12318 }
12319
12320 options = setOptions(this, options);
12321
12322 var realRetina = options.detectRetina && Browser.retina ? 2 : 1;
12323 var tileSize = this.getTileSize();
12324 wmsParams.width = tileSize.x * realRetina;
12325 wmsParams.height = tileSize.y * realRetina;
12326
12327 this.wmsParams = wmsParams;
12328 },
12329
12330 onAdd: function (map) {
12331
12332 this._crs = this.options.crs || map.options.crs;
12333 this._wmsVersion = parseFloat(this.wmsParams.version);
12334
12335 var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
12336 this.wmsParams[projectionKey] = this._crs.code;
12337
12338 TileLayer.prototype.onAdd.call(this, map);
12339 },
12340
12341 getTileUrl: function (coords) {
12342
12343 var tileBounds = this._tileCoordsToNwSe(coords),
12344 crs = this._crs,
12345 bounds = toBounds(crs.project(tileBounds[0]), crs.project(tileBounds[1])),
12346 min = bounds.min,
12347 max = bounds.max,
12348 bbox = (this._wmsVersion >= 1.3 && this._crs === EPSG4326 ?
12349 [min.y, min.x, max.y, max.x] :
12350 [min.x, min.y, max.x, max.y]).join(','),
12351 url = TileLayer.prototype.getTileUrl.call(this, coords);
12352 return url +
12353 getParamString(this.wmsParams, url, this.options.uppercase) +
12354 (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
12355 },
12356
12357 // @method setParams(params: Object, noRedraw?: Boolean): this
12358 // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
12359 setParams: function (params, noRedraw) {
12360
12361 extend(this.wmsParams, params);
12362
12363 if (!noRedraw) {
12364 this.redraw();
12365 }
12366
12367 return this;
12368 }
12369 });
12370
12371
12372 // @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
12373 // Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
12374 function tileLayerWMS(url, options) {
12375 return new TileLayerWMS(url, options);
12376 }
12377
12378 TileLayer.WMS = TileLayerWMS;
12379 tileLayer.wms = tileLayerWMS;
12380
12381 /*
12382 * @class Renderer
12383 * @inherits Layer
12384 * @aka L.Renderer
12385 *
12386 * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
12387 * DOM container of the renderer, its bounds, and its zoom animation.
12388 *
12389 * A `Renderer` works as an implicit layer group for all `Path`s - the renderer
12390 * itself can be added or removed to the map. All paths use a renderer, which can
12391 * be implicit (the map will decide the type of renderer and use it automatically)
12392 * or explicit (using the [`renderer`](#path-renderer) option of the path).
12393 *
12394 * Do not use this class directly, use `SVG` and `Canvas` instead.
12395 *
12396 * @event update: Event
12397 * Fired when the renderer updates its bounds, center and zoom, for example when
12398 * its map has moved
12399 */
12400
12401 var Renderer = Layer.extend({
12402
12403 // @section
12404 // @aka Renderer options
12405 options: {
12406 // @option padding: Number = 0.1
12407 // How much to extend the clip area around the map view (relative to its size)
12408 // e.g. 0.1 would be 10% of map view in each direction
12409 padding: 0.1
12410 },
12411
12412 initialize: function (options) {
12413 setOptions(this, options);
12414 stamp(this);
12415 this._layers = this._layers || {};
12416 },
12417
12418 onAdd: function () {
12419 if (!this._container) {
12420 this._initContainer(); // defined by renderer implementations
12421
12422 if (this._zoomAnimated) {
12423 addClass(this._container, 'leaflet-zoom-animated');
12424 }
12425 }
12426
12427 this.getPane().appendChild(this._container);
12428 this._update();
12429 this.on('update', this._updatePaths, this);
12430 },
12431
12432 onRemove: function () {
12433 this.off('update', this._updatePaths, this);
12434 this._destroyContainer();
12435 },
12436
12437 getEvents: function () {
12438 var events = {
12439 viewreset: this._reset,
12440 zoom: this._onZoom,
12441 moveend: this._update,
12442 zoomend: this._onZoomEnd
12443 };
12444 if (this._zoomAnimated) {
12445 events.zoomanim = this._onAnimZoom;
12446 }
12447 return events;
12448 },
12449
12450 _onAnimZoom: function (ev) {
12451 this._updateTransform(ev.center, ev.zoom);
12452 },
12453
12454 _onZoom: function () {
12455 this._updateTransform(this._map.getCenter(), this._map.getZoom());
12456 },
12457
12458 _updateTransform: function (center, zoom) {
12459 var scale = this._map.getZoomScale(zoom, this._zoom),
12460 viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
12461 currentCenterPoint = this._map.project(this._center, zoom),
12462
12463 topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint)
12464 .subtract(this._map._getNewPixelOrigin(center, zoom));
12465
12466 if (Browser.any3d) {
12467 setTransform(this._container, topLeftOffset, scale);
12468 } else {
12469 setPosition(this._container, topLeftOffset);
12470 }
12471 },
12472
12473 _reset: function () {
12474 this._update();
12475 this._updateTransform(this._center, this._zoom);
12476
12477 for (var id in this._layers) {
12478 this._layers[id]._reset();
12479 }
12480 },
12481
12482 _onZoomEnd: function () {
12483 for (var id in this._layers) {
12484 this._layers[id]._project();
12485 }
12486 },
12487
12488 _updatePaths: function () {
12489 for (var id in this._layers) {
12490 this._layers[id]._update();
12491 }
12492 },
12493
12494 _update: function () {
12495 // Update pixel bounds of renderer container (for positioning/sizing/clipping later)
12496 // Subclasses are responsible of firing the 'update' event.
12497 var p = this.options.padding,
12498 size = this._map.getSize(),
12499 min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
12500
12501 this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
12502
12503 this._center = this._map.getCenter();
12504 this._zoom = this._map.getZoom();
12505 }
12506 });
12507
12508 /*
12509 * @class Canvas
12510 * @inherits Renderer
12511 * @aka L.Canvas
12512 *
12513 * Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
12514 * Inherits `Renderer`.
12515 *
12516 * Due to [technical limitations](https://caniuse.com/canvas), Canvas is not
12517 * available in all web browsers, notably IE8, and overlapping geometries might
12518 * not display properly in some edge cases.
12519 *
12520 * @example
12521 *
12522 * Use Canvas by default for all paths in the map:
12523 *
12524 * ```js
12525 * var map = L.map('map', {
12526 * renderer: L.canvas()
12527 * });
12528 * ```
12529 *
12530 * Use a Canvas renderer with extra padding for specific vector geometries:
12531 *
12532 * ```js
12533 * var map = L.map('map');
12534 * var myRenderer = L.canvas({ padding: 0.5 });
12535 * var line = L.polyline( coordinates, { renderer: myRenderer } );
12536 * var circle = L.circle( center, { renderer: myRenderer } );
12537 * ```
12538 */
12539
12540 var Canvas = Renderer.extend({
12541
12542 // @section
12543 // @aka Canvas options
12544 options: {
12545 // @option tolerance: Number = 0
12546 // How much to extend the click tolerance around a path/object on the map.
12547 tolerance: 0
12548 },
12549
12550 getEvents: function () {
12551 var events = Renderer.prototype.getEvents.call(this);
12552 events.viewprereset = this._onViewPreReset;
12553 return events;
12554 },
12555
12556 _onViewPreReset: function () {
12557 // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once
12558 this._postponeUpdatePaths = true;
12559 },
12560
12561 onAdd: function () {
12562 Renderer.prototype.onAdd.call(this);
12563
12564 // Redraw vectors since canvas is cleared upon removal,
12565 // in case of removing the renderer itself from the map.
12566 this._draw();
12567 },
12568
12569 _initContainer: function () {
12570 var container = this._container = document.createElement('canvas');
12571
12572 on(container, 'mousemove', this._onMouseMove, this);
12573 on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this);
12574 on(container, 'mouseout', this._handleMouseOut, this);
12575 container['_leaflet_disable_events'] = true;
12576
12577 this._ctx = container.getContext('2d');
12578 },
12579
12580 _destroyContainer: function () {
12581 cancelAnimFrame(this._redrawRequest);
12582 delete this._ctx;
12583 remove(this._container);
12584 off(this._container);
12585 delete this._container;
12586 },
12587
12588 _updatePaths: function () {
12589 if (this._postponeUpdatePaths) { return; }
12590
12591 var layer;
12592 this._redrawBounds = null;
12593 for (var id in this._layers) {
12594 layer = this._layers[id];
12595 layer._update();
12596 }
12597 this._redraw();
12598 },
12599
12600 _update: function () {
12601 if (this._map._animatingZoom && this._bounds) { return; }
12602
12603 Renderer.prototype._update.call(this);
12604
12605 var b = this._bounds,
12606 container = this._container,
12607 size = b.getSize(),
12608 m = Browser.retina ? 2 : 1;
12609
12610 setPosition(container, b.min);
12611
12612 // set canvas size (also clearing it); use double size on retina
12613 container.width = m * size.x;
12614 container.height = m * size.y;
12615 container.style.width = size.x + 'px';
12616 container.style.height = size.y + 'px';
12617
12618 if (Browser.retina) {
12619 this._ctx.scale(2, 2);
12620 }
12621
12622 // translate so we use the same path coordinates after canvas element moves
12623 this._ctx.translate(-b.min.x, -b.min.y);
12624
12625 // Tell paths to redraw themselves
12626 this.fire('update');
12627 },
12628
12629 _reset: function () {
12630 Renderer.prototype._reset.call(this);
12631
12632 if (this._postponeUpdatePaths) {
12633 this._postponeUpdatePaths = false;
12634 this._updatePaths();
12635 }
12636 },
12637
12638 _initPath: function (layer) {
12639 this._updateDashArray(layer);
12640 this._layers[stamp(layer)] = layer;
12641
12642 var order = layer._order = {
12643 layer: layer,
12644 prev: this._drawLast,
12645 next: null
12646 };
12647 if (this._drawLast) { this._drawLast.next = order; }
12648 this._drawLast = order;
12649 this._drawFirst = this._drawFirst || this._drawLast;
12650 },
12651
12652 _addPath: function (layer) {
12653 this._requestRedraw(layer);
12654 },
12655
12656 _removePath: function (layer) {
12657 var order = layer._order;
12658 var next = order.next;
12659 var prev = order.prev;
12660
12661 if (next) {
12662 next.prev = prev;
12663 } else {
12664 this._drawLast = prev;
12665 }
12666 if (prev) {
12667 prev.next = next;
12668 } else {
12669 this._drawFirst = next;
12670 }
12671
12672 delete layer._order;
12673
12674 delete this._layers[stamp(layer)];
12675
12676 this._requestRedraw(layer);
12677 },
12678
12679 _updatePath: function (layer) {
12680 // Redraw the union of the layer's old pixel
12681 // bounds and the new pixel bounds.
12682 this._extendRedrawBounds(layer);
12683 layer._project();
12684 layer._update();
12685 // The redraw will extend the redraw bounds
12686 // with the new pixel bounds.
12687 this._requestRedraw(layer);
12688 },
12689
12690 _updateStyle: function (layer) {
12691 this._updateDashArray(layer);
12692 this._requestRedraw(layer);
12693 },
12694
12695 _updateDashArray: function (layer) {
12696 if (typeof layer.options.dashArray === 'string') {
12697 var parts = layer.options.dashArray.split(/[, ]+/),
12698 dashArray = [],
12699 dashValue,
12700 i;
12701 for (i = 0; i < parts.length; i++) {
12702 dashValue = Number(parts[i]);
12703 // Ignore dash array containing invalid lengths
12704 if (isNaN(dashValue)) { return; }
12705 dashArray.push(dashValue);
12706 }
12707 layer.options._dashArray = dashArray;
12708 } else {
12709 layer.options._dashArray = layer.options.dashArray;
12710 }
12711 },
12712
12713 _requestRedraw: function (layer) {
12714 if (!this._map) { return; }
12715
12716 this._extendRedrawBounds(layer);
12717 this._redrawRequest = this._redrawRequest || requestAnimFrame(this._redraw, this);
12718 },
12719
12720 _extendRedrawBounds: function (layer) {
12721 if (layer._pxBounds) {
12722 var padding = (layer.options.weight || 0) + 1;
12723 this._redrawBounds = this._redrawBounds || new Bounds();
12724 this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
12725 this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
12726 }
12727 },
12728
12729 _redraw: function () {
12730 this._redrawRequest = null;
12731
12732 if (this._redrawBounds) {
12733 this._redrawBounds.min._floor();
12734 this._redrawBounds.max._ceil();
12735 }
12736
12737 this._clear(); // clear layers in redraw bounds
12738 this._draw(); // draw layers
12739
12740 this._redrawBounds = null;
12741 },
12742
12743 _clear: function () {
12744 var bounds = this._redrawBounds;
12745 if (bounds) {
12746 var size = bounds.getSize();
12747 this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
12748 } else {
12749 this._ctx.save();
12750 this._ctx.setTransform(1, 0, 0, 1, 0, 0);
12751 this._ctx.clearRect(0, 0, this._container.width, this._container.height);
12752 this._ctx.restore();
12753 }
12754 },
12755
12756 _draw: function () {
12757 var layer, bounds = this._redrawBounds;
12758 this._ctx.save();
12759 if (bounds) {
12760 var size = bounds.getSize();
12761 this._ctx.beginPath();
12762 this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y);
12763 this._ctx.clip();
12764 }
12765
12766 this._drawing = true;
12767
12768 for (var order = this._drawFirst; order; order = order.next) {
12769 layer = order.layer;
12770 if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) {
12771 layer._updatePath();
12772 }
12773 }
12774
12775 this._drawing = false;
12776
12777 this._ctx.restore(); // Restore state before clipping.
12778 },
12779
12780 _updatePoly: function (layer, closed) {
12781 if (!this._drawing) { return; }
12782
12783 var i, j, len2, p,
12784 parts = layer._parts,
12785 len = parts.length,
12786 ctx = this._ctx;
12787
12788 if (!len) { return; }
12789
12790 ctx.beginPath();
12791
12792 for (i = 0; i < len; i++) {
12793 for (j = 0, len2 = parts[i].length; j < len2; j++) {
12794 p = parts[i][j];
12795 ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y);
12796 }
12797 if (closed) {
12798 ctx.closePath();
12799 }
12800 }
12801
12802 this._fillStroke(ctx, layer);
12803
12804 // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
12805 },
12806
12807 _updateCircle: function (layer) {
12808
12809 if (!this._drawing || layer._empty()) { return; }
12810
12811 var p = layer._point,
12812 ctx = this._ctx,
12813 r = Math.max(Math.round(layer._radius), 1),
12814 s = (Math.max(Math.round(layer._radiusY), 1) || r) / r;
12815
12816 if (s !== 1) {
12817 ctx.save();
12818 ctx.scale(1, s);
12819 }
12820
12821 ctx.beginPath();
12822 ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false);
12823
12824 if (s !== 1) {
12825 ctx.restore();
12826 }
12827
12828 this._fillStroke(ctx, layer);
12829 },
12830
12831 _fillStroke: function (ctx, layer) {
12832 var options = layer.options;
12833
12834 if (options.fill) {
12835 ctx.globalAlpha = options.fillOpacity;
12836 ctx.fillStyle = options.fillColor || options.color;
12837 ctx.fill(options.fillRule || 'evenodd');
12838 }
12839
12840 if (options.stroke && options.weight !== 0) {
12841 if (ctx.setLineDash) {
12842 ctx.setLineDash(layer.options && layer.options._dashArray || []);
12843 }
12844 ctx.globalAlpha = options.opacity;
12845 ctx.lineWidth = options.weight;
12846 ctx.strokeStyle = options.color;
12847 ctx.lineCap = options.lineCap;
12848 ctx.lineJoin = options.lineJoin;
12849 ctx.stroke();
12850 }
12851 },
12852
12853 // Canvas obviously doesn't have mouse events for individual drawn objects,
12854 // so we emulate that by calculating what's under the mouse on mousemove/click manually
12855
12856 _onClick: function (e) {
12857 var point = this._map.mouseEventToLayerPoint(e), layer, clickedLayer;
12858
12859 for (var order = this._drawFirst; order; order = order.next) {
12860 layer = order.layer;
12861 if (layer.options.interactive && layer._containsPoint(point)) {
12862 if (!(e.type === 'click' || e.type === 'preclick') || !this._map._draggableMoved(layer)) {
12863 clickedLayer = layer;
12864 }
12865 }
12866 }
12867 this._fireEvent(clickedLayer ? [clickedLayer] : false, e);
12868 },
12869
12870 _onMouseMove: function (e) {
12871 if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; }
12872
12873 var point = this._map.mouseEventToLayerPoint(e);
12874 this._handleMouseHover(e, point);
12875 },
12876
12877
12878 _handleMouseOut: function (e) {
12879 var layer = this._hoveredLayer;
12880 if (layer) {
12881 // if we're leaving the layer, fire mouseout
12882 removeClass(this._container, 'leaflet-interactive');
12883 this._fireEvent([layer], e, 'mouseout');
12884 this._hoveredLayer = null;
12885 this._mouseHoverThrottled = false;
12886 }
12887 },
12888
12889 _handleMouseHover: function (e, point) {
12890 if (this._mouseHoverThrottled) {
12891 return;
12892 }
12893
12894 var layer, candidateHoveredLayer;
12895
12896 for (var order = this._drawFirst; order; order = order.next) {
12897 layer = order.layer;
12898 if (layer.options.interactive && layer._containsPoint(point)) {
12899 candidateHoveredLayer = layer;
12900 }
12901 }
12902
12903 if (candidateHoveredLayer !== this._hoveredLayer) {
12904 this._handleMouseOut(e);
12905
12906 if (candidateHoveredLayer) {
12907 addClass(this._container, 'leaflet-interactive'); // change cursor
12908 this._fireEvent([candidateHoveredLayer], e, 'mouseover');
12909 this._hoveredLayer = candidateHoveredLayer;
12910 }
12911 }
12912
12913 this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e);
12914
12915 this._mouseHoverThrottled = true;
12916 setTimeout(bind(function () {
12917 this._mouseHoverThrottled = false;
12918 }, this), 32);
12919 },
12920
12921 _fireEvent: function (layers, e, type) {
12922 this._map._fireDOMEvent(e, type || e.type, layers);
12923 },
12924
12925 _bringToFront: function (layer) {
12926 var order = layer._order;
12927
12928 if (!order) { return; }
12929
12930 var next = order.next;
12931 var prev = order.prev;
12932
12933 if (next) {
12934 next.prev = prev;
12935 } else {
12936 // Already last
12937 return;
12938 }
12939 if (prev) {
12940 prev.next = next;
12941 } else if (next) {
12942 // Update first entry unless this is the
12943 // single entry
12944 this._drawFirst = next;
12945 }
12946
12947 order.prev = this._drawLast;
12948 this._drawLast.next = order;
12949
12950 order.next = null;
12951 this._drawLast = order;
12952
12953 this._requestRedraw(layer);
12954 },
12955
12956 _bringToBack: function (layer) {
12957 var order = layer._order;
12958
12959 if (!order) { return; }
12960
12961 var next = order.next;
12962 var prev = order.prev;
12963
12964 if (prev) {
12965 prev.next = next;
12966 } else {
12967 // Already first
12968 return;
12969 }
12970 if (next) {
12971 next.prev = prev;
12972 } else if (prev) {
12973 // Update last entry unless this is the
12974 // single entry
12975 this._drawLast = prev;
12976 }
12977
12978 order.prev = null;
12979
12980 order.next = this._drawFirst;
12981 this._drawFirst.prev = order;
12982 this._drawFirst = order;
12983
12984 this._requestRedraw(layer);
12985 }
12986 });
12987
12988 // @factory L.canvas(options?: Renderer options)
12989 // Creates a Canvas renderer with the given options.
12990 function canvas(options) {
12991 return Browser.canvas ? new Canvas(options) : null;
12992 }
12993
12994 /*
12995 * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
12996 */
12997
12998
12999 var vmlCreate = (function () {
13000 try {
13001 document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
13002 return function (name) {
13003 return document.createElement('<lvml:' + name + ' class="lvml">');
13004 };
13005 } catch (e) {
13006 // Do not return fn from catch block so `e` can be garbage collected
13007 // See https://github.com/Leaflet/Leaflet/pull/7279
13008 }
13009 return function (name) {
13010 return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
13011 };
13012 })();
13013
13014
13015 /*
13016 * @class SVG
13017 *
13018 *
13019 * VML was deprecated in 2012, which means VML functionality exists only for backwards compatibility
13020 * with old versions of Internet Explorer.
13021 */
13022
13023 // mixin to redefine some SVG methods to handle VML syntax which is similar but with some differences
13024 var vmlMixin = {
13025
13026 _initContainer: function () {
13027 this._container = create$1('div', 'leaflet-vml-container');
13028 },
13029
13030 _update: function () {
13031 if (this._map._animatingZoom) { return; }
13032 Renderer.prototype._update.call(this);
13033 this.fire('update');
13034 },
13035
13036 _initPath: function (layer) {
13037 var container = layer._container = vmlCreate('shape');
13038
13039 addClass(container, 'leaflet-vml-shape ' + (this.options.className || ''));
13040
13041 container.coordsize = '1 1';
13042
13043 layer._path = vmlCreate('path');
13044 container.appendChild(layer._path);
13045
13046 this._updateStyle(layer);
13047 this._layers[stamp(layer)] = layer;
13048 },
13049
13050 _addPath: function (layer) {
13051 var container = layer._container;
13052 this._container.appendChild(container);
13053
13054 if (layer.options.interactive) {
13055 layer.addInteractiveTarget(container);
13056 }
13057 },
13058
13059 _removePath: function (layer) {
13060 var container = layer._container;
13061 remove(container);
13062 layer.removeInteractiveTarget(container);
13063 delete this._layers[stamp(layer)];
13064 },
13065
13066 _updateStyle: function (layer) {
13067 var stroke = layer._stroke,
13068 fill = layer._fill,
13069 options = layer.options,
13070 container = layer._container;
13071
13072 container.stroked = !!options.stroke;
13073 container.filled = !!options.fill;
13074
13075 if (options.stroke) {
13076 if (!stroke) {
13077 stroke = layer._stroke = vmlCreate('stroke');
13078 }
13079 container.appendChild(stroke);
13080 stroke.weight = options.weight + 'px';
13081 stroke.color = options.color;
13082 stroke.opacity = options.opacity;
13083
13084 if (options.dashArray) {
13085 stroke.dashStyle = isArray(options.dashArray) ?
13086 options.dashArray.join(' ') :
13087 options.dashArray.replace(/( *, *)/g, ' ');
13088 } else {
13089 stroke.dashStyle = '';
13090 }
13091 stroke.endcap = options.lineCap.replace('butt', 'flat');
13092 stroke.joinstyle = options.lineJoin;
13093
13094 } else if (stroke) {
13095 container.removeChild(stroke);
13096 layer._stroke = null;
13097 }
13098
13099 if (options.fill) {
13100 if (!fill) {
13101 fill = layer._fill = vmlCreate('fill');
13102 }
13103 container.appendChild(fill);
13104 fill.color = options.fillColor || options.color;
13105 fill.opacity = options.fillOpacity;
13106
13107 } else if (fill) {
13108 container.removeChild(fill);
13109 layer._fill = null;
13110 }
13111 },
13112
13113 _updateCircle: function (layer) {
13114 var p = layer._point.round(),
13115 r = Math.round(layer._radius),
13116 r2 = Math.round(layer._radiusY || r);
13117
13118 this._setPath(layer, layer._empty() ? 'M0 0' :
13119 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360));
13120 },
13121
13122 _setPath: function (layer, path) {
13123 layer._path.v = path;
13124 },
13125
13126 _bringToFront: function (layer) {
13127 toFront(layer._container);
13128 },
13129
13130 _bringToBack: function (layer) {
13131 toBack(layer._container);
13132 }
13133 };
13134
13135 var create = Browser.vml ? vmlCreate : svgCreate;
13136
13137 /*
13138 * @class SVG
13139 * @inherits Renderer
13140 * @aka L.SVG
13141 *
13142 * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG).
13143 * Inherits `Renderer`.
13144 *
13145 * Due to [technical limitations](https://caniuse.com/svg), SVG is not
13146 * available in all web browsers, notably Android 2.x and 3.x.
13147 *
13148 * Although SVG is not available on IE7 and IE8, these browsers support
13149 * [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language)
13150 * (a now deprecated technology), and the SVG renderer will fall back to VML in
13151 * this case.
13152 *
13153 * @example
13154 *
13155 * Use SVG by default for all paths in the map:
13156 *
13157 * ```js
13158 * var map = L.map('map', {
13159 * renderer: L.svg()
13160 * });
13161 * ```
13162 *
13163 * Use a SVG renderer with extra padding for specific vector geometries:
13164 *
13165 * ```js
13166 * var map = L.map('map');
13167 * var myRenderer = L.svg({ padding: 0.5 });
13168 * var line = L.polyline( coordinates, { renderer: myRenderer } );
13169 * var circle = L.circle( center, { renderer: myRenderer } );
13170 * ```
13171 */
13172
13173 var SVG = Renderer.extend({
13174
13175 _initContainer: function () {
13176 this._container = create('svg');
13177
13178 // makes it possible to click through svg root; we'll reset it back in individual paths
13179 this._container.setAttribute('pointer-events', 'none');
13180
13181 this._rootGroup = create('g');
13182 this._container.appendChild(this._rootGroup);
13183 },
13184
13185 _destroyContainer: function () {
13186 remove(this._container);
13187 off(this._container);
13188 delete this._container;
13189 delete this._rootGroup;
13190 delete this._svgSize;
13191 },
13192
13193 _update: function () {
13194 if (this._map._animatingZoom && this._bounds) { return; }
13195
13196 Renderer.prototype._update.call(this);
13197
13198 var b = this._bounds,
13199 size = b.getSize(),
13200 container = this._container;
13201
13202 // set size of svg-container if changed
13203 if (!this._svgSize || !this._svgSize.equals(size)) {
13204 this._svgSize = size;
13205 container.setAttribute('width', size.x);
13206 container.setAttribute('height', size.y);
13207 }
13208
13209 // movement: update container viewBox so that we don't have to change coordinates of individual layers
13210 setPosition(container, b.min);
13211 container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' '));
13212
13213 this.fire('update');
13214 },
13215
13216 // methods below are called by vector layers implementations
13217
13218 _initPath: function (layer) {
13219 var path = layer._path = create('path');
13220
13221 // @namespace Path
13222 // @option className: String = null
13223 // Custom class name set on an element. Only for SVG renderer.
13224 if (layer.options.className) {
13225 addClass(path, layer.options.className);
13226 }
13227
13228 if (layer.options.interactive) {
13229 addClass(path, 'leaflet-interactive');
13230 }
13231
13232 this._updateStyle(layer);
13233 this._layers[stamp(layer)] = layer;
13234 },
13235
13236 _addPath: function (layer) {
13237 if (!this._rootGroup) { this._initContainer(); }
13238 this._rootGroup.appendChild(layer._path);
13239 layer.addInteractiveTarget(layer._path);
13240 },
13241
13242 _removePath: function (layer) {
13243 remove(layer._path);
13244 layer.removeInteractiveTarget(layer._path);
13245 delete this._layers[stamp(layer)];
13246 },
13247
13248 _updatePath: function (layer) {
13249 layer._project();
13250 layer._update();
13251 },
13252
13253 _updateStyle: function (layer) {
13254 var path = layer._path,
13255 options = layer.options;
13256
13257 if (!path) { return; }
13258
13259 if (options.stroke) {
13260 path.setAttribute('stroke', options.color);
13261 path.setAttribute('stroke-opacity', options.opacity);
13262 path.setAttribute('stroke-width', options.weight);
13263 path.setAttribute('stroke-linecap', options.lineCap);
13264 path.setAttribute('stroke-linejoin', options.lineJoin);
13265
13266 if (options.dashArray) {
13267 path.setAttribute('stroke-dasharray', options.dashArray);
13268 } else {
13269 path.removeAttribute('stroke-dasharray');
13270 }
13271
13272 if (options.dashOffset) {
13273 path.setAttribute('stroke-dashoffset', options.dashOffset);
13274 } else {
13275 path.removeAttribute('stroke-dashoffset');
13276 }
13277 } else {
13278 path.setAttribute('stroke', 'none');
13279 }
13280
13281 if (options.fill) {
13282 path.setAttribute('fill', options.fillColor || options.color);
13283 path.setAttribute('fill-opacity', options.fillOpacity);
13284 path.setAttribute('fill-rule', options.fillRule || 'evenodd');
13285 } else {
13286 path.setAttribute('fill', 'none');
13287 }
13288 },
13289
13290 _updatePoly: function (layer, closed) {
13291 this._setPath(layer, pointsToPath(layer._parts, closed));
13292 },
13293
13294 _updateCircle: function (layer) {
13295 var p = layer._point,
13296 r = Math.max(Math.round(layer._radius), 1),
13297 r2 = Math.max(Math.round(layer._radiusY), 1) || r,
13298 arc = 'a' + r + ',' + r2 + ' 0 1,0 ';
13299
13300 // drawing a circle with two half-arcs
13301 var d = layer._empty() ? 'M0 0' :
13302 'M' + (p.x - r) + ',' + p.y +
13303 arc + (r * 2) + ',0 ' +
13304 arc + (-r * 2) + ',0 ';
13305
13306 this._setPath(layer, d);
13307 },
13308
13309 _setPath: function (layer, path) {
13310 layer._path.setAttribute('d', path);
13311 },
13312
13313 // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements
13314 _bringToFront: function (layer) {
13315 toFront(layer._path);
13316 },
13317
13318 _bringToBack: function (layer) {
13319 toBack(layer._path);
13320 }
13321 });
13322
13323 if (Browser.vml) {
13324 SVG.include(vmlMixin);
13325 }
13326
13327 // @namespace SVG
13328 // @factory L.svg(options?: Renderer options)
13329 // Creates a SVG renderer with the given options.
13330 function svg(options) {
13331 return Browser.svg || Browser.vml ? new SVG(options) : null;
13332 }
13333
13334 Map.include({
13335 // @namespace Map; @method getRenderer(layer: Path): Renderer
13336 // Returns the instance of `Renderer` that should be used to render the given
13337 // `Path`. It will ensure that the `renderer` options of the map and paths
13338 // are respected, and that the renderers do exist on the map.
13339 getRenderer: function (layer) {
13340 // @namespace Path; @option renderer: Renderer
13341 // Use this specific instance of `Renderer` for this path. Takes
13342 // precedence over the map's [default renderer](#map-renderer).
13343 var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
13344
13345 if (!renderer) {
13346 renderer = this._renderer = this._createRenderer();
13347 }
13348
13349 if (!this.hasLayer(renderer)) {
13350 this.addLayer(renderer);
13351 }
13352 return renderer;
13353 },
13354
13355 _getPaneRenderer: function (name) {
13356 if (name === 'overlayPane' || name === undefined) {
13357 return false;
13358 }
13359
13360 var renderer = this._paneRenderers[name];
13361 if (renderer === undefined) {
13362 renderer = this._createRenderer({pane: name});
13363 this._paneRenderers[name] = renderer;
13364 }
13365 return renderer;
13366 },
13367
13368 _createRenderer: function (options) {
13369 // @namespace Map; @option preferCanvas: Boolean = false
13370 // Whether `Path`s should be rendered on a `Canvas` renderer.
13371 // By default, all `Path`s are rendered in a `SVG` renderer.
13372 return (this.options.preferCanvas && canvas(options)) || svg(options);
13373 }
13374 });
13375
13376 /*
13377 * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object.
13378 */
13379
13380 /*
13381 * @class Rectangle
13382 * @aka L.Rectangle
13383 * @inherits Polygon
13384 *
13385 * A class for drawing rectangle overlays on a map. Extends `Polygon`.
13386 *
13387 * @example
13388 *
13389 * ```js
13390 * // define rectangle geographical bounds
13391 * var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
13392 *
13393 * // create an orange rectangle
13394 * L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map);
13395 *
13396 * // zoom the map to the rectangle bounds
13397 * map.fitBounds(bounds);
13398 * ```
13399 *
13400 */
13401
13402
13403 var Rectangle = Polygon.extend({
13404 initialize: function (latLngBounds, options) {
13405 Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
13406 },
13407
13408 // @method setBounds(latLngBounds: LatLngBounds): this
13409 // Redraws the rectangle with the passed bounds.
13410 setBounds: function (latLngBounds) {
13411 return this.setLatLngs(this._boundsToLatLngs(latLngBounds));
13412 },
13413
13414 _boundsToLatLngs: function (latLngBounds) {
13415 latLngBounds = toLatLngBounds(latLngBounds);
13416 return [
13417 latLngBounds.getSouthWest(),
13418 latLngBounds.getNorthWest(),
13419 latLngBounds.getNorthEast(),
13420 latLngBounds.getSouthEast()
13421 ];
13422 }
13423 });
13424
13425
13426 // @factory L.rectangle(latLngBounds: LatLngBounds, options?: Polyline options)
13427 function rectangle(latLngBounds, options) {
13428 return new Rectangle(latLngBounds, options);
13429 }
13430
13431 SVG.create = create;
13432 SVG.pointsToPath = pointsToPath;
13433
13434 GeoJSON.geometryToLayer = geometryToLayer;
13435 GeoJSON.coordsToLatLng = coordsToLatLng;
13436 GeoJSON.coordsToLatLngs = coordsToLatLngs;
13437 GeoJSON.latLngToCoords = latLngToCoords;
13438 GeoJSON.latLngsToCoords = latLngsToCoords;
13439 GeoJSON.getFeature = getFeature;
13440 GeoJSON.asFeature = asFeature;
13441
13442 /*
13443 * L.Handler.BoxZoom is used to add shift-drag zoom interaction to the map
13444 * (zoom to a selected bounding box), enabled by default.
13445 */
13446
13447 // @namespace Map
13448 // @section Interaction Options
13449 Map.mergeOptions({
13450 // @option boxZoom: Boolean = true
13451 // Whether the map can be zoomed to a rectangular area specified by
13452 // dragging the mouse while pressing the shift key.
13453 boxZoom: true
13454 });
13455
13456 var BoxZoom = Handler.extend({
13457 initialize: function (map) {
13458 this._map = map;
13459 this._container = map._container;
13460 this._pane = map._panes.overlayPane;
13461 this._resetStateTimeout = 0;
13462 map.on('unload', this._destroy, this);
13463 },
13464
13465 addHooks: function () {
13466 on(this._container, 'mousedown', this._onMouseDown, this);
13467 },
13468
13469 removeHooks: function () {
13470 off(this._container, 'mousedown', this._onMouseDown, this);
13471 },
13472
13473 moved: function () {
13474 return this._moved;
13475 },
13476
13477 _destroy: function () {
13478 remove(this._pane);
13479 delete this._pane;
13480 },
13481
13482 _resetState: function () {
13483 this._resetStateTimeout = 0;
13484 this._moved = false;
13485 },
13486
13487 _clearDeferredResetState: function () {
13488 if (this._resetStateTimeout !== 0) {
13489 clearTimeout(this._resetStateTimeout);
13490 this._resetStateTimeout = 0;
13491 }
13492 },
13493
13494 _onMouseDown: function (e) {
13495 if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
13496
13497 // Clear the deferred resetState if it hasn't executed yet, otherwise it
13498 // will interrupt the interaction and orphan a box element in the container.
13499 this._clearDeferredResetState();
13500 this._resetState();
13501
13502 disableTextSelection();
13503 disableImageDrag();
13504
13505 this._startPoint = this._map.mouseEventToContainerPoint(e);
13506
13507 on(document, {
13508 contextmenu: stop,
13509 mousemove: this._onMouseMove,
13510 mouseup: this._onMouseUp,
13511 keydown: this._onKeyDown
13512 }, this);
13513 },
13514
13515 _onMouseMove: function (e) {
13516 if (!this._moved) {
13517 this._moved = true;
13518
13519 this._box = create$1('div', 'leaflet-zoom-box', this._container);
13520 addClass(this._container, 'leaflet-crosshair');
13521
13522 this._map.fire('boxzoomstart');
13523 }
13524
13525 this._point = this._map.mouseEventToContainerPoint(e);
13526
13527 var bounds = new Bounds(this._point, this._startPoint),
13528 size = bounds.getSize();
13529
13530 setPosition(this._box, bounds.min);
13531
13532 this._box.style.width = size.x + 'px';
13533 this._box.style.height = size.y + 'px';
13534 },
13535
13536 _finish: function () {
13537 if (this._moved) {
13538 remove(this._box);
13539 removeClass(this._container, 'leaflet-crosshair');
13540 }
13541
13542 enableTextSelection();
13543 enableImageDrag();
13544
13545 off(document, {
13546 contextmenu: stop,
13547 mousemove: this._onMouseMove,
13548 mouseup: this._onMouseUp,
13549 keydown: this._onKeyDown
13550 }, this);
13551 },
13552
13553 _onMouseUp: function (e) {
13554 if ((e.which !== 1) && (e.button !== 1)) { return; }
13555
13556 this._finish();
13557
13558 if (!this._moved) { return; }
13559 // Postpone to next JS tick so internal click event handling
13560 // still see it as "moved".
13561 this._clearDeferredResetState();
13562 this._resetStateTimeout = setTimeout(bind(this._resetState, this), 0);
13563
13564 var bounds = new LatLngBounds(
13565 this._map.containerPointToLatLng(this._startPoint),
13566 this._map.containerPointToLatLng(this._point));
13567
13568 this._map
13569 .fitBounds(bounds)
13570 .fire('boxzoomend', {boxZoomBounds: bounds});
13571 },
13572
13573 _onKeyDown: function (e) {
13574 if (e.keyCode === 27) {
13575 this._finish();
13576 this._clearDeferredResetState();
13577 this._resetState();
13578 }
13579 }
13580 });
13581
13582 // @section Handlers
13583 // @property boxZoom: Handler
13584 // Box (shift-drag with mouse) zoom handler.
13585 Map.addInitHook('addHandler', 'boxZoom', BoxZoom);
13586
13587 /*
13588 * L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default.
13589 */
13590
13591 // @namespace Map
13592 // @section Interaction Options
13593
13594 Map.mergeOptions({
13595 // @option doubleClickZoom: Boolean|String = true
13596 // Whether the map can be zoomed in by double clicking on it and
13597 // zoomed out by double clicking while holding shift. If passed
13598 // `'center'`, double-click zoom will zoom to the center of the
13599 // view regardless of where the mouse was.
13600 doubleClickZoom: true
13601 });
13602
13603 var DoubleClickZoom = Handler.extend({
13604 addHooks: function () {
13605 this._map.on('dblclick', this._onDoubleClick, this);
13606 },
13607
13608 removeHooks: function () {
13609 this._map.off('dblclick', this._onDoubleClick, this);
13610 },
13611
13612 _onDoubleClick: function (e) {
13613 var map = this._map,
13614 oldZoom = map.getZoom(),
13615 delta = map.options.zoomDelta,
13616 zoom = e.originalEvent.shiftKey ? oldZoom - delta : oldZoom + delta;
13617
13618 if (map.options.doubleClickZoom === 'center') {
13619 map.setZoom(zoom);
13620 } else {
13621 map.setZoomAround(e.containerPoint, zoom);
13622 }
13623 }
13624 });
13625
13626 // @section Handlers
13627 //
13628 // Map properties include interaction handlers that allow you to control
13629 // interaction behavior in runtime, enabling or disabling certain features such
13630 // as dragging or touch zoom (see `Handler` methods). For example:
13631 //
13632 // ```js
13633 // map.doubleClickZoom.disable();
13634 // ```
13635 //
13636 // @property doubleClickZoom: Handler
13637 // Double click zoom handler.
13638 Map.addInitHook('addHandler', 'doubleClickZoom', DoubleClickZoom);
13639
13640 /*
13641 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
13642 */
13643
13644 // @namespace Map
13645 // @section Interaction Options
13646 Map.mergeOptions({
13647 // @option dragging: Boolean = true
13648 // Whether the map is draggable with mouse/touch or not.
13649 dragging: true,
13650
13651 // @section Panning Inertia Options
13652 // @option inertia: Boolean = *
13653 // If enabled, panning of the map will have an inertia effect where
13654 // the map builds momentum while dragging and continues moving in
13655 // the same direction for some time. Feels especially nice on touch
13656 // devices. Enabled by default.
13657 inertia: true,
13658
13659 // @option inertiaDeceleration: Number = 3000
13660 // The rate with which the inertial movement slows down, in pixels/second².
13661 inertiaDeceleration: 3400, // px/s^2
13662
13663 // @option inertiaMaxSpeed: Number = Infinity
13664 // Max speed of the inertial movement, in pixels/second.
13665 inertiaMaxSpeed: Infinity, // px/s
13666
13667 // @option easeLinearity: Number = 0.2
13668 easeLinearity: 0.2,
13669
13670 // TODO refactor, move to CRS
13671 // @option worldCopyJump: Boolean = false
13672 // With this option enabled, the map tracks when you pan to another "copy"
13673 // of the world and seamlessly jumps to the original one so that all overlays
13674 // like markers and vector layers are still visible.
13675 worldCopyJump: false,
13676
13677 // @option maxBoundsViscosity: Number = 0.0
13678 // If `maxBounds` is set, this option will control how solid the bounds
13679 // are when dragging the map around. The default value of `0.0` allows the
13680 // user to drag outside the bounds at normal speed, higher values will
13681 // slow down map dragging outside bounds, and `1.0` makes the bounds fully
13682 // solid, preventing the user from dragging outside the bounds.
13683 maxBoundsViscosity: 0.0
13684 });
13685
13686 var Drag = Handler.extend({
13687 addHooks: function () {
13688 if (!this._draggable) {
13689 var map = this._map;
13690
13691 this._draggable = new Draggable(map._mapPane, map._container);
13692
13693 this._draggable.on({
13694 dragstart: this._onDragStart,
13695 drag: this._onDrag,
13696 dragend: this._onDragEnd
13697 }, this);
13698
13699 this._draggable.on('predrag', this._onPreDragLimit, this);
13700 if (map.options.worldCopyJump) {
13701 this._draggable.on('predrag', this._onPreDragWrap, this);
13702 map.on('zoomend', this._onZoomEnd, this);
13703
13704 map.whenReady(this._onZoomEnd, this);
13705 }
13706 }
13707 addClass(this._map._container, 'leaflet-grab leaflet-touch-drag');
13708 this._draggable.enable();
13709 this._positions = [];
13710 this._times = [];
13711 },
13712
13713 removeHooks: function () {
13714 removeClass(this._map._container, 'leaflet-grab');
13715 removeClass(this._map._container, 'leaflet-touch-drag');
13716 this._draggable.disable();
13717 },
13718
13719 moved: function () {
13720 return this._draggable && this._draggable._moved;
13721 },
13722
13723 moving: function () {
13724 return this._draggable && this._draggable._moving;
13725 },
13726
13727 _onDragStart: function () {
13728 var map = this._map;
13729
13730 map._stop();
13731 if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
13732 var bounds = toLatLngBounds(this._map.options.maxBounds);
13733
13734 this._offsetLimit = toBounds(
13735 this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
13736 this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1)
13737 .add(this._map.getSize()));
13738
13739 this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity));
13740 } else {
13741 this._offsetLimit = null;
13742 }
13743
13744 map
13745 .fire('movestart')
13746 .fire('dragstart');
13747
13748 if (map.options.inertia) {
13749 this._positions = [];
13750 this._times = [];
13751 }
13752 },
13753
13754 _onDrag: function (e) {
13755 if (this._map.options.inertia) {
13756 var time = this._lastTime = +new Date(),
13757 pos = this._lastPos = this._draggable._absPos || this._draggable._newPos;
13758
13759 this._positions.push(pos);
13760 this._times.push(time);
13761
13762 this._prunePositions(time);
13763 }
13764
13765 this._map
13766 .fire('move', e)
13767 .fire('drag', e);
13768 },
13769
13770 _prunePositions: function (time) {
13771 while (this._positions.length > 1 && time - this._times[0] > 50) {
13772 this._positions.shift();
13773 this._times.shift();
13774 }
13775 },
13776
13777 _onZoomEnd: function () {
13778 var pxCenter = this._map.getSize().divideBy(2),
13779 pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);
13780
13781 this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
13782 this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
13783 },
13784
13785 _viscousLimit: function (value, threshold) {
13786 return value - (value - threshold) * this._viscosity;
13787 },
13788
13789 _onPreDragLimit: function () {
13790 if (!this._viscosity || !this._offsetLimit) { return; }
13791
13792 var offset = this._draggable._newPos.subtract(this._draggable._startPos);
13793
13794 var limit = this._offsetLimit;
13795 if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); }
13796 if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); }
13797 if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); }
13798 if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); }
13799
13800 this._draggable._newPos = this._draggable._startPos.add(offset);
13801 },
13802
13803 _onPreDragWrap: function () {
13804 // TODO refactor to be able to adjust map pane position after zoom
13805 var worldWidth = this._worldWidth,
13806 halfWidth = Math.round(worldWidth / 2),
13807 dx = this._initialWorldOffset,
13808 x = this._draggable._newPos.x,
13809 newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
13810 newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
13811 newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
13812
13813 this._draggable._absPos = this._draggable._newPos.clone();
13814 this._draggable._newPos.x = newX;
13815 },
13816
13817 _onDragEnd: function (e) {
13818 var map = this._map,
13819 options = map.options,
13820
13821 noInertia = !options.inertia || e.noInertia || this._times.length < 2;
13822
13823 map.fire('dragend', e);
13824
13825 if (noInertia) {
13826 map.fire('moveend');
13827
13828 } else {
13829 this._prunePositions(+new Date());
13830
13831 var direction = this._lastPos.subtract(this._positions[0]),
13832 duration = (this._lastTime - this._times[0]) / 1000,
13833 ease = options.easeLinearity,
13834
13835 speedVector = direction.multiplyBy(ease / duration),
13836 speed = speedVector.distanceTo([0, 0]),
13837
13838 limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
13839 limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
13840
13841 decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
13842 offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
13843
13844 if (!offset.x && !offset.y) {
13845 map.fire('moveend');
13846
13847 } else {
13848 offset = map._limitOffset(offset, map.options.maxBounds);
13849
13850 requestAnimFrame(function () {
13851 map.panBy(offset, {
13852 duration: decelerationDuration,
13853 easeLinearity: ease,
13854 noMoveStart: true,
13855 animate: true
13856 });
13857 });
13858 }
13859 }
13860 }
13861 });
13862
13863 // @section Handlers
13864 // @property dragging: Handler
13865 // Map dragging handler (by both mouse and touch).
13866 Map.addInitHook('addHandler', 'dragging', Drag);
13867
13868 /*
13869 * L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
13870 */
13871
13872 // @namespace Map
13873 // @section Keyboard Navigation Options
13874 Map.mergeOptions({
13875 // @option keyboard: Boolean = true
13876 // Makes the map focusable and allows users to navigate the map with keyboard
13877 // arrows and `+`/`-` keys.
13878 keyboard: true,
13879
13880 // @option keyboardPanDelta: Number = 80
13881 // Amount of pixels to pan when pressing an arrow key.
13882 keyboardPanDelta: 80
13883 });
13884
13885 var Keyboard = Handler.extend({
13886
13887 keyCodes: {
13888 left: [37],
13889 right: [39],
13890 down: [40],
13891 up: [38],
13892 zoomIn: [187, 107, 61, 171],
13893 zoomOut: [189, 109, 54, 173]
13894 },
13895
13896 initialize: function (map) {
13897 this._map = map;
13898
13899 this._setPanDelta(map.options.keyboardPanDelta);
13900 this._setZoomDelta(map.options.zoomDelta);
13901 },
13902
13903 addHooks: function () {
13904 var container = this._map._container;
13905
13906 // make the container focusable by tabbing
13907 if (container.tabIndex <= 0) {
13908 container.tabIndex = '0';
13909 }
13910
13911 on(container, {
13912 focus: this._onFocus,
13913 blur: this._onBlur,
13914 mousedown: this._onMouseDown
13915 }, this);
13916
13917 this._map.on({
13918 focus: this._addHooks,
13919 blur: this._removeHooks
13920 }, this);
13921 },
13922
13923 removeHooks: function () {
13924 this._removeHooks();
13925
13926 off(this._map._container, {
13927 focus: this._onFocus,
13928 blur: this._onBlur,
13929 mousedown: this._onMouseDown
13930 }, this);
13931
13932 this._map.off({
13933 focus: this._addHooks,
13934 blur: this._removeHooks
13935 }, this);
13936 },
13937
13938 _onMouseDown: function () {
13939 if (this._focused) { return; }
13940
13941 var body = document.body,
13942 docEl = document.documentElement,
13943 top = body.scrollTop || docEl.scrollTop,
13944 left = body.scrollLeft || docEl.scrollLeft;
13945
13946 this._map._container.focus();
13947
13948 window.scrollTo(left, top);
13949 },
13950
13951 _onFocus: function () {
13952 this._focused = true;
13953 this._map.fire('focus');
13954 },
13955
13956 _onBlur: function () {
13957 this._focused = false;
13958 this._map.fire('blur');
13959 },
13960
13961 _setPanDelta: function (panDelta) {
13962 var keys = this._panKeys = {},
13963 codes = this.keyCodes,
13964 i, len;
13965
13966 for (i = 0, len = codes.left.length; i < len; i++) {
13967 keys[codes.left[i]] = [-1 * panDelta, 0];
13968 }
13969 for (i = 0, len = codes.right.length; i < len; i++) {
13970 keys[codes.right[i]] = [panDelta, 0];
13971 }
13972 for (i = 0, len = codes.down.length; i < len; i++) {
13973 keys[codes.down[i]] = [0, panDelta];
13974 }
13975 for (i = 0, len = codes.up.length; i < len; i++) {
13976 keys[codes.up[i]] = [0, -1 * panDelta];
13977 }
13978 },
13979
13980 _setZoomDelta: function (zoomDelta) {
13981 var keys = this._zoomKeys = {},
13982 codes = this.keyCodes,
13983 i, len;
13984
13985 for (i = 0, len = codes.zoomIn.length; i < len; i++) {
13986 keys[codes.zoomIn[i]] = zoomDelta;
13987 }
13988 for (i = 0, len = codes.zoomOut.length; i < len; i++) {
13989 keys[codes.zoomOut[i]] = -zoomDelta;
13990 }
13991 },
13992
13993 _addHooks: function () {
13994 on(document, 'keydown', this._onKeyDown, this);
13995 },
13996
13997 _removeHooks: function () {
13998 off(document, 'keydown', this._onKeyDown, this);
13999 },
14000
14001 _onKeyDown: function (e) {
14002 if (e.altKey || e.ctrlKey || e.metaKey) { return; }
14003
14004 var key = e.keyCode,
14005 map = this._map,
14006 offset;
14007
14008 if (key in this._panKeys) {
14009 if (!map._panAnim || !map._panAnim._inProgress) {
14010 offset = this._panKeys[key];
14011 if (e.shiftKey) {
14012 offset = toPoint(offset).multiplyBy(3);
14013 }
14014
14015 if (map.options.maxBounds) {
14016 offset = map._limitOffset(toPoint(offset), map.options.maxBounds);
14017 }
14018
14019 if (map.options.worldCopyJump) {
14020 var newLatLng = map.wrapLatLng(map.unproject(map.project(map.getCenter()).add(offset)));
14021 map.panTo(newLatLng);
14022 } else {
14023 map.panBy(offset);
14024 }
14025 }
14026 } else if (key in this._zoomKeys) {
14027 map.setZoom(map.getZoom() + (e.shiftKey ? 3 : 1) * this._zoomKeys[key]);
14028
14029 } else if (key === 27 && map._popup && map._popup.options.closeOnEscapeKey) {
14030 map.closePopup();
14031
14032 } else {
14033 return;
14034 }
14035
14036 stop(e);
14037 }
14038 });
14039
14040 // @section Handlers
14041 // @section Handlers
14042 // @property keyboard: Handler
14043 // Keyboard navigation handler.
14044 Map.addInitHook('addHandler', 'keyboard', Keyboard);
14045
14046 /*
14047 * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
14048 */
14049
14050 // @namespace Map
14051 // @section Interaction Options
14052 Map.mergeOptions({
14053 // @section Mouse wheel options
14054 // @option scrollWheelZoom: Boolean|String = true
14055 // Whether the map can be zoomed by using the mouse wheel. If passed `'center'`,
14056 // it will zoom to the center of the view regardless of where the mouse was.
14057 scrollWheelZoom: true,
14058
14059 // @option wheelDebounceTime: Number = 40
14060 // Limits the rate at which a wheel can fire (in milliseconds). By default
14061 // user can't zoom via wheel more often than once per 40 ms.
14062 wheelDebounceTime: 40,
14063
14064 // @option wheelPxPerZoomLevel: Number = 60
14065 // How many scroll pixels (as reported by [L.DomEvent.getWheelDelta](#domevent-getwheeldelta))
14066 // mean a change of one full zoom level. Smaller values will make wheel-zooming
14067 // faster (and vice versa).
14068 wheelPxPerZoomLevel: 60
14069 });
14070
14071 var ScrollWheelZoom = Handler.extend({
14072 addHooks: function () {
14073 on(this._map._container, 'wheel', this._onWheelScroll, this);
14074
14075 this._delta = 0;
14076 },
14077
14078 removeHooks: function () {
14079 off(this._map._container, 'wheel', this._onWheelScroll, this);
14080 },
14081
14082 _onWheelScroll: function (e) {
14083 var delta = getWheelDelta(e);
14084
14085 var debounce = this._map.options.wheelDebounceTime;
14086
14087 this._delta += delta;
14088 this._lastMousePos = this._map.mouseEventToContainerPoint(e);
14089
14090 if (!this._startTime) {
14091 this._startTime = +new Date();
14092 }
14093
14094 var left = Math.max(debounce - (+new Date() - this._startTime), 0);
14095
14096 clearTimeout(this._timer);
14097 this._timer = setTimeout(bind(this._performZoom, this), left);
14098
14099 stop(e);
14100 },
14101
14102 _performZoom: function () {
14103 var map = this._map,
14104 zoom = map.getZoom(),
14105 snap = this._map.options.zoomSnap || 0;
14106
14107 map._stop(); // stop panning and fly animations if any
14108
14109 // map the delta with a sigmoid function to -4..4 range leaning on -1..1
14110 var d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4),
14111 d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2,
14112 d4 = snap ? Math.ceil(d3 / snap) * snap : d3,
14113 delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom;
14114
14115 this._delta = 0;
14116 this._startTime = null;
14117
14118 if (!delta) { return; }
14119
14120 if (map.options.scrollWheelZoom === 'center') {
14121 map.setZoom(zoom + delta);
14122 } else {
14123 map.setZoomAround(this._lastMousePos, zoom + delta);
14124 }
14125 }
14126 });
14127
14128 // @section Handlers
14129 // @property scrollWheelZoom: Handler
14130 // Scroll wheel zoom handler.
14131 Map.addInitHook('addHandler', 'scrollWheelZoom', ScrollWheelZoom);
14132
14133 /*
14134 * L.Map.TapHold is used to simulate `contextmenu` event on long hold,
14135 * which otherwise is not fired by mobile Safari.
14136 */
14137
14138 var tapHoldDelay = 600;
14139
14140 // @namespace Map
14141 // @section Interaction Options
14142 Map.mergeOptions({
14143 // @section Touch interaction options
14144 // @option tapHold: Boolean
14145 // Enables simulation of `contextmenu` event, default is `true` for mobile Safari.
14146 tapHold: Browser.touchNative && Browser.safari && Browser.mobile,
14147
14148 // @option tapTolerance: Number = 15
14149 // The max number of pixels a user can shift his finger during touch
14150 // for it to be considered a valid tap.
14151 tapTolerance: 15
14152 });
14153
14154 var TapHold = Handler.extend({
14155 addHooks: function () {
14156 on(this._map._container, 'touchstart', this._onDown, this);
14157 },
14158
14159 removeHooks: function () {
14160 off(this._map._container, 'touchstart', this._onDown, this);
14161 },
14162
14163 _onDown: function (e) {
14164 clearTimeout(this._holdTimeout);
14165 if (e.touches.length !== 1) { return; }
14166
14167 var first = e.touches[0];
14168 this._startPos = this._newPos = new Point(first.clientX, first.clientY);
14169
14170 this._holdTimeout = setTimeout(bind(function () {
14171 this._cancel();
14172 if (!this._isTapValid()) { return; }
14173
14174 // prevent simulated mouse events https://w3c.github.io/touch-events/#mouse-events
14175 on(document, 'touchend', preventDefault);
14176 on(document, 'touchend touchcancel', this._cancelClickPrevent);
14177 this._simulateEvent('contextmenu', first);
14178 }, this), tapHoldDelay);
14179
14180 on(document, 'touchend touchcancel contextmenu', this._cancel, this);
14181 on(document, 'touchmove', this._onMove, this);
14182 },
14183
14184 _cancelClickPrevent: function cancelClickPrevent() {
14185 off(document, 'touchend', preventDefault);
14186 off(document, 'touchend touchcancel', cancelClickPrevent);
14187 },
14188
14189 _cancel: function () {
14190 clearTimeout(this._holdTimeout);
14191 off(document, 'touchend touchcancel contextmenu', this._cancel, this);
14192 off(document, 'touchmove', this._onMove, this);
14193 },
14194
14195 _onMove: function (e) {
14196 var first = e.touches[0];
14197 this._newPos = new Point(first.clientX, first.clientY);
14198 },
14199
14200 _isTapValid: function () {
14201 return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
14202 },
14203
14204 _simulateEvent: function (type, e) {
14205 var simulatedEvent = new MouseEvent(type, {
14206 bubbles: true,
14207 cancelable: true,
14208 view: window,
14209 // detail: 1,
14210 screenX: e.screenX,
14211 screenY: e.screenY,
14212 clientX: e.clientX,
14213 clientY: e.clientY,
14214 // button: 2,
14215 // buttons: 2
14216 });
14217
14218 simulatedEvent._simulated = true;
14219
14220 e.target.dispatchEvent(simulatedEvent);
14221 }
14222 });
14223
14224 // @section Handlers
14225 // @property tapHold: Handler
14226 // Long tap handler to simulate `contextmenu` event (useful in mobile Safari).
14227 Map.addInitHook('addHandler', 'tapHold', TapHold);
14228
14229 /*
14230 * L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers.
14231 */
14232
14233 // @namespace Map
14234 // @section Interaction Options
14235 Map.mergeOptions({
14236 // @section Touch interaction options
14237 // @option touchZoom: Boolean|String = *
14238 // Whether the map can be zoomed by touch-dragging with two fingers. If
14239 // passed `'center'`, it will zoom to the center of the view regardless of
14240 // where the touch events (fingers) were. Enabled for touch-capable web
14241 // browsers.
14242 touchZoom: Browser.touch,
14243
14244 // @option bounceAtZoomLimits: Boolean = true
14245 // Set it to false if you don't want the map to zoom beyond min/max zoom
14246 // and then bounce back when pinch-zooming.
14247 bounceAtZoomLimits: true
14248 });
14249
14250 var TouchZoom = Handler.extend({
14251 addHooks: function () {
14252 addClass(this._map._container, 'leaflet-touch-zoom');
14253 on(this._map._container, 'touchstart', this._onTouchStart, this);
14254 },
14255
14256 removeHooks: function () {
14257 removeClass(this._map._container, 'leaflet-touch-zoom');
14258 off(this._map._container, 'touchstart', this._onTouchStart, this);
14259 },
14260
14261 _onTouchStart: function (e) {
14262 var map = this._map;
14263 if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
14264
14265 var p1 = map.mouseEventToContainerPoint(e.touches[0]),
14266 p2 = map.mouseEventToContainerPoint(e.touches[1]);
14267
14268 this._centerPoint = map.getSize()._divideBy(2);
14269 this._startLatLng = map.containerPointToLatLng(this._centerPoint);
14270 if (map.options.touchZoom !== 'center') {
14271 this._pinchStartLatLng = map.containerPointToLatLng(p1.add(p2)._divideBy(2));
14272 }
14273
14274 this._startDist = p1.distanceTo(p2);
14275 this._startZoom = map.getZoom();
14276
14277 this._moved = false;
14278 this._zooming = true;
14279
14280 map._stop();
14281
14282 on(document, 'touchmove', this._onTouchMove, this);
14283 on(document, 'touchend touchcancel', this._onTouchEnd, this);
14284
14285 preventDefault(e);
14286 },
14287
14288 _onTouchMove: function (e) {
14289 if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; }
14290
14291 var map = this._map,
14292 p1 = map.mouseEventToContainerPoint(e.touches[0]),
14293 p2 = map.mouseEventToContainerPoint(e.touches[1]),
14294 scale = p1.distanceTo(p2) / this._startDist;
14295
14296 this._zoom = map.getScaleZoom(scale, this._startZoom);
14297
14298 if (!map.options.bounceAtZoomLimits && (
14299 (this._zoom < map.getMinZoom() && scale < 1) ||
14300 (this._zoom > map.getMaxZoom() && scale > 1))) {
14301 this._zoom = map._limitZoom(this._zoom);
14302 }
14303
14304 if (map.options.touchZoom === 'center') {
14305 this._center = this._startLatLng;
14306 if (scale === 1) { return; }
14307 } else {
14308 // Get delta from pinch to center, so centerLatLng is delta applied to initial pinchLatLng
14309 var delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
14310 if (scale === 1 && delta.x === 0 && delta.y === 0) { return; }
14311 this._center = map.unproject(map.project(this._pinchStartLatLng, this._zoom).subtract(delta), this._zoom);
14312 }
14313
14314 if (!this._moved) {
14315 map._moveStart(true, false);
14316 this._moved = true;
14317 }
14318
14319 cancelAnimFrame(this._animRequest);
14320
14321 var moveFn = bind(map._move, map, this._center, this._zoom, {pinch: true, round: false}, undefined);
14322 this._animRequest = requestAnimFrame(moveFn, this, true);
14323
14324 preventDefault(e);
14325 },
14326
14327 _onTouchEnd: function () {
14328 if (!this._moved || !this._zooming) {
14329 this._zooming = false;
14330 return;
14331 }
14332
14333 this._zooming = false;
14334 cancelAnimFrame(this._animRequest);
14335
14336 off(document, 'touchmove', this._onTouchMove, this);
14337 off(document, 'touchend touchcancel', this._onTouchEnd, this);
14338
14339 // Pinch updates GridLayers' levels only when zoomSnap is off, so zoomSnap becomes noUpdate.
14340 if (this._map.options.zoomAnimation) {
14341 this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.zoomSnap);
14342 } else {
14343 this._map._resetView(this._center, this._map._limitZoom(this._zoom));
14344 }
14345 }
14346 });
14347
14348 // @section Handlers
14349 // @property touchZoom: Handler
14350 // Touch zoom handler.
14351 Map.addInitHook('addHandler', 'touchZoom', TouchZoom);
14352
14353 Map.BoxZoom = BoxZoom;
14354 Map.DoubleClickZoom = DoubleClickZoom;
14355 Map.Drag = Drag;
14356 Map.Keyboard = Keyboard;
14357 Map.ScrollWheelZoom = ScrollWheelZoom;
14358 Map.TapHold = TapHold;
14359 Map.TouchZoom = TouchZoom;
14360
14361 exports.Bounds = Bounds;
14362 exports.Browser = Browser;
14363 exports.CRS = CRS;
14364 exports.Canvas = Canvas;
14365 exports.Circle = Circle;
14366 exports.CircleMarker = CircleMarker;
14367 exports.Class = Class;
14368 exports.Control = Control;
14369 exports.DivIcon = DivIcon;
14370 exports.DivOverlay = DivOverlay;
14371 exports.DomEvent = DomEvent;
14372 exports.DomUtil = DomUtil;
14373 exports.Draggable = Draggable;
14374 exports.Evented = Evented;
14375 exports.FeatureGroup = FeatureGroup;
14376 exports.GeoJSON = GeoJSON;
14377 exports.GridLayer = GridLayer;
14378 exports.Handler = Handler;
14379 exports.Icon = Icon;
14380 exports.ImageOverlay = ImageOverlay;
14381 exports.LatLng = LatLng;
14382 exports.LatLngBounds = LatLngBounds;
14383 exports.Layer = Layer;
14384 exports.LayerGroup = LayerGroup;
14385 exports.LineUtil = LineUtil;
14386 exports.Map = Map;
14387 exports.Marker = Marker;
14388 exports.Mixin = Mixin;
14389 exports.Path = Path;
14390 exports.Point = Point;
14391 exports.PolyUtil = PolyUtil;
14392 exports.Polygon = Polygon;
14393 exports.Polyline = Polyline;
14394 exports.Popup = Popup;
14395 exports.PosAnimation = PosAnimation;
14396 exports.Projection = index;
14397 exports.Rectangle = Rectangle;
14398 exports.Renderer = Renderer;
14399 exports.SVG = SVG;
14400 exports.SVGOverlay = SVGOverlay;
14401 exports.TileLayer = TileLayer;
14402 exports.Tooltip = Tooltip;
14403 exports.Transformation = Transformation;
14404 exports.Util = Util;
14405 exports.VideoOverlay = VideoOverlay;
14406 exports.bind = bind;
14407 exports.bounds = toBounds;
14408 exports.canvas = canvas;
14409 exports.circle = circle;
14410 exports.circleMarker = circleMarker;
14411 exports.control = control;
14412 exports.divIcon = divIcon;
14413 exports.extend = extend;
14414 exports.featureGroup = featureGroup;
14415 exports.geoJSON = geoJSON;
14416 exports.geoJson = geoJson;
14417 exports.gridLayer = gridLayer;
14418 exports.icon = icon;
14419 exports.imageOverlay = imageOverlay;
14420 exports.latLng = toLatLng;
14421 exports.latLngBounds = toLatLngBounds;
14422 exports.layerGroup = layerGroup;
14423 exports.map = createMap;
14424 exports.marker = marker;
14425 exports.point = toPoint;
14426 exports.polygon = polygon;
14427 exports.polyline = polyline;
14428 exports.popup = popup;
14429 exports.rectangle = rectangle;
14430 exports.setOptions = setOptions;
14431 exports.stamp = stamp;
14432 exports.svg = svg;
14433 exports.svgOverlay = svgOverlay;
14434 exports.tileLayer = tileLayer;
14435 exports.tooltip = tooltip;
14436 exports.transformation = toTransformation;
14437 exports.version = version;
14438 exports.videoOverlay = videoOverlay;
14439
14440 var oldL = window.L;
14441 exports.noConflict = function() {
14442 window.L = oldL;
14443 return this;
14444 }
14445 // Always export us to window global (see #2364)
14446 window.L = exports;
14447
14448}));
14449//# sourceMappingURL=leaflet-src.js.map