UNPKG

412 kBJavaScriptView Raw
1/* @preserve
2 * Leaflet 1.8.0, a JS library for interactive maps. https://leafletjs.com
3 * (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade
4 */
5
6var version = "1.8.0";
7
8/*
9 * @namespace Util
10 *
11 * Various utility functions, used by Leaflet internally.
12 */
13
14// @function extend(dest: Object, src?: Object): Object
15// Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.
16function extend(dest) {
17 var i, j, len, src;
18
19 for (j = 1, len = arguments.length; j < len; j++) {
20 src = arguments[j];
21 for (i in src) {
22 dest[i] = src[i];
23 }
24 }
25 return dest;
26}
27
28// @function create(proto: Object, properties?: Object): Object
29// Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)
30var create$2 = Object.create || (function () {
31 function F() {}
32 return function (proto) {
33 F.prototype = proto;
34 return new F();
35 };
36})();
37
38// @function bind(fn: Function, …): Function
39// 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).
40// Has a `L.bind()` shortcut.
41function bind(fn, obj) {
42 var slice = Array.prototype.slice;
43
44 if (fn.bind) {
45 return fn.bind.apply(fn, slice.call(arguments, 1));
46 }
47
48 var args = slice.call(arguments, 2);
49
50 return function () {
51 return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);
52 };
53}
54
55// @property lastId: Number
56// Last unique ID used by [`stamp()`](#util-stamp)
57var lastId = 0;
58
59// @function stamp(obj: Object): Number
60// Returns the unique ID of an object, assigning it one if it doesn't have it.
61function stamp(obj) {
62 if (!('_leaflet_id' in obj)) {
63 obj['_leaflet_id'] = ++lastId;
64 }
65 return obj._leaflet_id;
66}
67
68// @function throttle(fn: Function, time: Number, context: Object): Function
69// Returns a function which executes function `fn` with the given scope `context`
70// (so that the `this` keyword refers to `context` inside `fn`'s code). The function
71// `fn` will be called no more than one time per given amount of `time`. The arguments
72// received by the bound function will be any arguments passed when binding the
73// function, followed by any arguments passed when invoking the bound function.
74// Has an `L.throttle` shortcut.
75function throttle(fn, time, context) {
76 var lock, args, wrapperFn, later;
77
78 later = function () {
79 // reset lock and call if queued
80 lock = false;
81 if (args) {
82 wrapperFn.apply(context, args);
83 args = false;
84 }
85 };
86
87 wrapperFn = function () {
88 if (lock) {
89 // called too soon, queue to call later
90 args = arguments;
91
92 } else {
93 // call and lock until later
94 fn.apply(context, arguments);
95 setTimeout(later, time);
96 lock = true;
97 }
98 };
99
100 return wrapperFn;
101}
102
103// @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number
104// Returns the number `num` modulo `range` in such a way so it lies within
105// `range[0]` and `range[1]`. The returned value will be always smaller than
106// `range[1]` unless `includeMax` is set to `true`.
107function wrapNum(x, range, includeMax) {
108 var max = range[1],
109 min = range[0],
110 d = max - min;
111 return x === max && includeMax ? x : ((x - min) % d + d) % d + min;
112}
113
114// @function falseFn(): Function
115// Returns a function which always returns `false`.
116function falseFn() { return false; }
117
118// @function formatNum(num: Number, precision?: Number|false): Number
119// Returns the number `num` rounded with specified `precision`.
120// The default `precision` value is 6 decimal places.
121// `false` can be passed to skip any processing (can be useful to avoid round-off errors).
122function formatNum(num, precision) {
123 if (precision === false) { return num; }
124 var pow = Math.pow(10, precision === undefined ? 6 : precision);
125 return Math.round(num * pow) / pow;
126}
127
128// @function trim(str: String): String
129// Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)
130function trim(str) {
131 return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
132}
133
134// @function splitWords(str: String): String[]
135// Trims and splits the string on whitespace and returns the array of parts.
136function splitWords(str) {
137 return trim(str).split(/\s+/);
138}
139
140// @function setOptions(obj: Object, options: Object): Object
141// Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.
142function setOptions(obj, options) {
143 if (!Object.prototype.hasOwnProperty.call(obj, 'options')) {
144 obj.options = obj.options ? create$2(obj.options) : {};
145 }
146 for (var i in options) {
147 obj.options[i] = options[i];
148 }
149 return obj.options;
150}
151
152// @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String
153// Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}`
154// translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will
155// be appended at the end. If `uppercase` is `true`, the parameter names will
156// be uppercased (e.g. `'?A=foo&B=bar'`)
157function getParamString(obj, existingUrl, uppercase) {
158 var params = [];
159 for (var i in obj) {
160 params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
161 }
162 return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
163}
164
165var templateRe = /\{ *([\w_ -]+) *\}/g;
166
167// @function template(str: String, data: Object): String
168// Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`
169// and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string
170// `('Hello foo, bar')`. You can also specify functions instead of strings for
171// data values — they will be evaluated passing `data` as an argument.
172function template(str, data) {
173 return str.replace(templateRe, function (str, key) {
174 var value = data[key];
175
176 if (value === undefined) {
177 throw new Error('No value provided for variable ' + str);
178
179 } else if (typeof value === 'function') {
180 value = value(data);
181 }
182 return value;
183 });
184}
185
186// @function isArray(obj): Boolean
187// Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
188var isArray = Array.isArray || function (obj) {
189 return (Object.prototype.toString.call(obj) === '[object Array]');
190};
191
192// @function indexOf(array: Array, el: Object): Number
193// Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)
194function indexOf(array, el) {
195 for (var i = 0; i < array.length; i++) {
196 if (array[i] === el) { return i; }
197 }
198 return -1;
199}
200
201// @property emptyImageUrl: String
202// Data URI string containing a base64-encoded empty GIF image.
203// Used as a hack to free memory from unused images on WebKit-powered
204// mobile devices (by setting image `src` to this string).
205var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
206
207// inspired by https://paulirish.com/2011/requestanimationframe-for-smart-animating/
208
209function getPrefixed(name) {
210 return window['webkit' + name] || window['moz' + name] || window['ms' + name];
211}
212
213var lastTime = 0;
214
215// fallback for IE 7-8
216function timeoutDefer(fn) {
217 var time = +new Date(),
218 timeToCall = Math.max(0, 16 - (time - lastTime));
219
220 lastTime = time + timeToCall;
221 return window.setTimeout(fn, timeToCall);
222}
223
224var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
225var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||
226 getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };
227
228// @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number
229// Schedules `fn` to be executed when the browser repaints. `fn` is bound to
230// `context` if given. When `immediate` is set, `fn` is called immediately if
231// the browser doesn't have native support for
232// [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),
233// otherwise it's delayed. Returns a request ID that can be used to cancel the request.
234function requestAnimFrame(fn, context, immediate) {
235 if (immediate && requestFn === timeoutDefer) {
236 fn.call(context);
237 } else {
238 return requestFn.call(window, bind(fn, context));
239 }
240}
241
242// @function cancelAnimFrame(id: Number): undefined
243// Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).
244function cancelAnimFrame(id) {
245 if (id) {
246 cancelFn.call(window, id);
247 }
248}
249
250var Util = {
251 __proto__: null,
252 extend: extend,
253 create: create$2,
254 bind: bind,
255 get lastId () { return lastId; },
256 stamp: stamp,
257 throttle: throttle,
258 wrapNum: wrapNum,
259 falseFn: falseFn,
260 formatNum: formatNum,
261 trim: trim,
262 splitWords: splitWords,
263 setOptions: setOptions,
264 getParamString: getParamString,
265 template: template,
266 isArray: isArray,
267 indexOf: indexOf,
268 emptyImageUrl: emptyImageUrl,
269 requestFn: requestFn,
270 cancelFn: cancelFn,
271 requestAnimFrame: requestAnimFrame,
272 cancelAnimFrame: cancelAnimFrame
273};
274
275// @class Class
276// @aka L.Class
277
278// @section
279// @uninheritable
280
281// Thanks to John Resig and Dean Edwards for inspiration!
282
283function Class() {}
284
285Class.extend = function (props) {
286
287 // @function extend(props: Object): Function
288 // [Extends the current class](#class-inheritance) given the properties to be included.
289 // Returns a Javascript function that is a class constructor (to be called with `new`).
290 var NewClass = function () {
291
292 setOptions(this);
293
294 // call the constructor
295 if (this.initialize) {
296 this.initialize.apply(this, arguments);
297 }
298
299 // call all constructor hooks
300 this.callInitHooks();
301 };
302
303 var parentProto = NewClass.__super__ = this.prototype;
304
305 var proto = create$2(parentProto);
306 proto.constructor = NewClass;
307
308 NewClass.prototype = proto;
309
310 // inherit parent's statics
311 for (var i in this) {
312 if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') {
313 NewClass[i] = this[i];
314 }
315 }
316
317 // mix static properties into the class
318 if (props.statics) {
319 extend(NewClass, props.statics);
320 }
321
322 // mix includes into the prototype
323 if (props.includes) {
324 checkDeprecatedMixinEvents(props.includes);
325 extend.apply(null, [proto].concat(props.includes));
326 }
327
328 // mix given properties into the prototype
329 extend(proto, props);
330 delete proto.statics;
331 delete proto.includes;
332
333 // merge options
334 if (proto.options) {
335 proto.options = parentProto.options ? create$2(parentProto.options) : {};
336 extend(proto.options, props.options);
337 }
338
339 proto._initHooks = [];
340
341 // add method for calling all hooks
342 proto.callInitHooks = function () {
343
344 if (this._initHooksCalled) { return; }
345
346 if (parentProto.callInitHooks) {
347 parentProto.callInitHooks.call(this);
348 }
349
350 this._initHooksCalled = true;
351
352 for (var i = 0, len = proto._initHooks.length; i < len; i++) {
353 proto._initHooks[i].call(this);
354 }
355 };
356
357 return NewClass;
358};
359
360
361// @function include(properties: Object): this
362// [Includes a mixin](#class-includes) into the current class.
363Class.include = function (props) {
364 var parentOptions = this.prototype.options;
365 extend(this.prototype, props);
366 if (props.options) {
367 this.prototype.options = parentOptions;
368 this.mergeOptions(props.options);
369 }
370 return this;
371};
372
373// @function mergeOptions(options: Object): this
374// [Merges `options`](#class-options) into the defaults of the class.
375Class.mergeOptions = function (options) {
376 extend(this.prototype.options, options);
377 return this;
378};
379
380// @function addInitHook(fn: Function): this
381// Adds a [constructor hook](#class-constructor-hooks) to the class.
382Class.addInitHook = function (fn) { // (Function) || (String, args...)
383 var args = Array.prototype.slice.call(arguments, 1);
384
385 var init = typeof fn === 'function' ? fn : function () {
386 this[fn].apply(this, args);
387 };
388
389 this.prototype._initHooks = this.prototype._initHooks || [];
390 this.prototype._initHooks.push(init);
391 return this;
392};
393
394function checkDeprecatedMixinEvents(includes) {
395 if (typeof L === 'undefined' || !L || !L.Mixin) { return; }
396
397 includes = isArray(includes) ? includes : [includes];
398
399 for (var i = 0; i < includes.length; i++) {
400 if (includes[i] === L.Mixin.Events) {
401 console.warn('Deprecated include of L.Mixin.Events: ' +
402 'this property will be removed in future releases, ' +
403 'please inherit from L.Evented instead.', new Error().stack);
404 }
405 }
406}
407
408/*
409 * @class Evented
410 * @aka L.Evented
411 * @inherits Class
412 *
413 * 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).
414 *
415 * @example
416 *
417 * ```js
418 * map.on('click', function(e) {
419 * alert(e.latlng);
420 * } );
421 * ```
422 *
423 * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
424 *
425 * ```js
426 * function onClick(e) { ... }
427 *
428 * map.on('click', onClick);
429 * map.off('click', onClick);
430 * ```
431 */
432
433var Events = {
434 /* @method on(type: String, fn: Function, context?: Object): this
435 * 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'`).
436 *
437 * @alternative
438 * @method on(eventMap: Object): this
439 * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
440 */
441 on: function (types, fn, context) {
442
443 // types can be a map of types/handlers
444 if (typeof types === 'object') {
445 for (var type in types) {
446 // we don't process space-separated events here for performance;
447 // it's a hot path since Layer uses the on(obj) syntax
448 this._on(type, types[type], fn);
449 }
450
451 } else {
452 // types can be a string of space-separated words
453 types = splitWords(types);
454
455 for (var i = 0, len = types.length; i < len; i++) {
456 this._on(types[i], fn, context);
457 }
458 }
459
460 return this;
461 },
462
463 /* @method off(type: String, fn?: Function, context?: Object): this
464 * 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.
465 *
466 * @alternative
467 * @method off(eventMap: Object): this
468 * Removes a set of type/listener pairs.
469 *
470 * @alternative
471 * @method off: this
472 * Removes all listeners to all events on the object. This includes implicitly attached events.
473 */
474 off: function (types, fn, context) {
475
476 if (!arguments.length) {
477 // clear all listeners if called without arguments
478 delete this._events;
479
480 } else if (typeof types === 'object') {
481 for (var type in types) {
482 this._off(type, types[type], fn);
483 }
484
485 } else {
486 types = splitWords(types);
487
488 var removeAll = arguments.length === 1;
489 for (var i = 0, len = types.length; i < len; i++) {
490 if (removeAll) {
491 this._off(types[i]);
492 } else {
493 this._off(types[i], fn, context);
494 }
495 }
496 }
497
498 return this;
499 },
500
501 // attach listener (without syntactic sugar now)
502 _on: function (type, fn, context) {
503 if (typeof fn !== 'function') {
504 console.warn('wrong listener type: ' + typeof fn);
505 return;
506 }
507 this._events = this._events || {};
508
509 /* get/init listeners for type */
510 var typeListeners = this._events[type];
511 if (!typeListeners) {
512 typeListeners = [];
513 this._events[type] = typeListeners;
514 }
515
516 if (context === this) {
517 // Less memory footprint.
518 context = undefined;
519 }
520 var newListener = {fn: fn, ctx: context},
521 listeners = typeListeners;
522
523 // check if fn already there
524 for (var i = 0, len = listeners.length; i < len; i++) {
525 if (listeners[i].fn === fn && listeners[i].ctx === context) {
526 return;
527 }
528 }
529
530 listeners.push(newListener);
531 },
532
533 _off: function (type, fn, context) {
534 var listeners,
535 i,
536 len;
537
538 if (!this._events) { return; }
539
540 listeners = this._events[type];
541
542 if (!listeners) {
543 return;
544 }
545
546 if (arguments.length === 1) { // remove all
547 if (this._firingCount) {
548 // Set all removed listeners to noop
549 // so they are not called if remove happens in fire
550 for (i = 0, len = listeners.length; i < len; i++) {
551 listeners[i].fn = falseFn;
552 }
553 }
554 // clear all listeners for a type if function isn't specified
555 delete this._events[type];
556 return;
557 }
558
559 if (context === this) {
560 context = undefined;
561 }
562
563 if (typeof fn !== 'function') {
564 console.warn('wrong listener type: ' + typeof fn);
565 return;
566 }
567 // find fn and remove it
568 for (i = 0, len = listeners.length; i < len; i++) {
569 var l = listeners[i];
570 if (l.ctx !== context) { continue; }
571 if (l.fn === fn) {
572 if (this._firingCount) {
573 // set the removed listener to noop so that's not called if remove happens in fire
574 l.fn = falseFn;
575
576 /* copy array in case events are being fired */
577 this._events[type] = listeners = listeners.slice();
578 }
579 listeners.splice(i, 1);
580
581 return;
582 }
583 }
584 console.warn('listener not found');
585 },
586
587 // @method fire(type: String, data?: Object, propagate?: Boolean): this
588 // Fires an event of the specified type. You can optionally provide a data
589 // object — the first argument of the listener function will contain its
590 // properties. The event can optionally be propagated to event parents.
591 fire: function (type, data, propagate) {
592 if (!this.listens(type, propagate)) { return this; }
593
594 var event = extend({}, data, {
595 type: type,
596 target: this,
597 sourceTarget: data && data.sourceTarget || this
598 });
599
600 if (this._events) {
601 var listeners = this._events[type];
602
603 if (listeners) {
604 this._firingCount = (this._firingCount + 1) || 1;
605 for (var i = 0, len = listeners.length; i < len; i++) {
606 var l = listeners[i];
607 l.fn.call(l.ctx || this, event);
608 }
609
610 this._firingCount--;
611 }
612 }
613
614 if (propagate) {
615 // propagate the event to parents (set with addEventParent)
616 this._propagateEvent(event);
617 }
618
619 return this;
620 },
621
622 // @method listens(type: String, 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, propagate) {
626 if (typeof type !== 'string') {
627 console.warn('"string" type argument expected');
628 }
629 var listeners = this._events && this._events[type];
630 if (listeners && listeners.length) { return true; }
631
632 if (propagate) {
633 // also check parents for listeners if event propagates
634 for (var id in this._eventParents) {
635 if (this._eventParents[id].listens(type, propagate)) { return true; }
636 }
637 }
638 return false;
639 },
640
641 // @method once(…): this
642 // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.
643 once: function (types, fn, context) {
644
645 if (typeof types === 'object') {
646 for (var type in types) {
647 this.once(type, types[type], fn);
648 }
649 return this;
650 }
651
652 var handler = bind(function () {
653 this
654 .off(types, fn, context)
655 .off(types, handler, context);
656 }, this);
657
658 // add a listener that's executed once and removed after that
659 return this
660 .on(types, fn, context)
661 .on(types, handler, context);
662 },
663
664 // @method addEventParent(obj: Evented): this
665 // Adds an event parent - an `Evented` that will receive propagated events
666 addEventParent: function (obj) {
667 this._eventParents = this._eventParents || {};
668 this._eventParents[stamp(obj)] = obj;
669 return this;
670 },
671
672 // @method removeEventParent(obj: Evented): this
673 // Removes an event parent, so it will stop receiving propagated events
674 removeEventParent: function (obj) {
675 if (this._eventParents) {
676 delete this._eventParents[stamp(obj)];
677 }
678 return this;
679 },
680
681 _propagateEvent: function (e) {
682 for (var id in this._eventParents) {
683 this._eventParents[id].fire(e.type, extend({
684 layer: e.target,
685 propagatedFrom: e.target
686 }, e), true);
687 }
688 }
689};
690
691// aliases; we should ditch those eventually
692
693// @method addEventListener(…): this
694// Alias to [`on(…)`](#evented-on)
695Events.addEventListener = Events.on;
696
697// @method removeEventListener(…): this
698// Alias to [`off(…)`](#evented-off)
699
700// @method clearAllEventListeners(…): this
701// Alias to [`off()`](#evented-off)
702Events.removeEventListener = Events.clearAllEventListeners = Events.off;
703
704// @method addOneTimeEventListener(…): this
705// Alias to [`once(…)`](#evented-once)
706Events.addOneTimeEventListener = Events.once;
707
708// @method fireEvent(…): this
709// Alias to [`fire(…)`](#evented-fire)
710Events.fireEvent = Events.fire;
711
712// @method hasEventListeners(…): Boolean
713// Alias to [`listens(…)`](#evented-listens)
714Events.hasEventListeners = Events.listens;
715
716var Evented = Class.extend(Events);
717
718/*
719 * @class Point
720 * @aka L.Point
721 *
722 * Represents a point with `x` and `y` coordinates in pixels.
723 *
724 * @example
725 *
726 * ```js
727 * var point = L.point(200, 300);
728 * ```
729 *
730 * 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:
731 *
732 * ```js
733 * map.panBy([200, 300]);
734 * map.panBy(L.point(200, 300));
735 * ```
736 *
737 * Note that `Point` does not inherit from Leaflet's `Class` object,
738 * which means new classes can't inherit from it, and new methods
739 * can't be added to it with the `include` function.
740 */
741
742function Point(x, y, round) {
743 // @property x: Number; The `x` coordinate of the point
744 this.x = (round ? Math.round(x) : x);
745 // @property y: Number; The `y` coordinate of the point
746 this.y = (round ? Math.round(y) : y);
747}
748
749var trunc = Math.trunc || function (v) {
750 return v > 0 ? Math.floor(v) : Math.ceil(v);
751};
752
753Point.prototype = {
754
755 // @method clone(): Point
756 // Returns a copy of the current point.
757 clone: function () {
758 return new Point(this.x, this.y);
759 },
760
761 // @method add(otherPoint: Point): Point
762 // Returns the result of addition of the current and the given points.
763 add: function (point) {
764 // non-destructive, returns a new point
765 return this.clone()._add(toPoint(point));
766 },
767
768 _add: function (point) {
769 // destructive, used directly for performance in situations where it's safe to modify existing point
770 this.x += point.x;
771 this.y += point.y;
772 return this;
773 },
774
775 // @method subtract(otherPoint: Point): Point
776 // Returns the result of subtraction of the given point from the current.
777 subtract: function (point) {
778 return this.clone()._subtract(toPoint(point));
779 },
780
781 _subtract: function (point) {
782 this.x -= point.x;
783 this.y -= point.y;
784 return this;
785 },
786
787 // @method divideBy(num: Number): Point
788 // Returns the result of division of the current point by the given number.
789 divideBy: function (num) {
790 return this.clone()._divideBy(num);
791 },
792
793 _divideBy: function (num) {
794 this.x /= num;
795 this.y /= num;
796 return this;
797 },
798
799 // @method multiplyBy(num: Number): Point
800 // Returns the result of multiplication of the current point by the given number.
801 multiplyBy: function (num) {
802 return this.clone()._multiplyBy(num);
803 },
804
805 _multiplyBy: function (num) {
806 this.x *= num;
807 this.y *= num;
808 return this;
809 },
810
811 // @method scaleBy(scale: Point): Point
812 // Multiply each coordinate of the current point by each coordinate of
813 // `scale`. In linear algebra terms, multiply the point by the
814 // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
815 // defined by `scale`.
816 scaleBy: function (point) {
817 return new Point(this.x * point.x, this.y * point.y);
818 },
819
820 // @method unscaleBy(scale: Point): Point
821 // Inverse of `scaleBy`. Divide each coordinate of the current point by
822 // each coordinate of `scale`.
823 unscaleBy: function (point) {
824 return new Point(this.x / point.x, this.y / point.y);
825 },
826
827 // @method round(): Point
828 // Returns a copy of the current point with rounded coordinates.
829 round: function () {
830 return this.clone()._round();
831 },
832
833 _round: function () {
834 this.x = Math.round(this.x);
835 this.y = Math.round(this.y);
836 return this;
837 },
838
839 // @method floor(): Point
840 // Returns a copy of the current point with floored coordinates (rounded down).
841 floor: function () {
842 return this.clone()._floor();
843 },
844
845 _floor: function () {
846 this.x = Math.floor(this.x);
847 this.y = Math.floor(this.y);
848 return this;
849 },
850
851 // @method ceil(): Point
852 // Returns a copy of the current point with ceiled coordinates (rounded up).
853 ceil: function () {
854 return this.clone()._ceil();
855 },
856
857 _ceil: function () {
858 this.x = Math.ceil(this.x);
859 this.y = Math.ceil(this.y);
860 return this;
861 },
862
863 // @method trunc(): Point
864 // Returns a copy of the current point with truncated coordinates (rounded towards zero).
865 trunc: function () {
866 return this.clone()._trunc();
867 },
868
869 _trunc: function () {
870 this.x = trunc(this.x);
871 this.y = trunc(this.y);
872 return this;
873 },
874
875 // @method distanceTo(otherPoint: Point): Number
876 // Returns the cartesian distance between the current and the given points.
877 distanceTo: function (point) {
878 point = toPoint(point);
879
880 var x = point.x - this.x,
881 y = point.y - this.y;
882
883 return Math.sqrt(x * x + y * y);
884 },
885
886 // @method equals(otherPoint: Point): Boolean
887 // Returns `true` if the given point has the same coordinates.
888 equals: function (point) {
889 point = toPoint(point);
890
891 return point.x === this.x &&
892 point.y === this.y;
893 },
894
895 // @method contains(otherPoint: Point): Boolean
896 // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
897 contains: function (point) {
898 point = toPoint(point);
899
900 return Math.abs(point.x) <= Math.abs(this.x) &&
901 Math.abs(point.y) <= Math.abs(this.y);
902 },
903
904 // @method toString(): String
905 // Returns a string representation of the point for debugging purposes.
906 toString: function () {
907 return 'Point(' +
908 formatNum(this.x) + ', ' +
909 formatNum(this.y) + ')';
910 }
911};
912
913// @factory L.point(x: Number, y: Number, round?: Boolean)
914// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.
915
916// @alternative
917// @factory L.point(coords: Number[])
918// Expects an array of the form `[x, y]` instead.
919
920// @alternative
921// @factory L.point(coords: Object)
922// Expects a plain object of the form `{x: Number, y: Number}` instead.
923function toPoint(x, y, round) {
924 if (x instanceof Point) {
925 return x;
926 }
927 if (isArray(x)) {
928 return new Point(x[0], x[1]);
929 }
930 if (x === undefined || x === null) {
931 return x;
932 }
933 if (typeof x === 'object' && 'x' in x && 'y' in x) {
934 return new Point(x.x, x.y);
935 }
936 return new Point(x, y, round);
937}
938
939/*
940 * @class Bounds
941 * @aka L.Bounds
942 *
943 * Represents a rectangular area in pixel coordinates.
944 *
945 * @example
946 *
947 * ```js
948 * var p1 = L.point(10, 10),
949 * p2 = L.point(40, 60),
950 * bounds = L.bounds(p1, p2);
951 * ```
952 *
953 * 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:
954 *
955 * ```js
956 * otherBounds.intersects([[10, 10], [40, 60]]);
957 * ```
958 *
959 * Note that `Bounds` does not inherit from Leaflet's `Class` object,
960 * which means new classes can't inherit from it, and new methods
961 * can't be added to it with the `include` function.
962 */
963
964function Bounds(a, b) {
965 if (!a) { return; }
966
967 var points = b ? [a, b] : a;
968
969 for (var i = 0, len = points.length; i < len; i++) {
970 this.extend(points[i]);
971 }
972}
973
974Bounds.prototype = {
975 // @method extend(point: Point): this
976 // Extends the bounds to contain the given point.
977 extend: function (point) { // (Point)
978 point = toPoint(point);
979
980 // @property min: Point
981 // The top left corner of the rectangle.
982 // @property max: Point
983 // The bottom right corner of the rectangle.
984 if (!this.min && !this.max) {
985 this.min = point.clone();
986 this.max = point.clone();
987 } else {
988 this.min.x = Math.min(point.x, this.min.x);
989 this.max.x = Math.max(point.x, this.max.x);
990 this.min.y = Math.min(point.y, this.min.y);
991 this.max.y = Math.max(point.y, this.max.y);
992 }
993 return this;
994 },
995
996 // @method getCenter(round?: Boolean): Point
997 // Returns the center point of the bounds.
998 getCenter: function (round) {
999 return new Point(
1000 (this.min.x + this.max.x) / 2,
1001 (this.min.y + this.max.y) / 2, round);
1002 },
1003
1004 // @method getBottomLeft(): Point
1005 // Returns the bottom-left point of the bounds.
1006 getBottomLeft: function () {
1007 return new Point(this.min.x, this.max.y);
1008 },
1009
1010 // @method getTopRight(): Point
1011 // Returns the top-right point of the bounds.
1012 getTopRight: function () { // -> Point
1013 return new Point(this.max.x, this.min.y);
1014 },
1015
1016 // @method getTopLeft(): Point
1017 // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).
1018 getTopLeft: function () {
1019 return this.min; // left, top
1020 },
1021
1022 // @method getBottomRight(): Point
1023 // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).
1024 getBottomRight: function () {
1025 return this.max; // right, bottom
1026 },
1027
1028 // @method getSize(): Point
1029 // Returns the size of the given bounds
1030 getSize: function () {
1031 return this.max.subtract(this.min);
1032 },
1033
1034 // @method contains(otherBounds: Bounds): Boolean
1035 // Returns `true` if the rectangle contains the given one.
1036 // @alternative
1037 // @method contains(point: Point): Boolean
1038 // Returns `true` if the rectangle contains the given point.
1039 contains: function (obj) {
1040 var min, max;
1041
1042 if (typeof obj[0] === 'number' || obj instanceof Point) {
1043 obj = toPoint(obj);
1044 } else {
1045 obj = toBounds(obj);
1046 }
1047
1048 if (obj instanceof Bounds) {
1049 min = obj.min;
1050 max = obj.max;
1051 } else {
1052 min = max = obj;
1053 }
1054
1055 return (min.x >= this.min.x) &&
1056 (max.x <= this.max.x) &&
1057 (min.y >= this.min.y) &&
1058 (max.y <= this.max.y);
1059 },
1060
1061 // @method intersects(otherBounds: Bounds): Boolean
1062 // Returns `true` if the rectangle intersects the given bounds. Two bounds
1063 // intersect if they have at least one point in common.
1064 intersects: function (bounds) { // (Bounds) -> Boolean
1065 bounds = toBounds(bounds);
1066
1067 var min = this.min,
1068 max = this.max,
1069 min2 = bounds.min,
1070 max2 = bounds.max,
1071 xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
1072 yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
1073
1074 return xIntersects && yIntersects;
1075 },
1076
1077 // @method overlaps(otherBounds: Bounds): Boolean
1078 // Returns `true` if the rectangle overlaps the given bounds. Two bounds
1079 // overlap if their intersection is an area.
1080 overlaps: function (bounds) { // (Bounds) -> Boolean
1081 bounds = toBounds(bounds);
1082
1083 var min = this.min,
1084 max = this.max,
1085 min2 = bounds.min,
1086 max2 = bounds.max,
1087 xOverlaps = (max2.x > min.x) && (min2.x < max.x),
1088 yOverlaps = (max2.y > min.y) && (min2.y < max.y);
1089
1090 return xOverlaps && yOverlaps;
1091 },
1092
1093 isValid: function () {
1094 return !!(this.min && this.max);
1095 }
1096};
1097
1098
1099// @factory L.bounds(corner1: Point, corner2: Point)
1100// Creates a Bounds object from two corners coordinate pairs.
1101// @alternative
1102// @factory L.bounds(points: Point[])
1103// Creates a Bounds object from the given array of points.
1104function toBounds(a, b) {
1105 if (!a || a instanceof Bounds) {
1106 return a;
1107 }
1108 return new Bounds(a, b);
1109}
1110
1111/*
1112 * @class LatLngBounds
1113 * @aka L.LatLngBounds
1114 *
1115 * Represents a rectangular geographical area on a map.
1116 *
1117 * @example
1118 *
1119 * ```js
1120 * var corner1 = L.latLng(40.712, -74.227),
1121 * corner2 = L.latLng(40.774, -74.125),
1122 * bounds = L.latLngBounds(corner1, corner2);
1123 * ```
1124 *
1125 * 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:
1126 *
1127 * ```js
1128 * map.fitBounds([
1129 * [40.712, -74.227],
1130 * [40.774, -74.125]
1131 * ]);
1132 * ```
1133 *
1134 * 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.
1135 *
1136 * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object,
1137 * which means new classes can't inherit from it, and new methods
1138 * can't be added to it with the `include` function.
1139 */
1140
1141function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
1142 if (!corner1) { return; }
1143
1144 var latlngs = corner2 ? [corner1, corner2] : corner1;
1145
1146 for (var i = 0, len = latlngs.length; i < len; i++) {
1147 this.extend(latlngs[i]);
1148 }
1149}
1150
1151LatLngBounds.prototype = {
1152
1153 // @method extend(latlng: LatLng): this
1154 // Extend the bounds to contain the given point
1155
1156 // @alternative
1157 // @method extend(otherBounds: LatLngBounds): this
1158 // Extend the bounds to contain the given bounds
1159 extend: function (obj) {
1160 var sw = this._southWest,
1161 ne = this._northEast,
1162 sw2, ne2;
1163
1164 if (obj instanceof LatLng) {
1165 sw2 = obj;
1166 ne2 = obj;
1167
1168 } else if (obj instanceof LatLngBounds) {
1169 sw2 = obj._southWest;
1170 ne2 = obj._northEast;
1171
1172 if (!sw2 || !ne2) { return this; }
1173
1174 } else {
1175 return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this;
1176 }
1177
1178 if (!sw && !ne) {
1179 this._southWest = new LatLng(sw2.lat, sw2.lng);
1180 this._northEast = new LatLng(ne2.lat, ne2.lng);
1181 } else {
1182 sw.lat = Math.min(sw2.lat, sw.lat);
1183 sw.lng = Math.min(sw2.lng, sw.lng);
1184 ne.lat = Math.max(ne2.lat, ne.lat);
1185 ne.lng = Math.max(ne2.lng, ne.lng);
1186 }
1187
1188 return this;
1189 },
1190
1191 // @method pad(bufferRatio: Number): LatLngBounds
1192 // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
1193 // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
1194 // Negative values will retract the bounds.
1195 pad: function (bufferRatio) {
1196 var sw = this._southWest,
1197 ne = this._northEast,
1198 heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
1199 widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
1200
1201 return new LatLngBounds(
1202 new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
1203 new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
1204 },
1205
1206 // @method getCenter(): LatLng
1207 // Returns the center point of the bounds.
1208 getCenter: function () {
1209 return new LatLng(
1210 (this._southWest.lat + this._northEast.lat) / 2,
1211 (this._southWest.lng + this._northEast.lng) / 2);
1212 },
1213
1214 // @method getSouthWest(): LatLng
1215 // Returns the south-west point of the bounds.
1216 getSouthWest: function () {
1217 return this._southWest;
1218 },
1219
1220 // @method getNorthEast(): LatLng
1221 // Returns the north-east point of the bounds.
1222 getNorthEast: function () {
1223 return this._northEast;
1224 },
1225
1226 // @method getNorthWest(): LatLng
1227 // Returns the north-west point of the bounds.
1228 getNorthWest: function () {
1229 return new LatLng(this.getNorth(), this.getWest());
1230 },
1231
1232 // @method getSouthEast(): LatLng
1233 // Returns the south-east point of the bounds.
1234 getSouthEast: function () {
1235 return new LatLng(this.getSouth(), this.getEast());
1236 },
1237
1238 // @method getWest(): Number
1239 // Returns the west longitude of the bounds
1240 getWest: function () {
1241 return this._southWest.lng;
1242 },
1243
1244 // @method getSouth(): Number
1245 // Returns the south latitude of the bounds
1246 getSouth: function () {
1247 return this._southWest.lat;
1248 },
1249
1250 // @method getEast(): Number
1251 // Returns the east longitude of the bounds
1252 getEast: function () {
1253 return this._northEast.lng;
1254 },
1255
1256 // @method getNorth(): Number
1257 // Returns the north latitude of the bounds
1258 getNorth: function () {
1259 return this._northEast.lat;
1260 },
1261
1262 // @method contains(otherBounds: LatLngBounds): Boolean
1263 // Returns `true` if the rectangle contains the given one.
1264
1265 // @alternative
1266 // @method contains (latlng: LatLng): Boolean
1267 // Returns `true` if the rectangle contains the given point.
1268 contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
1269 if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) {
1270 obj = toLatLng(obj);
1271 } else {
1272 obj = toLatLngBounds(obj);
1273 }
1274
1275 var sw = this._southWest,
1276 ne = this._northEast,
1277 sw2, ne2;
1278
1279 if (obj instanceof LatLngBounds) {
1280 sw2 = obj.getSouthWest();
1281 ne2 = obj.getNorthEast();
1282 } else {
1283 sw2 = ne2 = obj;
1284 }
1285
1286 return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
1287 (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
1288 },
1289
1290 // @method intersects(otherBounds: LatLngBounds): Boolean
1291 // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.
1292 intersects: function (bounds) {
1293 bounds = toLatLngBounds(bounds);
1294
1295 var sw = this._southWest,
1296 ne = this._northEast,
1297 sw2 = bounds.getSouthWest(),
1298 ne2 = bounds.getNorthEast(),
1299
1300 latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
1301 lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
1302
1303 return latIntersects && lngIntersects;
1304 },
1305
1306 // @method overlaps(otherBounds: LatLngBounds): Boolean
1307 // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.
1308 overlaps: function (bounds) {
1309 bounds = toLatLngBounds(bounds);
1310
1311 var sw = this._southWest,
1312 ne = this._northEast,
1313 sw2 = bounds.getSouthWest(),
1314 ne2 = bounds.getNorthEast(),
1315
1316 latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
1317 lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
1318
1319 return latOverlaps && lngOverlaps;
1320 },
1321
1322 // @method toBBoxString(): String
1323 // 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.
1324 toBBoxString: function () {
1325 return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
1326 },
1327
1328 // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean
1329 // 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.
1330 equals: function (bounds, maxMargin) {
1331 if (!bounds) { return false; }
1332
1333 bounds = toLatLngBounds(bounds);
1334
1335 return this._southWest.equals(bounds.getSouthWest(), maxMargin) &&
1336 this._northEast.equals(bounds.getNorthEast(), maxMargin);
1337 },
1338
1339 // @method isValid(): Boolean
1340 // Returns `true` if the bounds are properly initialized.
1341 isValid: function () {
1342 return !!(this._southWest && this._northEast);
1343 }
1344};
1345
1346// TODO International date line?
1347
1348// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
1349// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
1350
1351// @alternative
1352// @factory L.latLngBounds(latlngs: LatLng[])
1353// 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).
1354function toLatLngBounds(a, b) {
1355 if (a instanceof LatLngBounds) {
1356 return a;
1357 }
1358 return new LatLngBounds(a, b);
1359}
1360
1361/* @class LatLng
1362 * @aka L.LatLng
1363 *
1364 * Represents a geographical point with a certain latitude and longitude.
1365 *
1366 * @example
1367 *
1368 * ```
1369 * var latlng = L.latLng(50.5, 30.5);
1370 * ```
1371 *
1372 * 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:
1373 *
1374 * ```
1375 * map.panTo([50, 30]);
1376 * map.panTo({lon: 30, lat: 50});
1377 * map.panTo({lat: 50, lng: 30});
1378 * map.panTo(L.latLng(50, 30));
1379 * ```
1380 *
1381 * Note that `LatLng` does not inherit from Leaflet's `Class` object,
1382 * which means new classes can't inherit from it, and new methods
1383 * can't be added to it with the `include` function.
1384 */
1385
1386function LatLng(lat, lng, alt) {
1387 if (isNaN(lat) || isNaN(lng)) {
1388 throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
1389 }
1390
1391 // @property lat: Number
1392 // Latitude in degrees
1393 this.lat = +lat;
1394
1395 // @property lng: Number
1396 // Longitude in degrees
1397 this.lng = +lng;
1398
1399 // @property alt: Number
1400 // Altitude in meters (optional)
1401 if (alt !== undefined) {
1402 this.alt = +alt;
1403 }
1404}
1405
1406LatLng.prototype = {
1407 // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean
1408 // 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.
1409 equals: function (obj, maxMargin) {
1410 if (!obj) { return false; }
1411
1412 obj = toLatLng(obj);
1413
1414 var margin = Math.max(
1415 Math.abs(this.lat - obj.lat),
1416 Math.abs(this.lng - obj.lng));
1417
1418 return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
1419 },
1420
1421 // @method toString(): String
1422 // Returns a string representation of the point (for debugging purposes).
1423 toString: function (precision) {
1424 return 'LatLng(' +
1425 formatNum(this.lat, precision) + ', ' +
1426 formatNum(this.lng, precision) + ')';
1427 },
1428
1429 // @method distanceTo(otherLatLng: LatLng): Number
1430 // 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).
1431 distanceTo: function (other) {
1432 return Earth.distance(this, toLatLng(other));
1433 },
1434
1435 // @method wrap(): LatLng
1436 // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.
1437 wrap: function () {
1438 return Earth.wrapLatLng(this);
1439 },
1440
1441 // @method toBounds(sizeInMeters: Number): LatLngBounds
1442 // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.
1443 toBounds: function (sizeInMeters) {
1444 var latAccuracy = 180 * sizeInMeters / 40075017,
1445 lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
1446
1447 return toLatLngBounds(
1448 [this.lat - latAccuracy, this.lng - lngAccuracy],
1449 [this.lat + latAccuracy, this.lng + lngAccuracy]);
1450 },
1451
1452 clone: function () {
1453 return new LatLng(this.lat, this.lng, this.alt);
1454 }
1455};
1456
1457
1458
1459// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng
1460// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).
1461
1462// @alternative
1463// @factory L.latLng(coords: Array): LatLng
1464// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.
1465
1466// @alternative
1467// @factory L.latLng(coords: Object): LatLng
1468// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.
1469
1470function toLatLng(a, b, c) {
1471 if (a instanceof LatLng) {
1472 return a;
1473 }
1474 if (isArray(a) && typeof a[0] !== 'object') {
1475 if (a.length === 3) {
1476 return new LatLng(a[0], a[1], a[2]);
1477 }
1478 if (a.length === 2) {
1479 return new LatLng(a[0], a[1]);
1480 }
1481 return null;
1482 }
1483 if (a === undefined || a === null) {
1484 return a;
1485 }
1486 if (typeof a === 'object' && 'lat' in a) {
1487 return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
1488 }
1489 if (b === undefined) {
1490 return null;
1491 }
1492 return new LatLng(a, b, c);
1493}
1494
1495/*
1496 * @namespace CRS
1497 * @crs L.CRS.Base
1498 * Object that defines coordinate reference systems for projecting
1499 * geographical points into pixel (screen) coordinates and back (and to
1500 * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See
1501 * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system).
1502 *
1503 * Leaflet defines the most usual CRSs by default. If you want to use a
1504 * CRS not defined by default, take a look at the
1505 * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.
1506 *
1507 * Note that the CRS instances do not inherit from Leaflet's `Class` object,
1508 * and can't be instantiated. Also, new classes can't inherit from them,
1509 * and methods can't be added to them with the `include` function.
1510 */
1511
1512var CRS = {
1513 // @method latLngToPoint(latlng: LatLng, zoom: Number): Point
1514 // Projects geographical coordinates into pixel coordinates for a given zoom.
1515 latLngToPoint: function (latlng, zoom) {
1516 var projectedPoint = this.projection.project(latlng),
1517 scale = this.scale(zoom);
1518
1519 return this.transformation._transform(projectedPoint, scale);
1520 },
1521
1522 // @method pointToLatLng(point: Point, zoom: Number): LatLng
1523 // The inverse of `latLngToPoint`. Projects pixel coordinates on a given
1524 // zoom into geographical coordinates.
1525 pointToLatLng: function (point, zoom) {
1526 var scale = this.scale(zoom),
1527 untransformedPoint = this.transformation.untransform(point, scale);
1528
1529 return this.projection.unproject(untransformedPoint);
1530 },
1531
1532 // @method project(latlng: LatLng): Point
1533 // Projects geographical coordinates into coordinates in units accepted for
1534 // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).
1535 project: function (latlng) {
1536 return this.projection.project(latlng);
1537 },
1538
1539 // @method unproject(point: Point): LatLng
1540 // Given a projected coordinate returns the corresponding LatLng.
1541 // The inverse of `project`.
1542 unproject: function (point) {
1543 return this.projection.unproject(point);
1544 },
1545
1546 // @method scale(zoom: Number): Number
1547 // Returns the scale used when transforming projected coordinates into
1548 // pixel coordinates for a particular zoom. For example, it returns
1549 // `256 * 2^zoom` for Mercator-based CRS.
1550 scale: function (zoom) {
1551 return 256 * Math.pow(2, zoom);
1552 },
1553
1554 // @method zoom(scale: Number): Number
1555 // Inverse of `scale()`, returns the zoom level corresponding to a scale
1556 // factor of `scale`.
1557 zoom: function (scale) {
1558 return Math.log(scale / 256) / Math.LN2;
1559 },
1560
1561 // @method getProjectedBounds(zoom: Number): Bounds
1562 // Returns the projection's bounds scaled and transformed for the provided `zoom`.
1563 getProjectedBounds: function (zoom) {
1564 if (this.infinite) { return null; }
1565
1566 var b = this.projection.bounds,
1567 s = this.scale(zoom),
1568 min = this.transformation.transform(b.min, s),
1569 max = this.transformation.transform(b.max, s);
1570
1571 return new Bounds(min, max);
1572 },
1573
1574 // @method distance(latlng1: LatLng, latlng2: LatLng): Number
1575 // Returns the distance between two geographical coordinates.
1576
1577 // @property code: String
1578 // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)
1579 //
1580 // @property wrapLng: Number[]
1581 // An array of two numbers defining whether the longitude (horizontal) coordinate
1582 // axis wraps around a given range and how. Defaults to `[-180, 180]` in most
1583 // geographical CRSs. If `undefined`, the longitude axis does not wrap around.
1584 //
1585 // @property wrapLat: Number[]
1586 // Like `wrapLng`, but for the latitude (vertical) axis.
1587
1588 // wrapLng: [min, max],
1589 // wrapLat: [min, max],
1590
1591 // @property infinite: Boolean
1592 // If true, the coordinate space will be unbounded (infinite in both axes)
1593 infinite: false,
1594
1595 // @method wrapLatLng(latlng: LatLng): LatLng
1596 // Returns a `LatLng` where lat and lng has been wrapped according to the
1597 // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.
1598 wrapLatLng: function (latlng) {
1599 var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,
1600 lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat,
1601 alt = latlng.alt;
1602
1603 return new LatLng(lat, lng, alt);
1604 },
1605
1606 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
1607 // Returns a `LatLngBounds` with the same size as the given one, ensuring
1608 // that its center is within the CRS's bounds.
1609 // Only accepts actual `L.LatLngBounds` instances, not arrays.
1610 wrapLatLngBounds: function (bounds) {
1611 var center = bounds.getCenter(),
1612 newCenter = this.wrapLatLng(center),
1613 latShift = center.lat - newCenter.lat,
1614 lngShift = center.lng - newCenter.lng;
1615
1616 if (latShift === 0 && lngShift === 0) {
1617 return bounds;
1618 }
1619
1620 var sw = bounds.getSouthWest(),
1621 ne = bounds.getNorthEast(),
1622 newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift),
1623 newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift);
1624
1625 return new LatLngBounds(newSw, newNe);
1626 }
1627};
1628
1629/*
1630 * @namespace CRS
1631 * @crs L.CRS.Earth
1632 *
1633 * Serves as the base for CRS that are global such that they cover the earth.
1634 * Can only be used as the base for other CRS and cannot be used directly,
1635 * since it does not have a `code`, `projection` or `transformation`. `distance()` returns
1636 * meters.
1637 */
1638
1639var Earth = extend({}, CRS, {
1640 wrapLng: [-180, 180],
1641
1642 // Mean Earth Radius, as recommended for use by
1643 // the International Union of Geodesy and Geophysics,
1644 // see https://rosettacode.org/wiki/Haversine_formula
1645 R: 6371000,
1646
1647 // distance between two geographical points using spherical law of cosines approximation
1648 distance: function (latlng1, latlng2) {
1649 var rad = Math.PI / 180,
1650 lat1 = latlng1.lat * rad,
1651 lat2 = latlng2.lat * rad,
1652 sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2),
1653 sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2),
1654 a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,
1655 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1656 return this.R * c;
1657 }
1658});
1659
1660/*
1661 * @namespace Projection
1662 * @projection L.Projection.SphericalMercator
1663 *
1664 * Spherical Mercator projection — the most common projection for online maps,
1665 * used by almost all free and commercial tile providers. Assumes that Earth is
1666 * a sphere. Used by the `EPSG:3857` CRS.
1667 */
1668
1669var earthRadius = 6378137;
1670
1671var SphericalMercator = {
1672
1673 R: earthRadius,
1674 MAX_LATITUDE: 85.0511287798,
1675
1676 project: function (latlng) {
1677 var d = Math.PI / 180,
1678 max = this.MAX_LATITUDE,
1679 lat = Math.max(Math.min(max, latlng.lat), -max),
1680 sin = Math.sin(lat * d);
1681
1682 return new Point(
1683 this.R * latlng.lng * d,
1684 this.R * Math.log((1 + sin) / (1 - sin)) / 2);
1685 },
1686
1687 unproject: function (point) {
1688 var d = 180 / Math.PI;
1689
1690 return new LatLng(
1691 (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
1692 point.x * d / this.R);
1693 },
1694
1695 bounds: (function () {
1696 var d = earthRadius * Math.PI;
1697 return new Bounds([-d, -d], [d, d]);
1698 })()
1699};
1700
1701/*
1702 * @class Transformation
1703 * @aka L.Transformation
1704 *
1705 * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
1706 * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
1707 * the reverse. Used by Leaflet in its projections code.
1708 *
1709 * @example
1710 *
1711 * ```js
1712 * var transformation = L.transformation(2, 5, -1, 10),
1713 * p = L.point(1, 2),
1714 * p2 = transformation.transform(p), // L.point(7, 8)
1715 * p3 = transformation.untransform(p2); // L.point(1, 2)
1716 * ```
1717 */
1718
1719
1720// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
1721// Creates a `Transformation` object with the given coefficients.
1722function Transformation(a, b, c, d) {
1723 if (isArray(a)) {
1724 // use array properties
1725 this._a = a[0];
1726 this._b = a[1];
1727 this._c = a[2];
1728 this._d = a[3];
1729 return;
1730 }
1731 this._a = a;
1732 this._b = b;
1733 this._c = c;
1734 this._d = d;
1735}
1736
1737Transformation.prototype = {
1738 // @method transform(point: Point, scale?: Number): Point
1739 // Returns a transformed point, optionally multiplied by the given scale.
1740 // Only accepts actual `L.Point` instances, not arrays.
1741 transform: function (point, scale) { // (Point, Number) -> Point
1742 return this._transform(point.clone(), scale);
1743 },
1744
1745 // destructive transform (faster)
1746 _transform: function (point, scale) {
1747 scale = scale || 1;
1748 point.x = scale * (this._a * point.x + this._b);
1749 point.y = scale * (this._c * point.y + this._d);
1750 return point;
1751 },
1752
1753 // @method untransform(point: Point, scale?: Number): Point
1754 // Returns the reverse transformation of the given point, optionally divided
1755 // by the given scale. Only accepts actual `L.Point` instances, not arrays.
1756 untransform: function (point, scale) {
1757 scale = scale || 1;
1758 return new Point(
1759 (point.x / scale - this._b) / this._a,
1760 (point.y / scale - this._d) / this._c);
1761 }
1762};
1763
1764// factory L.transformation(a: Number, b: Number, c: Number, d: Number)
1765
1766// @factory L.transformation(a: Number, b: Number, c: Number, d: Number)
1767// Instantiates a Transformation object with the given coefficients.
1768
1769// @alternative
1770// @factory L.transformation(coefficients: Array): Transformation
1771// Expects an coefficients array of the form
1772// `[a: Number, b: Number, c: Number, d: Number]`.
1773
1774function toTransformation(a, b, c, d) {
1775 return new Transformation(a, b, c, d);
1776}
1777
1778/*
1779 * @namespace CRS
1780 * @crs L.CRS.EPSG3857
1781 *
1782 * The most common CRS for online maps, used by almost all free and commercial
1783 * tile providers. Uses Spherical Mercator projection. Set in by default in
1784 * Map's `crs` option.
1785 */
1786
1787var EPSG3857 = extend({}, Earth, {
1788 code: 'EPSG:3857',
1789 projection: SphericalMercator,
1790
1791 transformation: (function () {
1792 var scale = 0.5 / (Math.PI * SphericalMercator.R);
1793 return toTransformation(scale, 0.5, -scale, 0.5);
1794 }())
1795});
1796
1797var EPSG900913 = extend({}, EPSG3857, {
1798 code: 'EPSG:900913'
1799});
1800
1801// @namespace SVG; @section
1802// There are several static functions which can be called without instantiating L.SVG:
1803
1804// @function create(name: String): SVGElement
1805// Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
1806// corresponding to the class name passed. For example, using 'line' will return
1807// an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
1808function svgCreate(name) {
1809 return document.createElementNS('http://www.w3.org/2000/svg', name);
1810}
1811
1812// @function pointsToPath(rings: Point[], closed: Boolean): String
1813// Generates a SVG path string for multiple rings, with each ring turning
1814// into "M..L..L.." instructions
1815function pointsToPath(rings, closed) {
1816 var str = '',
1817 i, j, len, len2, points, p;
1818
1819 for (i = 0, len = rings.length; i < len; i++) {
1820 points = rings[i];
1821
1822 for (j = 0, len2 = points.length; j < len2; j++) {
1823 p = points[j];
1824 str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
1825 }
1826
1827 // closes the ring for polygons; "x" is VML syntax
1828 str += closed ? (Browser.svg ? 'z' : 'x') : '';
1829 }
1830
1831 // SVG complains about empty path strings
1832 return str || 'M0 0';
1833}
1834
1835/*
1836 * @namespace Browser
1837 * @aka L.Browser
1838 *
1839 * A namespace with static properties for browser/feature detection used by Leaflet internally.
1840 *
1841 * @example
1842 *
1843 * ```js
1844 * if (L.Browser.ielt9) {
1845 * alert('Upgrade your browser, dude!');
1846 * }
1847 * ```
1848 */
1849
1850var style = document.documentElement.style;
1851
1852// @property ie: Boolean; `true` for all Internet Explorer versions (not Edge).
1853var ie = 'ActiveXObject' in window;
1854
1855// @property ielt9: Boolean; `true` for Internet Explorer versions less than 9.
1856var ielt9 = ie && !document.addEventListener;
1857
1858// @property edge: Boolean; `true` for the Edge web browser.
1859var edge = 'msLaunchUri' in navigator && !('documentMode' in document);
1860
1861// @property webkit: Boolean;
1862// `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
1863var webkit = userAgentContains('webkit');
1864
1865// @property android: Boolean
1866// **Deprecated.** `true` for any browser running on an Android platform.
1867var android = userAgentContains('android');
1868
1869// @property android23: Boolean; **Deprecated.** `true` for browsers running on Android 2 or Android 3.
1870var android23 = userAgentContains('android 2') || userAgentContains('android 3');
1871
1872/* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */
1873var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit
1874// @property androidStock: Boolean; **Deprecated.** `true` for the Android stock browser (i.e. not Chrome)
1875var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window);
1876
1877// @property opera: Boolean; `true` for the Opera browser
1878var opera = !!window.opera;
1879
1880// @property chrome: Boolean; `true` for the Chrome browser.
1881var chrome = !edge && userAgentContains('chrome');
1882
1883// @property gecko: Boolean; `true` for gecko-based browsers like Firefox.
1884var gecko = userAgentContains('gecko') && !webkit && !opera && !ie;
1885
1886// @property safari: Boolean; `true` for the Safari browser.
1887var safari = !chrome && userAgentContains('safari');
1888
1889var phantom = userAgentContains('phantom');
1890
1891// @property opera12: Boolean
1892// `true` for the Opera browser supporting CSS transforms (version 12 or later).
1893var opera12 = 'OTransition' in style;
1894
1895// @property win: Boolean; `true` when the browser is running in a Windows platform
1896var win = navigator.platform.indexOf('Win') === 0;
1897
1898// @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms.
1899var ie3d = ie && ('transition' in style);
1900
1901// @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms.
1902var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23;
1903
1904// @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms.
1905var gecko3d = 'MozPerspective' in style;
1906
1907// @property any3d: Boolean
1908// `true` for all browsers supporting CSS transforms.
1909var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom;
1910
1911// @property mobile: Boolean; `true` for all browsers running in a mobile device.
1912var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile');
1913
1914// @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device.
1915var mobileWebkit = mobile && webkit;
1916
1917// @property mobileWebkit3d: Boolean
1918// `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
1919var mobileWebkit3d = mobile && webkit3d;
1920
1921// @property msPointer: Boolean
1922// `true` for browsers implementing the Microsoft touch events model (notably IE10).
1923var msPointer = !window.PointerEvent && window.MSPointerEvent;
1924
1925// @property pointer: Boolean
1926// `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
1927var pointer = !!(window.PointerEvent || msPointer);
1928
1929// @property touchNative: Boolean
1930// `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
1931// **This does not necessarily mean** that the browser is running in a computer with
1932// a touchscreen, it only means that the browser is capable of understanding
1933// touch events.
1934var touchNative = 'ontouchstart' in window || !!window.TouchEvent;
1935
1936// @property touch: Boolean
1937// `true` for all browsers supporting either [touch](#browser-touch) or [pointer](#browser-pointer) events.
1938// Note: pointer events will be preferred (if available), and processed for all `touch*` listeners.
1939var touch = !window.L_NO_TOUCH && (touchNative || pointer);
1940
1941// @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device.
1942var mobileOpera = mobile && opera;
1943
1944// @property mobileGecko: Boolean
1945// `true` for gecko-based browsers running in a mobile device.
1946var mobileGecko = mobile && gecko;
1947
1948// @property retina: Boolean
1949// `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%.
1950var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1;
1951
1952// @property passiveEvents: Boolean
1953// `true` for browsers that support passive events.
1954var passiveEvents = (function () {
1955 var supportsPassiveOption = false;
1956 try {
1957 var opts = Object.defineProperty({}, 'passive', {
1958 get: function () { // eslint-disable-line getter-return
1959 supportsPassiveOption = true;
1960 }
1961 });
1962 window.addEventListener('testPassiveEventSupport', falseFn, opts);
1963 window.removeEventListener('testPassiveEventSupport', falseFn, opts);
1964 } catch (e) {
1965 // Errors can safely be ignored since this is only a browser support test.
1966 }
1967 return supportsPassiveOption;
1968}());
1969
1970// @property canvas: Boolean
1971// `true` when the browser supports [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
1972var canvas$1 = (function () {
1973 return !!document.createElement('canvas').getContext;
1974}());
1975
1976// @property svg: Boolean
1977// `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG).
1978var svg$1 = !!(document.createElementNS && svgCreate('svg').createSVGRect);
1979
1980var inlineSvg = !!svg$1 && (function () {
1981 var div = document.createElement('div');
1982 div.innerHTML = '<svg/>';
1983 return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
1984})();
1985
1986// @property vml: Boolean
1987// `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language).
1988var vml = !svg$1 && (function () {
1989 try {
1990 var div = document.createElement('div');
1991 div.innerHTML = '<v:shape adj="1"/>';
1992
1993 var shape = div.firstChild;
1994 shape.style.behavior = 'url(#default#VML)';
1995
1996 return shape && (typeof shape.adj === 'object');
1997
1998 } catch (e) {
1999 return false;
2000 }
2001}());
2002
2003function userAgentContains(str) {
2004 return navigator.userAgent.toLowerCase().indexOf(str) >= 0;
2005}
2006
2007
2008var Browser = {
2009 ie: ie,
2010 ielt9: ielt9,
2011 edge: edge,
2012 webkit: webkit,
2013 android: android,
2014 android23: android23,
2015 androidStock: androidStock,
2016 opera: opera,
2017 chrome: chrome,
2018 gecko: gecko,
2019 safari: safari,
2020 phantom: phantom,
2021 opera12: opera12,
2022 win: win,
2023 ie3d: ie3d,
2024 webkit3d: webkit3d,
2025 gecko3d: gecko3d,
2026 any3d: any3d,
2027 mobile: mobile,
2028 mobileWebkit: mobileWebkit,
2029 mobileWebkit3d: mobileWebkit3d,
2030 msPointer: msPointer,
2031 pointer: pointer,
2032 touch: touch,
2033 touchNative: touchNative,
2034 mobileOpera: mobileOpera,
2035 mobileGecko: mobileGecko,
2036 retina: retina,
2037 passiveEvents: passiveEvents,
2038 canvas: canvas$1,
2039 svg: svg$1,
2040 vml: vml,
2041 inlineSvg: inlineSvg
2042};
2043
2044/*
2045 * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices.
2046 */
2047
2048var POINTER_DOWN = Browser.msPointer ? 'MSPointerDown' : 'pointerdown';
2049var POINTER_MOVE = Browser.msPointer ? 'MSPointerMove' : 'pointermove';
2050var POINTER_UP = Browser.msPointer ? 'MSPointerUp' : 'pointerup';
2051var POINTER_CANCEL = Browser.msPointer ? 'MSPointerCancel' : 'pointercancel';
2052var pEvent = {
2053 touchstart : POINTER_DOWN,
2054 touchmove : POINTER_MOVE,
2055 touchend : POINTER_UP,
2056 touchcancel : POINTER_CANCEL
2057};
2058var handle = {
2059 touchstart : _onPointerStart,
2060 touchmove : _handlePointer,
2061 touchend : _handlePointer,
2062 touchcancel : _handlePointer
2063};
2064var _pointers = {};
2065var _pointerDocListener = false;
2066
2067// Provides a touch events wrapper for (ms)pointer events.
2068// ref https://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890
2069
2070function addPointerListener(obj, type, handler) {
2071 if (type === 'touchstart') {
2072 _addPointerDocListener();
2073 }
2074 if (!handle[type]) {
2075 console.warn('wrong event specified:', type);
2076 return L.Util.falseFn;
2077 }
2078 handler = handle[type].bind(this, handler);
2079 obj.addEventListener(pEvent[type], handler, false);
2080 return handler;
2081}
2082
2083function removePointerListener(obj, type, handler) {
2084 if (!pEvent[type]) {
2085 console.warn('wrong event specified:', type);
2086 return;
2087 }
2088 obj.removeEventListener(pEvent[type], handler, false);
2089}
2090
2091function _globalPointerDown(e) {
2092 _pointers[e.pointerId] = e;
2093}
2094
2095function _globalPointerMove(e) {
2096 if (_pointers[e.pointerId]) {
2097 _pointers[e.pointerId] = e;
2098 }
2099}
2100
2101function _globalPointerUp(e) {
2102 delete _pointers[e.pointerId];
2103}
2104
2105function _addPointerDocListener() {
2106 // need to keep track of what pointers and how many are active to provide e.touches emulation
2107 if (!_pointerDocListener) {
2108 // we listen document as any drags that end by moving the touch off the screen get fired there
2109 document.addEventListener(POINTER_DOWN, _globalPointerDown, true);
2110 document.addEventListener(POINTER_MOVE, _globalPointerMove, true);
2111 document.addEventListener(POINTER_UP, _globalPointerUp, true);
2112 document.addEventListener(POINTER_CANCEL, _globalPointerUp, true);
2113
2114 _pointerDocListener = true;
2115 }
2116}
2117
2118function _handlePointer(handler, e) {
2119 if (e.pointerType === (e.MSPOINTER_TYPE_MOUSE || 'mouse')) { return; }
2120
2121 e.touches = [];
2122 for (var i in _pointers) {
2123 e.touches.push(_pointers[i]);
2124 }
2125 e.changedTouches = [e];
2126
2127 handler(e);
2128}
2129
2130function _onPointerStart(handler, e) {
2131 // IE10 specific: MsTouch needs preventDefault. See #2000
2132 if (e.MSPOINTER_TYPE_TOUCH && e.pointerType === e.MSPOINTER_TYPE_TOUCH) {
2133 preventDefault(e);
2134 }
2135 _handlePointer(handler, e);
2136}
2137
2138/*
2139 * Extends the event handling code with double tap support for mobile browsers.
2140 *
2141 * Note: currently most browsers fire native dblclick, with only a few exceptions
2142 * (see https://github.com/Leaflet/Leaflet/issues/7012#issuecomment-595087386)
2143 */
2144
2145function makeDblclick(event) {
2146 // in modern browsers `type` cannot be just overridden:
2147 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only
2148 var newEvent = {},
2149 prop, i;
2150 for (i in event) {
2151 prop = event[i];
2152 newEvent[i] = prop && prop.bind ? prop.bind(event) : prop;
2153 }
2154 event = newEvent;
2155 newEvent.type = 'dblclick';
2156 newEvent.detail = 2;
2157 newEvent.isTrusted = false;
2158 newEvent._simulated = true; // for debug purposes
2159 return newEvent;
2160}
2161
2162var delay = 200;
2163function addDoubleTapListener(obj, handler) {
2164 // Most browsers handle double tap natively
2165 obj.addEventListener('dblclick', handler);
2166
2167 // On some platforms the browser doesn't fire native dblclicks for touch events.
2168 // It seems that in all such cases `detail` property of `click` event is always `1`.
2169 // So here we rely on that fact to avoid excessive 'dblclick' simulation when not needed.
2170 var last = 0,
2171 detail;
2172 function simDblclick(e) {
2173 if (e.detail !== 1) {
2174 detail = e.detail; // keep in sync to avoid false dblclick in some cases
2175 return;
2176 }
2177
2178 if (e.pointerType === 'mouse' ||
2179 (e.sourceCapabilities && !e.sourceCapabilities.firesTouchEvents)) {
2180
2181 return;
2182 }
2183
2184 var now = Date.now();
2185 if (now - last <= delay) {
2186 detail++;
2187 if (detail === 2) {
2188 handler(makeDblclick(e));
2189 }
2190 } else {
2191 detail = 1;
2192 }
2193 last = now;
2194 }
2195
2196 obj.addEventListener('click', simDblclick);
2197
2198 return {
2199 dblclick: handler,
2200 simDblclick: simDblclick
2201 };
2202}
2203
2204function removeDoubleTapListener(obj, handlers) {
2205 obj.removeEventListener('dblclick', handlers.dblclick);
2206 obj.removeEventListener('click', handlers.simDblclick);
2207}
2208
2209/*
2210 * @namespace DomUtil
2211 *
2212 * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model)
2213 * tree, used by Leaflet internally.
2214 *
2215 * Most functions expecting or returning a `HTMLElement` also work for
2216 * SVG elements. The only difference is that classes refer to CSS classes
2217 * in HTML and SVG classes in SVG.
2218 */
2219
2220
2221// @property TRANSFORM: String
2222// Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit).
2223var TRANSFORM = testProp(
2224 ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
2225
2226// webkitTransition comes first because some browser versions that drop vendor prefix don't do
2227// the same for the transitionend event, in particular the Android 4.1 stock browser
2228
2229// @property TRANSITION: String
2230// Vendor-prefixed transition style name.
2231var TRANSITION = testProp(
2232 ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
2233
2234// @property TRANSITION_END: String
2235// Vendor-prefixed transitionend event name.
2236var TRANSITION_END =
2237 TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend';
2238
2239
2240// @function get(id: String|HTMLElement): HTMLElement
2241// Returns an element given its DOM id, or returns the element itself
2242// if it was passed directly.
2243function get(id) {
2244 return typeof id === 'string' ? document.getElementById(id) : id;
2245}
2246
2247// @function getStyle(el: HTMLElement, styleAttrib: String): String
2248// Returns the value for a certain style attribute on an element,
2249// including computed values or values set through CSS.
2250function getStyle(el, style) {
2251 var value = el.style[style] || (el.currentStyle && el.currentStyle[style]);
2252
2253 if ((!value || value === 'auto') && document.defaultView) {
2254 var css = document.defaultView.getComputedStyle(el, null);
2255 value = css ? css[style] : null;
2256 }
2257 return value === 'auto' ? null : value;
2258}
2259
2260// @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement
2261// Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element.
2262function create$1(tagName, className, container) {
2263 var el = document.createElement(tagName);
2264 el.className = className || '';
2265
2266 if (container) {
2267 container.appendChild(el);
2268 }
2269 return el;
2270}
2271
2272// @function remove(el: HTMLElement)
2273// Removes `el` from its parent element
2274function remove(el) {
2275 var parent = el.parentNode;
2276 if (parent) {
2277 parent.removeChild(el);
2278 }
2279}
2280
2281// @function empty(el: HTMLElement)
2282// Removes all of `el`'s children elements from `el`
2283function empty(el) {
2284 while (el.firstChild) {
2285 el.removeChild(el.firstChild);
2286 }
2287}
2288
2289// @function toFront(el: HTMLElement)
2290// Makes `el` the last child of its parent, so it renders in front of the other children.
2291function toFront(el) {
2292 var parent = el.parentNode;
2293 if (parent && parent.lastChild !== el) {
2294 parent.appendChild(el);
2295 }
2296}
2297
2298// @function toBack(el: HTMLElement)
2299// Makes `el` the first child of its parent, so it renders behind the other children.
2300function toBack(el) {
2301 var parent = el.parentNode;
2302 if (parent && parent.firstChild !== el) {
2303 parent.insertBefore(el, parent.firstChild);
2304 }
2305}
2306
2307// @function hasClass(el: HTMLElement, name: String): Boolean
2308// Returns `true` if the element's class attribute contains `name`.
2309function hasClass(el, name) {
2310 if (el.classList !== undefined) {
2311 return el.classList.contains(name);
2312 }
2313 var className = getClass(el);
2314 return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
2315}
2316
2317// @function addClass(el: HTMLElement, name: String)
2318// Adds `name` to the element's class attribute.
2319function addClass(el, name) {
2320 if (el.classList !== undefined) {
2321 var classes = splitWords(name);
2322 for (var i = 0, len = classes.length; i < len; i++) {
2323 el.classList.add(classes[i]);
2324 }
2325 } else if (!hasClass(el, name)) {
2326 var className = getClass(el);
2327 setClass(el, (className ? className + ' ' : '') + name);
2328 }
2329}
2330
2331// @function removeClass(el: HTMLElement, name: String)
2332// Removes `name` from the element's class attribute.
2333function removeClass(el, name) {
2334 if (el.classList !== undefined) {
2335 el.classList.remove(name);
2336 } else {
2337 setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
2338 }
2339}
2340
2341// @function setClass(el: HTMLElement, name: String)
2342// Sets the element's class.
2343function setClass(el, name) {
2344 if (el.className.baseVal === undefined) {
2345 el.className = name;
2346 } else {
2347 // in case of SVG element
2348 el.className.baseVal = name;
2349 }
2350}
2351
2352// @function getClass(el: HTMLElement): String
2353// Returns the element's class.
2354function getClass(el) {
2355 // Check if the element is an SVGElementInstance and use the correspondingElement instead
2356 // (Required for linked SVG elements in IE11.)
2357 if (el.correspondingElement) {
2358 el = el.correspondingElement;
2359 }
2360 return el.className.baseVal === undefined ? el.className : el.className.baseVal;
2361}
2362
2363// @function setOpacity(el: HTMLElement, opacity: Number)
2364// Set the opacity of an element (including old IE support).
2365// `opacity` must be a number from `0` to `1`.
2366function setOpacity(el, value) {
2367 if ('opacity' in el.style) {
2368 el.style.opacity = value;
2369 } else if ('filter' in el.style) {
2370 _setOpacityIE(el, value);
2371 }
2372}
2373
2374function _setOpacityIE(el, value) {
2375 var filter = false,
2376 filterName = 'DXImageTransform.Microsoft.Alpha';
2377
2378 // filters collection throws an error if we try to retrieve a filter that doesn't exist
2379 try {
2380 filter = el.filters.item(filterName);
2381 } catch (e) {
2382 // don't set opacity to 1 if we haven't already set an opacity,
2383 // it isn't needed and breaks transparent pngs.
2384 if (value === 1) { return; }
2385 }
2386
2387 value = Math.round(value * 100);
2388
2389 if (filter) {
2390 filter.Enabled = (value !== 100);
2391 filter.Opacity = value;
2392 } else {
2393 el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
2394 }
2395}
2396
2397// @function testProp(props: String[]): String|false
2398// Goes through the array of style names and returns the first name
2399// that is a valid style name for an element. If no such name is found,
2400// it returns false. Useful for vendor-prefixed styles like `transform`.
2401function testProp(props) {
2402 var style = document.documentElement.style;
2403
2404 for (var i = 0; i < props.length; i++) {
2405 if (props[i] in style) {
2406 return props[i];
2407 }
2408 }
2409 return false;
2410}
2411
2412// @function setTransform(el: HTMLElement, offset: Point, scale?: Number)
2413// Resets the 3D CSS transform of `el` so it is translated by `offset` pixels
2414// and optionally scaled by `scale`. Does not have an effect if the
2415// browser doesn't support 3D CSS transforms.
2416function setTransform(el, offset, scale) {
2417 var pos = offset || new Point(0, 0);
2418
2419 el.style[TRANSFORM] =
2420 (Browser.ie3d ?
2421 'translate(' + pos.x + 'px,' + pos.y + 'px)' :
2422 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') +
2423 (scale ? ' scale(' + scale + ')' : '');
2424}
2425
2426// @function setPosition(el: HTMLElement, position: Point)
2427// Sets the position of `el` to coordinates specified by `position`,
2428// using CSS translate or top/left positioning depending on the browser
2429// (used by Leaflet internally to position its layers).
2430function setPosition(el, point) {
2431
2432 /*eslint-disable */
2433 el._leaflet_pos = point;
2434 /* eslint-enable */
2435
2436 if (Browser.any3d) {
2437 setTransform(el, point);
2438 } else {
2439 el.style.left = point.x + 'px';
2440 el.style.top = point.y + 'px';
2441 }
2442}
2443
2444// @function getPosition(el: HTMLElement): Point
2445// Returns the coordinates of an element previously positioned with setPosition.
2446function getPosition(el) {
2447 // this method is only used for elements previously positioned using setPosition,
2448 // so it's safe to cache the position for performance
2449
2450 return el._leaflet_pos || new Point(0, 0);
2451}
2452
2453// @function disableTextSelection()
2454// Prevents the user from generating `selectstart` DOM events, usually generated
2455// when the user drags the mouse through a page with text. Used internally
2456// by Leaflet to override the behaviour of any click-and-drag interaction on
2457// the map. Affects drag interactions on the whole document.
2458
2459// @function enableTextSelection()
2460// Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection).
2461var disableTextSelection;
2462var enableTextSelection;
2463var _userSelect;
2464if ('onselectstart' in document) {
2465 disableTextSelection = function () {
2466 on(window, 'selectstart', preventDefault);
2467 };
2468 enableTextSelection = function () {
2469 off(window, 'selectstart', preventDefault);
2470 };
2471} else {
2472 var userSelectProperty = testProp(
2473 ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);
2474
2475 disableTextSelection = function () {
2476 if (userSelectProperty) {
2477 var style = document.documentElement.style;
2478 _userSelect = style[userSelectProperty];
2479 style[userSelectProperty] = 'none';
2480 }
2481 };
2482 enableTextSelection = function () {
2483 if (userSelectProperty) {
2484 document.documentElement.style[userSelectProperty] = _userSelect;
2485 _userSelect = undefined;
2486 }
2487 };
2488}
2489
2490// @function disableImageDrag()
2491// As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but
2492// for `dragstart` DOM events, usually generated when the user drags an image.
2493function disableImageDrag() {
2494 on(window, 'dragstart', preventDefault);
2495}
2496
2497// @function enableImageDrag()
2498// Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection).
2499function enableImageDrag() {
2500 off(window, 'dragstart', preventDefault);
2501}
2502
2503var _outlineElement, _outlineStyle;
2504// @function preventOutline(el: HTMLElement)
2505// Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline)
2506// of the element `el` invisible. Used internally by Leaflet to prevent
2507// focusable elements from displaying an outline when the user performs a
2508// drag interaction on them.
2509function preventOutline(element) {
2510 while (element.tabIndex === -1) {
2511 element = element.parentNode;
2512 }
2513 if (!element.style) { return; }
2514 restoreOutline();
2515 _outlineElement = element;
2516 _outlineStyle = element.style.outline;
2517 element.style.outline = 'none';
2518 on(window, 'keydown', restoreOutline);
2519}
2520
2521// @function restoreOutline()
2522// Cancels the effects of a previous [`L.DomUtil.preventOutline`]().
2523function restoreOutline() {
2524 if (!_outlineElement) { return; }
2525 _outlineElement.style.outline = _outlineStyle;
2526 _outlineElement = undefined;
2527 _outlineStyle = undefined;
2528 off(window, 'keydown', restoreOutline);
2529}
2530
2531// @function getSizedParentNode(el: HTMLElement): HTMLElement
2532// Finds the closest parent node which size (width and height) is not null.
2533function getSizedParentNode(element) {
2534 do {
2535 element = element.parentNode;
2536 } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body);
2537 return element;
2538}
2539
2540// @function getScale(el: HTMLElement): Object
2541// Computes the CSS scale currently applied on the element.
2542// Returns an object with `x` and `y` members as horizontal and vertical scales respectively,
2543// and `boundingClientRect` as the result of [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
2544function getScale(element) {
2545 var rect = element.getBoundingClientRect(); // Read-only in old browsers.
2546
2547 return {
2548 x: rect.width / element.offsetWidth || 1,
2549 y: rect.height / element.offsetHeight || 1,
2550 boundingClientRect: rect
2551 };
2552}
2553
2554var DomUtil = {
2555 __proto__: null,
2556 TRANSFORM: TRANSFORM,
2557 TRANSITION: TRANSITION,
2558 TRANSITION_END: TRANSITION_END,
2559 get: get,
2560 getStyle: getStyle,
2561 create: create$1,
2562 remove: remove,
2563 empty: empty,
2564 toFront: toFront,
2565 toBack: toBack,
2566 hasClass: hasClass,
2567 addClass: addClass,
2568 removeClass: removeClass,
2569 setClass: setClass,
2570 getClass: getClass,
2571 setOpacity: setOpacity,
2572 testProp: testProp,
2573 setTransform: setTransform,
2574 setPosition: setPosition,
2575 getPosition: getPosition,
2576 get disableTextSelection () { return disableTextSelection; },
2577 get enableTextSelection () { return enableTextSelection; },
2578 disableImageDrag: disableImageDrag,
2579 enableImageDrag: enableImageDrag,
2580 preventOutline: preventOutline,
2581 restoreOutline: restoreOutline,
2582 getSizedParentNode: getSizedParentNode,
2583 getScale: getScale
2584};
2585
2586/*
2587 * @namespace DomEvent
2588 * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
2589 */
2590
2591// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
2592
2593// @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
2594// Adds a listener function (`fn`) to a particular DOM event type of the
2595// element `el`. You can optionally specify the context of the listener
2596// (object the `this` keyword will point to). You can also pass several
2597// space-separated types (e.g. `'click dblclick'`).
2598
2599// @alternative
2600// @function on(el: HTMLElement, eventMap: Object, context?: Object): this
2601// Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
2602function on(obj, types, fn, context) {
2603
2604 if (types && typeof types === 'object') {
2605 for (var type in types) {
2606 addOne(obj, type, types[type], fn);
2607 }
2608 } else {
2609 types = splitWords(types);
2610
2611 for (var i = 0, len = types.length; i < len; i++) {
2612 addOne(obj, types[i], fn, context);
2613 }
2614 }
2615
2616 return this;
2617}
2618
2619var eventsKey = '_leaflet_events';
2620
2621// @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
2622// Removes a previously added listener function.
2623// Note that if you passed a custom context to on, you must pass the same
2624// context to `off` in order to remove the listener.
2625
2626// @alternative
2627// @function off(el: HTMLElement, eventMap: Object, context?: Object): this
2628// Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
2629
2630// @alternative
2631// @function off(el: HTMLElement, types: String): this
2632// Removes all previously added listeners of given types.
2633
2634// @alternative
2635// @function off(el: HTMLElement): this
2636// Removes all previously added listeners from given HTMLElement
2637function off(obj, types, fn, context) {
2638
2639 if (arguments.length === 1) {
2640 batchRemove(obj);
2641 delete obj[eventsKey];
2642
2643 } else if (types && typeof types === 'object') {
2644 for (var type in types) {
2645 removeOne(obj, type, types[type], fn);
2646 }
2647
2648 } else {
2649 types = splitWords(types);
2650
2651 if (arguments.length === 2) {
2652 batchRemove(obj, function (type) {
2653 return indexOf(types, type) !== -1;
2654 });
2655 } else {
2656 for (var i = 0, len = types.length; i < len; i++) {
2657 removeOne(obj, types[i], fn, context);
2658 }
2659 }
2660 }
2661
2662 return this;
2663}
2664
2665function batchRemove(obj, filterFn) {
2666 for (var id in obj[eventsKey]) {
2667 var type = id.split(/\d/)[0];
2668 if (!filterFn || filterFn(type)) {
2669 removeOne(obj, type, null, null, id);
2670 }
2671 }
2672}
2673
2674var mouseSubst = {
2675 mouseenter: 'mouseover',
2676 mouseleave: 'mouseout',
2677 wheel: !('onwheel' in window) && 'mousewheel'
2678};
2679
2680function addOne(obj, type, fn, context) {
2681 var id = type + stamp(fn) + (context ? '_' + stamp(context) : '');
2682
2683 if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
2684
2685 var handler = function (e) {
2686 return fn.call(context || obj, e || window.event);
2687 };
2688
2689 var originalHandler = handler;
2690
2691 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) {
2692 // Needs DomEvent.Pointer.js
2693 handler = addPointerListener(obj, type, handler);
2694
2695 } else if (Browser.touch && (type === 'dblclick')) {
2696 handler = addDoubleTapListener(obj, handler);
2697
2698 } else if ('addEventListener' in obj) {
2699
2700 if (type === 'touchstart' || type === 'touchmove' || type === 'wheel' || type === 'mousewheel') {
2701 obj.addEventListener(mouseSubst[type] || type, handler, Browser.passiveEvents ? {passive: false} : false);
2702
2703 } else if (type === 'mouseenter' || type === 'mouseleave') {
2704 handler = function (e) {
2705 e = e || window.event;
2706 if (isExternalTarget(obj, e)) {
2707 originalHandler(e);
2708 }
2709 };
2710 obj.addEventListener(mouseSubst[type], handler, false);
2711
2712 } else {
2713 obj.addEventListener(type, originalHandler, false);
2714 }
2715
2716 } else {
2717 obj.attachEvent('on' + type, handler);
2718 }
2719
2720 obj[eventsKey] = obj[eventsKey] || {};
2721 obj[eventsKey][id] = handler;
2722}
2723
2724function removeOne(obj, type, fn, context, id) {
2725 id = id || type + stamp(fn) + (context ? '_' + stamp(context) : '');
2726 var handler = obj[eventsKey] && obj[eventsKey][id];
2727
2728 if (!handler) { return this; }
2729
2730 if (!Browser.touchNative && Browser.pointer && type.indexOf('touch') === 0) {
2731 removePointerListener(obj, type, handler);
2732
2733 } else if (Browser.touch && (type === 'dblclick')) {
2734 removeDoubleTapListener(obj, handler);
2735
2736 } else if ('removeEventListener' in obj) {
2737
2738 obj.removeEventListener(mouseSubst[type] || type, handler, false);
2739
2740 } else {
2741 obj.detachEvent('on' + type, handler);
2742 }
2743
2744 obj[eventsKey][id] = null;
2745}
2746
2747// @function stopPropagation(ev: DOMEvent): this
2748// Stop the given event from propagation to parent elements. Used inside the listener functions:
2749// ```js
2750// L.DomEvent.on(div, 'click', function (ev) {
2751// L.DomEvent.stopPropagation(ev);
2752// });
2753// ```
2754function stopPropagation(e) {
2755
2756 if (e.stopPropagation) {
2757 e.stopPropagation();
2758 } else if (e.originalEvent) { // In case of Leaflet event.
2759 e.originalEvent._stopped = true;
2760 } else {
2761 e.cancelBubble = true;
2762 }
2763
2764 return this;
2765}
2766
2767// @function disableScrollPropagation(el: HTMLElement): this
2768// Adds `stopPropagation` to the element's `'wheel'` events (plus browser variants).
2769function disableScrollPropagation(el) {
2770 addOne(el, 'wheel', stopPropagation);
2771 return this;
2772}
2773
2774// @function disableClickPropagation(el: HTMLElement): this
2775// Adds `stopPropagation` to the element's `'click'`, `'dblclick'`, `'contextmenu'`,
2776// `'mousedown'` and `'touchstart'` events (plus browser variants).
2777function disableClickPropagation(el) {
2778 on(el, 'mousedown touchstart dblclick contextmenu', stopPropagation);
2779 el['_leaflet_disable_click'] = true;
2780 return this;
2781}
2782
2783// @function preventDefault(ev: DOMEvent): this
2784// Prevents the default action of the DOM Event `ev` from happening (such as
2785// following a link in the href of the a element, or doing a POST request
2786// with page reload when a `<form>` is submitted).
2787// Use it inside listener functions.
2788function preventDefault(e) {
2789 if (e.preventDefault) {
2790 e.preventDefault();
2791 } else {
2792 e.returnValue = false;
2793 }
2794 return this;
2795}
2796
2797// @function stop(ev: DOMEvent): this
2798// Does `stopPropagation` and `preventDefault` at the same time.
2799function stop(e) {
2800 preventDefault(e);
2801 stopPropagation(e);
2802 return this;
2803}
2804
2805// @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
2806// Gets normalized mouse position from a DOM event relative to the
2807// `container` (border excluded) or to the whole page if not specified.
2808function getMousePosition(e, container) {
2809 if (!container) {
2810 return new Point(e.clientX, e.clientY);
2811 }
2812
2813 var scale = getScale(container),
2814 offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
2815
2816 return new Point(
2817 // offset.left/top values are in page scale (like clientX/Y),
2818 // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
2819 (e.clientX - offset.left) / scale.x - container.clientLeft,
2820 (e.clientY - offset.top) / scale.y - container.clientTop
2821 );
2822}
2823
2824// Chrome on Win scrolls double the pixels as in other platforms (see #4538),
2825// and Firefox scrolls device pixels, not CSS pixels
2826var wheelPxFactor =
2827 (Browser.win && Browser.chrome) ? 2 * window.devicePixelRatio :
2828 Browser.gecko ? window.devicePixelRatio : 1;
2829
2830// @function getWheelDelta(ev: DOMEvent): Number
2831// Gets normalized wheel delta from a wheel DOM event, in vertical
2832// pixels scrolled (negative if scrolling down).
2833// Events from pointing devices without precise scrolling are mapped to
2834// a best guess of 60 pixels.
2835function getWheelDelta(e) {
2836 return (Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
2837 (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels
2838 (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
2839 (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
2840 (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events
2841 e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
2842 (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
2843 e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
2844 0;
2845}
2846
2847// check if element really left/entered the event target (for mouseenter/mouseleave)
2848function isExternalTarget(el, e) {
2849
2850 var related = e.relatedTarget;
2851
2852 if (!related) { return true; }
2853
2854 try {
2855 while (related && (related !== el)) {
2856 related = related.parentNode;
2857 }
2858 } catch (err) {
2859 return false;
2860 }
2861 return (related !== el);
2862}
2863
2864var DomEvent = {
2865 __proto__: null,
2866 on: on,
2867 off: off,
2868 stopPropagation: stopPropagation,
2869 disableScrollPropagation: disableScrollPropagation,
2870 disableClickPropagation: disableClickPropagation,
2871 preventDefault: preventDefault,
2872 stop: stop,
2873 getMousePosition: getMousePosition,
2874 getWheelDelta: getWheelDelta,
2875 isExternalTarget: isExternalTarget,
2876 addListener: on,
2877 removeListener: off
2878};
2879
2880/*
2881 * @class PosAnimation
2882 * @aka L.PosAnimation
2883 * @inherits Evented
2884 * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9.
2885 *
2886 * @example
2887 * ```js
2888 * var fx = new L.PosAnimation();
2889 * fx.run(el, [300, 500], 0.5);
2890 * ```
2891 *
2892 * @constructor L.PosAnimation()
2893 * Creates a `PosAnimation` object.
2894 *
2895 */
2896
2897var PosAnimation = Evented.extend({
2898
2899 // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number)
2900 // Run an animation of a given element to a new position, optionally setting
2901 // duration in seconds (`0.25` by default) and easing linearity factor (3rd
2902 // argument of the [cubic bezier curve](https://cubic-bezier.com/#0,0,.5,1),
2903 // `0.5` by default).
2904 run: function (el, newPos, duration, easeLinearity) {
2905 this.stop();
2906
2907 this._el = el;
2908 this._inProgress = true;
2909 this._duration = duration || 0.25;
2910 this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2);
2911
2912 this._startPos = getPosition(el);
2913 this._offset = newPos.subtract(this._startPos);
2914 this._startTime = +new Date();
2915
2916 // @event start: Event
2917 // Fired when the animation starts
2918 this.fire('start');
2919
2920 this._animate();
2921 },
2922
2923 // @method stop()
2924 // Stops the animation (if currently running).
2925 stop: function () {
2926 if (!this._inProgress) { return; }
2927
2928 this._step(true);
2929 this._complete();
2930 },
2931
2932 _animate: function () {
2933 // animation loop
2934 this._animId = requestAnimFrame(this._animate, this);
2935 this._step();
2936 },
2937
2938 _step: function (round) {
2939 var elapsed = (+new Date()) - this._startTime,
2940 duration = this._duration * 1000;
2941
2942 if (elapsed < duration) {
2943 this._runFrame(this._easeOut(elapsed / duration), round);
2944 } else {
2945 this._runFrame(1);
2946 this._complete();
2947 }
2948 },
2949
2950 _runFrame: function (progress, round) {
2951 var pos = this._startPos.add(this._offset.multiplyBy(progress));
2952 if (round) {
2953 pos._round();
2954 }
2955 setPosition(this._el, pos);
2956
2957 // @event step: Event
2958 // Fired continuously during the animation.
2959 this.fire('step');
2960 },
2961
2962 _complete: function () {
2963 cancelAnimFrame(this._animId);
2964
2965 this._inProgress = false;
2966 // @event end: Event
2967 // Fired when the animation ends.
2968 this.fire('end');
2969 },
2970
2971 _easeOut: function (t) {
2972 return 1 - Math.pow(1 - t, this._easeOutPower);
2973 }
2974});
2975
2976/*
2977 * @class Map
2978 * @aka L.Map
2979 * @inherits Evented
2980 *
2981 * The central class of the API — it is used to create a map on a page and manipulate it.
2982 *
2983 * @example
2984 *
2985 * ```js
2986 * // initialize the map on the "map" div with a given center and zoom
2987 * var map = L.map('map', {
2988 * center: [51.505, -0.09],
2989 * zoom: 13
2990 * });
2991 * ```
2992 *
2993 */
2994
2995var Map = Evented.extend({
2996
2997 options: {
2998 // @section Map State Options
2999 // @option crs: CRS = L.CRS.EPSG3857
3000 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not
3001 // sure what it means.
3002 crs: EPSG3857,
3003
3004 // @option center: LatLng = undefined
3005 // Initial geographic center of the map
3006 center: undefined,
3007
3008 // @option zoom: Number = undefined
3009 // Initial map zoom level
3010 zoom: undefined,
3011
3012 // @option minZoom: Number = *
3013 // Minimum zoom level of the map.
3014 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
3015 // the lowest of their `minZoom` options will be used instead.
3016 minZoom: undefined,
3017
3018 // @option maxZoom: Number = *
3019 // Maximum zoom level of the map.
3020 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
3021 // the highest of their `maxZoom` options will be used instead.
3022 maxZoom: undefined,
3023
3024 // @option layers: Layer[] = []
3025 // Array of layers that will be added to the map initially
3026 layers: [],
3027
3028 // @option maxBounds: LatLngBounds = null
3029 // When this option is set, the map restricts the view to the given
3030 // geographical bounds, bouncing the user back if the user tries to pan
3031 // outside the view. To set the restriction dynamically, use
3032 // [`setMaxBounds`](#map-setmaxbounds) method.
3033 maxBounds: undefined,
3034
3035 // @option renderer: Renderer = *
3036 // The default method for drawing vector layers on the map. `L.SVG`
3037 // or `L.Canvas` by default depending on browser support.
3038 renderer: undefined,
3039
3040
3041 // @section Animation Options
3042 // @option zoomAnimation: Boolean = true
3043 // Whether the map zoom animation is enabled. By default it's enabled
3044 // in all browsers that support CSS3 Transitions except Android.
3045 zoomAnimation: true,
3046
3047 // @option zoomAnimationThreshold: Number = 4
3048 // Won't animate zoom if the zoom difference exceeds this value.
3049 zoomAnimationThreshold: 4,
3050
3051 // @option fadeAnimation: Boolean = true
3052 // Whether the tile fade animation is enabled. By default it's enabled
3053 // in all browsers that support CSS3 Transitions except Android.
3054 fadeAnimation: true,
3055
3056 // @option markerZoomAnimation: Boolean = true
3057 // Whether markers animate their zoom with the zoom animation, if disabled
3058 // they will disappear for the length of the animation. By default it's
3059 // enabled in all browsers that support CSS3 Transitions except Android.
3060 markerZoomAnimation: true,
3061
3062 // @option transform3DLimit: Number = 2^23
3063 // Defines the maximum size of a CSS translation transform. The default
3064 // value should not be changed unless a web browser positions layers in
3065 // the wrong place after doing a large `panBy`.
3066 transform3DLimit: 8388608, // Precision limit of a 32-bit float
3067
3068 // @section Interaction Options
3069 // @option zoomSnap: Number = 1
3070 // Forces the map's zoom level to always be a multiple of this, particularly
3071 // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom.
3072 // By default, the zoom level snaps to the nearest integer; lower values
3073 // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0`
3074 // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom.
3075 zoomSnap: 1,
3076
3077 // @option zoomDelta: Number = 1
3078 // Controls how much the map's zoom level will change after a
3079 // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+`
3080 // or `-` on the keyboard, or using the [zoom controls](#control-zoom).
3081 // Values smaller than `1` (e.g. `0.5`) allow for greater granularity.
3082 zoomDelta: 1,
3083
3084 // @option trackResize: Boolean = true
3085 // Whether the map automatically handles browser window resize to update itself.
3086 trackResize: true
3087 },
3088
3089 initialize: function (id, options) { // (HTMLElement or String, Object)
3090 options = setOptions(this, options);
3091
3092 // Make sure to assign internal flags at the beginning,
3093 // to avoid inconsistent state in some edge cases.
3094 this._handlers = [];
3095 this._layers = {};
3096 this._zoomBoundLayers = {};
3097 this._sizeChanged = true;
3098
3099 this._initContainer(id);
3100 this._initLayout();
3101
3102 // hack for https://github.com/Leaflet/Leaflet/issues/1980
3103 this._onResize = bind(this._onResize, this);
3104
3105 this._initEvents();
3106
3107 if (options.maxBounds) {
3108 this.setMaxBounds(options.maxBounds);
3109 }
3110
3111 if (options.zoom !== undefined) {
3112 this._zoom = this._limitZoom(options.zoom);
3113 }
3114
3115 if (options.center && options.zoom !== undefined) {
3116 this.setView(toLatLng(options.center), options.zoom, {reset: true});
3117 }
3118
3119 this.callInitHooks();
3120
3121 // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
3122 this._zoomAnimated = TRANSITION && Browser.any3d && !Browser.mobileOpera &&
3123 this.options.zoomAnimation;
3124
3125 // zoom transitions run with the same duration for all layers, so if one of transitionend events
3126 // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
3127 if (this._zoomAnimated) {
3128 this._createAnimProxy();
3129 on(this._proxy, TRANSITION_END, this._catchTransitionEnd, this);
3130 }
3131
3132 this._addLayers(this.options.layers);
3133 },
3134
3135
3136 // @section Methods for modifying map state
3137
3138 // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
3139 // Sets the view of the map (geographical center and zoom) with the given
3140 // animation options.
3141 setView: function (center, zoom, options) {
3142
3143 zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
3144 center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds);
3145 options = options || {};
3146
3147 this._stop();
3148
3149 if (this._loaded && !options.reset && options !== true) {
3150
3151 if (options.animate !== undefined) {
3152 options.zoom = extend({animate: options.animate}, options.zoom);
3153 options.pan = extend({animate: options.animate, duration: options.duration}, options.pan);
3154 }
3155
3156 // try animating pan or zoom
3157 var moved = (this._zoom !== zoom) ?
3158 this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
3159 this._tryAnimatedPan(center, options.pan);
3160
3161 if (moved) {
3162 // prevent resize handler call, the view will refresh after animation anyway
3163 clearTimeout(this._sizeTimer);
3164 return this;
3165 }
3166 }
3167
3168 // animation didn't start, just reset the map view
3169 this._resetView(center, zoom);
3170
3171 return this;
3172 },
3173
3174 // @method setZoom(zoom: Number, options?: Zoom/pan options): this
3175 // Sets the zoom of the map.
3176 setZoom: function (zoom, options) {
3177 if (!this._loaded) {
3178 this._zoom = zoom;
3179 return this;
3180 }
3181 return this.setView(this.getCenter(), zoom, {zoom: options});
3182 },
3183
3184 // @method zoomIn(delta?: Number, options?: Zoom options): this
3185 // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
3186 zoomIn: function (delta, options) {
3187 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
3188 return this.setZoom(this._zoom + delta, options);
3189 },
3190
3191 // @method zoomOut(delta?: Number, options?: Zoom options): this
3192 // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
3193 zoomOut: function (delta, options) {
3194 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
3195 return this.setZoom(this._zoom - delta, options);
3196 },
3197
3198 // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this
3199 // Zooms the map while keeping a specified geographical point on the map
3200 // stationary (e.g. used internally for scroll zoom and double-click zoom).
3201 // @alternative
3202 // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this
3203 // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary.
3204 setZoomAround: function (latlng, zoom, options) {
3205 var scale = this.getZoomScale(zoom),
3206 viewHalf = this.getSize().divideBy(2),
3207 containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng),
3208
3209 centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
3210 newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
3211
3212 return this.setView(newCenter, zoom, {zoom: options});
3213 },
3214
3215 _getBoundsCenterZoom: function (bounds, options) {
3216
3217 options = options || {};
3218 bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds);
3219
3220 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
3221 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
3222
3223 zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
3224
3225 zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom;
3226
3227 if (zoom === Infinity) {
3228 return {
3229 center: bounds.getCenter(),
3230 zoom: zoom
3231 };
3232 }
3233
3234 var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
3235
3236 swPoint = this.project(bounds.getSouthWest(), zoom),
3237 nePoint = this.project(bounds.getNorthEast(), zoom),
3238 center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
3239
3240 return {
3241 center: center,
3242 zoom: zoom
3243 };
3244 },
3245
3246 // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this
3247 // Sets a map view that contains the given geographical bounds with the
3248 // maximum zoom level possible.
3249 fitBounds: function (bounds, options) {
3250
3251 bounds = toLatLngBounds(bounds);
3252
3253 if (!bounds.isValid()) {
3254 throw new Error('Bounds are not valid.');
3255 }
3256
3257 var target = this._getBoundsCenterZoom(bounds, options);
3258 return this.setView(target.center, target.zoom, options);
3259 },
3260
3261 // @method fitWorld(options?: fitBounds options): this
3262 // Sets a map view that mostly contains the whole world with the maximum
3263 // zoom level possible.
3264 fitWorld: function (options) {
3265 return this.fitBounds([[-90, -180], [90, 180]], options);
3266 },
3267
3268 // @method panTo(latlng: LatLng, options?: Pan options): this
3269 // Pans the map to a given center.
3270 panTo: function (center, options) { // (LatLng)
3271 return this.setView(center, this._zoom, {pan: options});
3272 },
3273
3274 // @method panBy(offset: Point, options?: Pan options): this
3275 // Pans the map by a given number of pixels (animated).
3276 panBy: function (offset, options) {
3277 offset = toPoint(offset).round();
3278 options = options || {};
3279
3280 if (!offset.x && !offset.y) {
3281 return this.fire('moveend');
3282 }
3283 // If we pan too far, Chrome gets issues with tiles
3284 // and makes them disappear or appear in the wrong place (slightly offset) #2602
3285 if (options.animate !== true && !this.getSize().contains(offset)) {
3286 this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
3287 return this;
3288 }
3289
3290 if (!this._panAnim) {
3291 this._panAnim = new PosAnimation();
3292
3293 this._panAnim.on({
3294 'step': this._onPanTransitionStep,
3295 'end': this._onPanTransitionEnd
3296 }, this);
3297 }
3298
3299 // don't fire movestart if animating inertia
3300 if (!options.noMoveStart) {
3301 this.fire('movestart');
3302 }
3303
3304 // animate pan unless animate: false specified
3305 if (options.animate !== false) {
3306 addClass(this._mapPane, 'leaflet-pan-anim');
3307
3308 var newPos = this._getMapPanePos().subtract(offset).round();
3309 this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
3310 } else {
3311 this._rawPanBy(offset);
3312 this.fire('move').fire('moveend');
3313 }
3314
3315 return this;
3316 },
3317
3318 // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
3319 // Sets the view of the map (geographical center and zoom) performing a smooth
3320 // pan-zoom animation.
3321 flyTo: function (targetCenter, targetZoom, options) {
3322
3323 options = options || {};
3324 if (options.animate === false || !Browser.any3d) {
3325 return this.setView(targetCenter, targetZoom, options);
3326 }
3327
3328 this._stop();
3329
3330 var from = this.project(this.getCenter()),
3331 to = this.project(targetCenter),
3332 size = this.getSize(),
3333 startZoom = this._zoom;
3334
3335 targetCenter = toLatLng(targetCenter);
3336 targetZoom = targetZoom === undefined ? startZoom : targetZoom;
3337
3338 var w0 = Math.max(size.x, size.y),
3339 w1 = w0 * this.getZoomScale(startZoom, targetZoom),
3340 u1 = (to.distanceTo(from)) || 1,
3341 rho = 1.42,
3342 rho2 = rho * rho;
3343
3344 function r(i) {
3345 var s1 = i ? -1 : 1,
3346 s2 = i ? w1 : w0,
3347 t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
3348 b1 = 2 * s2 * rho2 * u1,
3349 b = t1 / b1,
3350 sq = Math.sqrt(b * b + 1) - b;
3351
3352 // workaround for floating point precision bug when sq = 0, log = -Infinite,
3353 // thus triggering an infinite loop in flyTo
3354 var log = sq < 0.000000001 ? -18 : Math.log(sq);
3355
3356 return log;
3357 }
3358
3359 function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
3360 function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
3361 function tanh(n) { return sinh(n) / cosh(n); }
3362
3363 var r0 = r(0);
3364
3365 function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
3366 function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }
3367
3368 function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }
3369
3370 var start = Date.now(),
3371 S = (r(1) - r0) / rho,
3372 duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
3373
3374 function frame() {
3375 var t = (Date.now() - start) / duration,
3376 s = easeOut(t) * S;
3377
3378 if (t <= 1) {
3379 this._flyToFrame = requestAnimFrame(frame, this);
3380
3381 this._move(
3382 this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
3383 this.getScaleZoom(w0 / w(s), startZoom),
3384 {flyTo: true});
3385
3386 } else {
3387 this
3388 ._move(targetCenter, targetZoom)
3389 ._moveEnd(true);
3390 }
3391 }
3392
3393 this._moveStart(true, options.noMoveStart);
3394
3395 frame.call(this);
3396 return this;
3397 },
3398
3399 // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
3400 // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
3401 // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
3402 flyToBounds: function (bounds, options) {
3403 var target = this._getBoundsCenterZoom(bounds, options);
3404 return this.flyTo(target.center, target.zoom, options);
3405 },
3406
3407 // @method setMaxBounds(bounds: LatLngBounds): this
3408 // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option).
3409 setMaxBounds: function (bounds) {
3410 bounds = toLatLngBounds(bounds);
3411
3412 if (!bounds.isValid()) {
3413 this.options.maxBounds = null;
3414 return this.off('moveend', this._panInsideMaxBounds);
3415 } else if (this.options.maxBounds) {
3416 this.off('moveend', this._panInsideMaxBounds);
3417 }
3418
3419 this.options.maxBounds = bounds;
3420
3421 if (this._loaded) {
3422 this._panInsideMaxBounds();
3423 }
3424
3425 return this.on('moveend', this._panInsideMaxBounds);
3426 },
3427
3428 // @method setMinZoom(zoom: Number): this
3429 // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option).
3430 setMinZoom: function (zoom) {
3431 var oldZoom = this.options.minZoom;
3432 this.options.minZoom = zoom;
3433
3434 if (this._loaded && oldZoom !== zoom) {
3435 this.fire('zoomlevelschange');
3436
3437 if (this.getZoom() < this.options.minZoom) {
3438 return this.setZoom(zoom);
3439 }
3440 }
3441
3442 return this;
3443 },
3444
3445 // @method setMaxZoom(zoom: Number): this
3446 // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option).
3447 setMaxZoom: function (zoom) {
3448 var oldZoom = this.options.maxZoom;
3449 this.options.maxZoom = zoom;
3450
3451 if (this._loaded && oldZoom !== zoom) {
3452 this.fire('zoomlevelschange');
3453
3454 if (this.getZoom() > this.options.maxZoom) {
3455 return this.setZoom(zoom);
3456 }
3457 }
3458
3459 return this;
3460 },
3461
3462 // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this
3463 // 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.
3464 panInsideBounds: function (bounds, options) {
3465 this._enforcingBounds = true;
3466 var center = this.getCenter(),
3467 newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds));
3468
3469 if (!center.equals(newCenter)) {
3470 this.panTo(newCenter, options);
3471 }
3472
3473 this._enforcingBounds = false;
3474 return this;
3475 },
3476
3477 // @method panInside(latlng: LatLng, options?: padding options): this
3478 // Pans the map the minimum amount to make the `latlng` visible. Use
3479 // padding options to fit the display to more restricted bounds.
3480 // If `latlng` is already within the (optionally padded) display bounds,
3481 // the map will not be panned.
3482 panInside: function (latlng, options) {
3483 options = options || {};
3484
3485 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
3486 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
3487 pixelCenter = this.project(this.getCenter()),
3488 pixelPoint = this.project(latlng),
3489 pixelBounds = this.getPixelBounds(),
3490 paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
3491 paddedSize = paddedBounds.getSize();
3492
3493 if (!paddedBounds.contains(pixelPoint)) {
3494 this._enforcingBounds = true;
3495 var centerOffset = pixelPoint.subtract(paddedBounds.getCenter());
3496 var offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize);
3497 pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x;
3498 pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y;
3499 this.panTo(this.unproject(pixelCenter), options);
3500 this._enforcingBounds = false;
3501 }
3502 return this;
3503 },
3504
3505 // @method invalidateSize(options: Zoom/pan options): this
3506 // Checks if the map container size changed and updates the map if so —
3507 // call it after you've changed the map size dynamically, also animating
3508 // pan by default. If `options.pan` is `false`, panning will not occur.
3509 // If `options.debounceMoveend` is `true`, it will delay `moveend` event so
3510 // that it doesn't happen often even if the method is called many
3511 // times in a row.
3512
3513 // @alternative
3514 // @method invalidateSize(animate: Boolean): this
3515 // Checks if the map container size changed and updates the map if so —
3516 // call it after you've changed the map size dynamically, also animating
3517 // pan by default.
3518 invalidateSize: function (options) {
3519 if (!this._loaded) { return this; }
3520
3521 options = extend({
3522 animate: false,
3523 pan: true
3524 }, options === true ? {animate: true} : options);
3525
3526 var oldSize = this.getSize();
3527 this._sizeChanged = true;
3528 this._lastCenter = null;
3529
3530 var newSize = this.getSize(),
3531 oldCenter = oldSize.divideBy(2).round(),
3532 newCenter = newSize.divideBy(2).round(),
3533 offset = oldCenter.subtract(newCenter);
3534
3535 if (!offset.x && !offset.y) { return this; }
3536
3537 if (options.animate && options.pan) {
3538 this.panBy(offset);
3539
3540 } else {
3541 if (options.pan) {
3542 this._rawPanBy(offset);
3543 }
3544
3545 this.fire('move');
3546
3547 if (options.debounceMoveend) {
3548 clearTimeout(this._sizeTimer);
3549 this._sizeTimer = setTimeout(bind(this.fire, this, 'moveend'), 200);
3550 } else {
3551 this.fire('moveend');
3552 }
3553 }
3554
3555 // @section Map state change events
3556 // @event resize: ResizeEvent
3557 // Fired when the map is resized.
3558 return this.fire('resize', {
3559 oldSize: oldSize,
3560 newSize: newSize
3561 });
3562 },
3563
3564 // @section Methods for modifying map state
3565 // @method stop(): this
3566 // Stops the currently running `panTo` or `flyTo` animation, if any.
3567 stop: function () {
3568 this.setZoom(this._limitZoom(this._zoom));
3569 if (!this.options.zoomSnap) {
3570 this.fire('viewreset');
3571 }
3572 return this._stop();
3573 },
3574
3575 // @section Geolocation methods
3576 // @method locate(options?: Locate options): this
3577 // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
3578 // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
3579 // and optionally sets the map view to the user's location with respect to
3580 // detection accuracy (or to the world view if geolocation failed).
3581 // Note that, if your page doesn't use HTTPS, this method will fail in
3582 // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
3583 // See `Locate options` for more details.
3584 locate: function (options) {
3585
3586 options = this._locateOptions = extend({
3587 timeout: 10000,
3588 watch: false
3589 // setView: false
3590 // maxZoom: <Number>
3591 // maximumAge: 0
3592 // enableHighAccuracy: false
3593 }, options);
3594
3595 if (!('geolocation' in navigator)) {
3596 this._handleGeolocationError({
3597 code: 0,
3598 message: 'Geolocation not supported.'
3599 });
3600 return this;
3601 }
3602
3603 var onResponse = bind(this._handleGeolocationResponse, this),
3604 onError = bind(this._handleGeolocationError, this);
3605
3606 if (options.watch) {
3607 this._locationWatchId =
3608 navigator.geolocation.watchPosition(onResponse, onError, options);
3609 } else {
3610 navigator.geolocation.getCurrentPosition(onResponse, onError, options);
3611 }
3612 return this;
3613 },
3614
3615 // @method stopLocate(): this
3616 // Stops watching location previously initiated by `map.locate({watch: true})`
3617 // and aborts resetting the map view if map.locate was called with
3618 // `{setView: true}`.
3619 stopLocate: function () {
3620 if (navigator.geolocation && navigator.geolocation.clearWatch) {
3621 navigator.geolocation.clearWatch(this._locationWatchId);
3622 }
3623 if (this._locateOptions) {
3624 this._locateOptions.setView = false;
3625 }
3626 return this;
3627 },
3628
3629 _handleGeolocationError: function (error) {
3630 if (!this._container._leaflet_id) { return; }
3631
3632 var c = error.code,
3633 message = error.message ||
3634 (c === 1 ? 'permission denied' :
3635 (c === 2 ? 'position unavailable' : 'timeout'));
3636
3637 if (this._locateOptions.setView && !this._loaded) {
3638 this.fitWorld();
3639 }
3640
3641 // @section Location events
3642 // @event locationerror: ErrorEvent
3643 // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
3644 this.fire('locationerror', {
3645 code: c,
3646 message: 'Geolocation error: ' + message + '.'
3647 });
3648 },
3649
3650 _handleGeolocationResponse: function (pos) {
3651 if (!this._container._leaflet_id) { return; }
3652
3653 var lat = pos.coords.latitude,
3654 lng = pos.coords.longitude,
3655 latlng = new LatLng(lat, lng),
3656 bounds = latlng.toBounds(pos.coords.accuracy * 2),
3657 options = this._locateOptions;
3658
3659 if (options.setView) {
3660 var zoom = this.getBoundsZoom(bounds);
3661 this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
3662 }
3663
3664 var data = {
3665 latlng: latlng,
3666 bounds: bounds,
3667 timestamp: pos.timestamp
3668 };
3669
3670 for (var i in pos.coords) {
3671 if (typeof pos.coords[i] === 'number') {
3672 data[i] = pos.coords[i];
3673 }
3674 }
3675
3676 // @event locationfound: LocationEvent
3677 // Fired when geolocation (using the [`locate`](#map-locate) method)
3678 // went successfully.
3679 this.fire('locationfound', data);
3680 },
3681
3682 // TODO Appropriate docs section?
3683 // @section Other Methods
3684 // @method addHandler(name: String, HandlerClass: Function): this
3685 // Adds a new `Handler` to the map, given its name and constructor function.
3686 addHandler: function (name, HandlerClass) {
3687 if (!HandlerClass) { return this; }
3688
3689 var handler = this[name] = new HandlerClass(this);
3690
3691 this._handlers.push(handler);
3692
3693 if (this.options[name]) {
3694 handler.enable();
3695 }
3696
3697 return this;
3698 },
3699
3700 // @method remove(): this
3701 // Destroys the map and clears all related event listeners.
3702 remove: function () {
3703
3704 this._initEvents(true);
3705 if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); }
3706
3707 if (this._containerId !== this._container._leaflet_id) {
3708 throw new Error('Map container is being reused by another instance');
3709 }
3710
3711 try {
3712 // throws error in IE6-8
3713 delete this._container._leaflet_id;
3714 delete this._containerId;
3715 } catch (e) {
3716 /*eslint-disable */
3717 this._container._leaflet_id = undefined;
3718 /* eslint-enable */
3719 this._containerId = undefined;
3720 }
3721
3722 if (this._locationWatchId !== undefined) {
3723 this.stopLocate();
3724 }
3725
3726 this._stop();
3727
3728 remove(this._mapPane);
3729
3730 if (this._clearControlPos) {
3731 this._clearControlPos();
3732 }
3733 if (this._resizeRequest) {
3734 cancelAnimFrame(this._resizeRequest);
3735 this._resizeRequest = null;
3736 }
3737
3738 this._clearHandlers();
3739
3740 if (this._loaded) {
3741 // @section Map state change events
3742 // @event unload: Event
3743 // Fired when the map is destroyed with [remove](#map-remove) method.
3744 this.fire('unload');
3745 }
3746
3747 var i;
3748 for (i in this._layers) {
3749 this._layers[i].remove();
3750 }
3751 for (i in this._panes) {
3752 remove(this._panes[i]);
3753 }
3754
3755 this._layers = [];
3756 this._panes = [];
3757 delete this._mapPane;
3758 delete this._renderer;
3759
3760 return this;
3761 },
3762
3763 // @section Other Methods
3764 // @method createPane(name: String, container?: HTMLElement): HTMLElement
3765 // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already,
3766 // then returns it. The pane is created as a child of `container`, or
3767 // as a child of the main map pane if not set.
3768 createPane: function (name, container) {
3769 var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''),
3770 pane = create$1('div', className, container || this._mapPane);
3771
3772 if (name) {
3773 this._panes[name] = pane;
3774 }
3775 return pane;
3776 },
3777
3778 // @section Methods for Getting Map State
3779
3780 // @method getCenter(): LatLng
3781 // Returns the geographical center of the map view
3782 getCenter: function () {
3783 this._checkIfLoaded();
3784
3785 if (this._lastCenter && !this._moved()) {
3786 return this._lastCenter;
3787 }
3788 return this.layerPointToLatLng(this._getCenterLayerPoint());
3789 },
3790
3791 // @method getZoom(): Number
3792 // Returns the current zoom level of the map view
3793 getZoom: function () {
3794 return this._zoom;
3795 },
3796
3797 // @method getBounds(): LatLngBounds
3798 // Returns the geographical bounds visible in the current map view
3799 getBounds: function () {
3800 var bounds = this.getPixelBounds(),
3801 sw = this.unproject(bounds.getBottomLeft()),
3802 ne = this.unproject(bounds.getTopRight());
3803
3804 return new LatLngBounds(sw, ne);
3805 },
3806
3807 // @method getMinZoom(): Number
3808 // 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.
3809 getMinZoom: function () {
3810 return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom;
3811 },
3812
3813 // @method getMaxZoom(): Number
3814 // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers).
3815 getMaxZoom: function () {
3816 return this.options.maxZoom === undefined ?
3817 (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
3818 this.options.maxZoom;
3819 },
3820
3821 // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean, padding?: Point): Number
3822 // Returns the maximum zoom level on which the given bounds fit to the map
3823 // view in its entirety. If `inside` (optional) is set to `true`, the method
3824 // instead returns the minimum zoom level on which the map view fits into
3825 // the given bounds in its entirety.
3826 getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
3827 bounds = toLatLngBounds(bounds);
3828 padding = toPoint(padding || [0, 0]);
3829
3830 var zoom = this.getZoom() || 0,
3831 min = this.getMinZoom(),
3832 max = this.getMaxZoom(),
3833 nw = bounds.getNorthWest(),
3834 se = bounds.getSouthEast(),
3835 size = this.getSize().subtract(padding),
3836 boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
3837 snap = Browser.any3d ? this.options.zoomSnap : 1,
3838 scalex = size.x / boundsSize.x,
3839 scaley = size.y / boundsSize.y,
3840 scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);
3841
3842 zoom = this.getScaleZoom(scale, zoom);
3843
3844 if (snap) {
3845 zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
3846 zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
3847 }
3848
3849 return Math.max(min, Math.min(max, zoom));
3850 },
3851
3852 // @method getSize(): Point
3853 // Returns the current size of the map container (in pixels).
3854 getSize: function () {
3855 if (!this._size || this._sizeChanged) {
3856 this._size = new Point(
3857 this._container.clientWidth || 0,
3858 this._container.clientHeight || 0);
3859
3860 this._sizeChanged = false;
3861 }
3862 return this._size.clone();
3863 },
3864
3865 // @method getPixelBounds(): Bounds
3866 // Returns the bounds of the current map view in projected pixel
3867 // coordinates (sometimes useful in layer and overlay implementations).
3868 getPixelBounds: function (center, zoom) {
3869 var topLeftPoint = this._getTopLeftPoint(center, zoom);
3870 return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
3871 },
3872
3873 // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to
3874 // the map pane? "left point of the map layer" can be confusing, specially
3875 // since there can be negative offsets.
3876 // @method getPixelOrigin(): Point
3877 // Returns the projected pixel coordinates of the top left point of
3878 // the map layer (useful in custom layer and overlay implementations).
3879 getPixelOrigin: function () {
3880 this._checkIfLoaded();
3881 return this._pixelOrigin;
3882 },
3883
3884 // @method getPixelWorldBounds(zoom?: Number): Bounds
3885 // Returns the world's bounds in pixel coordinates for zoom level `zoom`.
3886 // If `zoom` is omitted, the map's current zoom level is used.
3887 getPixelWorldBounds: function (zoom) {
3888 return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom);
3889 },
3890
3891 // @section Other Methods
3892
3893 // @method getPane(pane: String|HTMLElement): HTMLElement
3894 // Returns a [map pane](#map-pane), given its name or its HTML element (its identity).
3895 getPane: function (pane) {
3896 return typeof pane === 'string' ? this._panes[pane] : pane;
3897 },
3898
3899 // @method getPanes(): Object
3900 // Returns a plain object containing the names of all [panes](#map-pane) as keys and
3901 // the panes as values.
3902 getPanes: function () {
3903 return this._panes;
3904 },
3905
3906 // @method getContainer: HTMLElement
3907 // Returns the HTML element that contains the map.
3908 getContainer: function () {
3909 return this._container;
3910 },
3911
3912
3913 // @section Conversion Methods
3914
3915 // @method getZoomScale(toZoom: Number, fromZoom: Number): Number
3916 // Returns the scale factor to be applied to a map transition from zoom level
3917 // `fromZoom` to `toZoom`. Used internally to help with zoom animations.
3918 getZoomScale: function (toZoom, fromZoom) {
3919 // TODO replace with universal implementation after refactoring projections
3920 var crs = this.options.crs;
3921 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
3922 return crs.scale(toZoom) / crs.scale(fromZoom);
3923 },
3924
3925 // @method getScaleZoom(scale: Number, fromZoom: Number): Number
3926 // Returns the zoom level that the map would end up at, if it is at `fromZoom`
3927 // level and everything is scaled by a factor of `scale`. Inverse of
3928 // [`getZoomScale`](#map-getZoomScale).
3929 getScaleZoom: function (scale, fromZoom) {
3930 var crs = this.options.crs;
3931 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
3932 var zoom = crs.zoom(scale * crs.scale(fromZoom));
3933 return isNaN(zoom) ? Infinity : zoom;
3934 },
3935
3936 // @method project(latlng: LatLng, zoom: Number): Point
3937 // Projects a geographical coordinate `LatLng` according to the projection
3938 // of the map's CRS, then scales it according to `zoom` and the CRS's
3939 // `Transformation`. The result is pixel coordinate relative to
3940 // the CRS origin.
3941 project: function (latlng, zoom) {
3942 zoom = zoom === undefined ? this._zoom : zoom;
3943 return this.options.crs.latLngToPoint(toLatLng(latlng), zoom);
3944 },
3945
3946 // @method unproject(point: Point, zoom: Number): LatLng
3947 // Inverse of [`project`](#map-project).
3948 unproject: function (point, zoom) {
3949 zoom = zoom === undefined ? this._zoom : zoom;
3950 return this.options.crs.pointToLatLng(toPoint(point), zoom);
3951 },
3952
3953 // @method layerPointToLatLng(point: Point): LatLng
3954 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
3955 // returns the corresponding geographical coordinate (for the current zoom level).
3956 layerPointToLatLng: function (point) {
3957 var projectedPoint = toPoint(point).add(this.getPixelOrigin());
3958 return this.unproject(projectedPoint);
3959 },
3960
3961 // @method latLngToLayerPoint(latlng: LatLng): Point
3962 // Given a geographical coordinate, returns the corresponding pixel coordinate
3963 // relative to the [origin pixel](#map-getpixelorigin).
3964 latLngToLayerPoint: function (latlng) {
3965 var projectedPoint = this.project(toLatLng(latlng))._round();
3966 return projectedPoint._subtract(this.getPixelOrigin());
3967 },
3968
3969 // @method wrapLatLng(latlng: LatLng): LatLng
3970 // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the
3971 // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the
3972 // CRS's bounds.
3973 // By default this means longitude is wrapped around the dateline so its
3974 // value is between -180 and +180 degrees.
3975 wrapLatLng: function (latlng) {
3976 return this.options.crs.wrapLatLng(toLatLng(latlng));
3977 },
3978
3979 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
3980 // Returns a `LatLngBounds` with the same size as the given one, ensuring that
3981 // its center is within the CRS's bounds.
3982 // By default this means the center longitude is wrapped around the dateline so its
3983 // value is between -180 and +180 degrees, and the majority of the bounds
3984 // overlaps the CRS's bounds.
3985 wrapLatLngBounds: function (latlng) {
3986 return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng));
3987 },
3988
3989 // @method distance(latlng1: LatLng, latlng2: LatLng): Number
3990 // Returns the distance between two geographical coordinates according to
3991 // the map's CRS. By default this measures distance in meters.
3992 distance: function (latlng1, latlng2) {
3993 return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2));
3994 },
3995
3996 // @method containerPointToLayerPoint(point: Point): Point
3997 // Given a pixel coordinate relative to the map container, returns the corresponding
3998 // pixel coordinate relative to the [origin pixel](#map-getpixelorigin).
3999 containerPointToLayerPoint: function (point) { // (Point)
4000 return toPoint(point).subtract(this._getMapPanePos());
4001 },
4002
4003 // @method layerPointToContainerPoint(point: Point): Point
4004 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
4005 // returns the corresponding pixel coordinate relative to the map container.
4006 layerPointToContainerPoint: function (point) { // (Point)
4007 return toPoint(point).add(this._getMapPanePos());
4008 },
4009
4010 // @method containerPointToLatLng(point: Point): LatLng
4011 // Given a pixel coordinate relative to the map container, returns
4012 // the corresponding geographical coordinate (for the current zoom level).
4013 containerPointToLatLng: function (point) {
4014 var layerPoint = this.containerPointToLayerPoint(toPoint(point));
4015 return this.layerPointToLatLng(layerPoint);
4016 },
4017
4018 // @method latLngToContainerPoint(latlng: LatLng): Point
4019 // Given a geographical coordinate, returns the corresponding pixel coordinate
4020 // relative to the map container.
4021 latLngToContainerPoint: function (latlng) {
4022 return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
4023 },
4024
4025 // @method mouseEventToContainerPoint(ev: MouseEvent): Point
4026 // Given a MouseEvent object, returns the pixel coordinate relative to the
4027 // map container where the event took place.
4028 mouseEventToContainerPoint: function (e) {
4029 return getMousePosition(e, this._container);
4030 },
4031
4032 // @method mouseEventToLayerPoint(ev: MouseEvent): Point
4033 // Given a MouseEvent object, returns the pixel coordinate relative to
4034 // the [origin pixel](#map-getpixelorigin) where the event took place.
4035 mouseEventToLayerPoint: function (e) {
4036 return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
4037 },
4038
4039 // @method mouseEventToLatLng(ev: MouseEvent): LatLng
4040 // Given a MouseEvent object, returns geographical coordinate where the
4041 // event took place.
4042 mouseEventToLatLng: function (e) { // (MouseEvent)
4043 return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
4044 },
4045
4046
4047 // map initialization methods
4048
4049 _initContainer: function (id) {
4050 var container = this._container = get(id);
4051
4052 if (!container) {
4053 throw new Error('Map container not found.');
4054 } else if (container._leaflet_id) {
4055 throw new Error('Map container is already initialized.');
4056 }
4057
4058 on(container, 'scroll', this._onScroll, this);
4059 this._containerId = stamp(container);
4060 },
4061
4062 _initLayout: function () {
4063 var container = this._container;
4064
4065 this._fadeAnimated = this.options.fadeAnimation && Browser.any3d;
4066
4067 addClass(container, 'leaflet-container' +
4068 (Browser.touch ? ' leaflet-touch' : '') +
4069 (Browser.retina ? ' leaflet-retina' : '') +
4070 (Browser.ielt9 ? ' leaflet-oldie' : '') +
4071 (Browser.safari ? ' leaflet-safari' : '') +
4072 (this._fadeAnimated ? ' leaflet-fade-anim' : ''));
4073
4074 var position = getStyle(container, 'position');
4075
4076 if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
4077 container.style.position = 'relative';
4078 }
4079
4080 this._initPanes();
4081
4082 if (this._initControlPos) {
4083 this._initControlPos();
4084 }
4085 },
4086
4087 _initPanes: function () {
4088 var panes = this._panes = {};
4089 this._paneRenderers = {};
4090
4091 // @section
4092 //
4093 // Panes are DOM elements used to control the ordering of layers on the map. You
4094 // can access panes with [`map.getPane`](#map-getpane) or
4095 // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the
4096 // [`map.createPane`](#map-createpane) method.
4097 //
4098 // Every map has the following default panes that differ only in zIndex.
4099 //
4100 // @pane mapPane: HTMLElement = 'auto'
4101 // Pane that contains all other map panes
4102
4103 this._mapPane = this.createPane('mapPane', this._container);
4104 setPosition(this._mapPane, new Point(0, 0));
4105
4106 // @pane tilePane: HTMLElement = 200
4107 // Pane for `GridLayer`s and `TileLayer`s
4108 this.createPane('tilePane');
4109 // @pane overlayPane: HTMLElement = 400
4110 // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s
4111 this.createPane('overlayPane');
4112 // @pane shadowPane: HTMLElement = 500
4113 // Pane for overlay shadows (e.g. `Marker` shadows)
4114 this.createPane('shadowPane');
4115 // @pane markerPane: HTMLElement = 600
4116 // Pane for `Icon`s of `Marker`s
4117 this.createPane('markerPane');
4118 // @pane tooltipPane: HTMLElement = 650
4119 // Pane for `Tooltip`s.
4120 this.createPane('tooltipPane');
4121 // @pane popupPane: HTMLElement = 700
4122 // Pane for `Popup`s.
4123 this.createPane('popupPane');
4124
4125 if (!this.options.markerZoomAnimation) {
4126 addClass(panes.markerPane, 'leaflet-zoom-hide');
4127 addClass(panes.shadowPane, 'leaflet-zoom-hide');
4128 }
4129 },
4130
4131
4132 // private methods that modify map state
4133
4134 // @section Map state change events
4135 _resetView: function (center, zoom) {
4136 setPosition(this._mapPane, new Point(0, 0));
4137
4138 var loading = !this._loaded;
4139 this._loaded = true;
4140 zoom = this._limitZoom(zoom);
4141
4142 this.fire('viewprereset');
4143
4144 var zoomChanged = this._zoom !== zoom;
4145 this
4146 ._moveStart(zoomChanged, false)
4147 ._move(center, zoom)
4148 ._moveEnd(zoomChanged);
4149
4150 // @event viewreset: Event
4151 // Fired when the map needs to redraw its content (this usually happens
4152 // on map zoom or load). Very useful for creating custom overlays.
4153 this.fire('viewreset');
4154
4155 // @event load: Event
4156 // Fired when the map is initialized (when its center and zoom are set
4157 // for the first time).
4158 if (loading) {
4159 this.fire('load');
4160 }
4161 },
4162
4163 _moveStart: function (zoomChanged, noMoveStart) {
4164 // @event zoomstart: Event
4165 // Fired when the map zoom is about to change (e.g. before zoom animation).
4166 // @event movestart: Event
4167 // Fired when the view of the map starts changing (e.g. user starts dragging the map).
4168 if (zoomChanged) {
4169 this.fire('zoomstart');
4170 }
4171 if (!noMoveStart) {
4172 this.fire('movestart');
4173 }
4174 return this;
4175 },
4176
4177 _move: function (center, zoom, data, supressEvent) {
4178 if (zoom === undefined) {
4179 zoom = this._zoom;
4180 }
4181 var zoomChanged = this._zoom !== zoom;
4182
4183 this._zoom = zoom;
4184 this._lastCenter = center;
4185 this._pixelOrigin = this._getNewPixelOrigin(center);
4186
4187 if (!supressEvent) {
4188 // @event zoom: Event
4189 // Fired repeatedly during any change in zoom level,
4190 // including zoom and fly animations.
4191 if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530
4192 this.fire('zoom', data);
4193 }
4194
4195 // @event move: Event
4196 // Fired repeatedly during any movement of the map,
4197 // including pan and fly animations.
4198 this.fire('move', data);
4199 } else if (data && data.pinch) { // Always fire 'zoom' if pinching because #3530
4200 this.fire('zoom', data);
4201 }
4202 return this;
4203 },
4204
4205 _moveEnd: function (zoomChanged) {
4206 // @event zoomend: Event
4207 // Fired when the map zoom changed, after any animations.
4208 if (zoomChanged) {
4209 this.fire('zoomend');
4210 }
4211
4212 // @event moveend: Event
4213 // Fired when the center of the map stops changing
4214 // (e.g. user stopped dragging the map or after non-centered zoom).
4215 return this.fire('moveend');
4216 },
4217
4218 _stop: function () {
4219 cancelAnimFrame(this._flyToFrame);
4220 if (this._panAnim) {
4221 this._panAnim.stop();
4222 }
4223 return this;
4224 },
4225
4226 _rawPanBy: function (offset) {
4227 setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
4228 },
4229
4230 _getZoomSpan: function () {
4231 return this.getMaxZoom() - this.getMinZoom();
4232 },
4233
4234 _panInsideMaxBounds: function () {
4235 if (!this._enforcingBounds) {
4236 this.panInsideBounds(this.options.maxBounds);
4237 }
4238 },
4239
4240 _checkIfLoaded: function () {
4241 if (!this._loaded) {
4242 throw new Error('Set map center and zoom first.');
4243 }
4244 },
4245
4246 // DOM event handling
4247
4248 // @section Interaction events
4249 _initEvents: function (remove) {
4250 this._targets = {};
4251 this._targets[stamp(this._container)] = this;
4252
4253 var onOff = remove ? off : on;
4254
4255 // @event click: MouseEvent
4256 // Fired when the user clicks (or taps) the map.
4257 // @event dblclick: MouseEvent
4258 // Fired when the user double-clicks (or double-taps) the map.
4259 // @event mousedown: MouseEvent
4260 // Fired when the user pushes the mouse button on the map.
4261 // @event mouseup: MouseEvent
4262 // Fired when the user releases the mouse button on the map.
4263 // @event mouseover: MouseEvent
4264 // Fired when the mouse enters the map.
4265 // @event mouseout: MouseEvent
4266 // Fired when the mouse leaves the map.
4267 // @event mousemove: MouseEvent
4268 // Fired while the mouse moves over the map.
4269 // @event contextmenu: MouseEvent
4270 // Fired when the user pushes the right mouse button on the map, prevents
4271 // default browser context menu from showing if there are listeners on
4272 // this event. Also fired on mobile when the user holds a single touch
4273 // for a second (also called long press).
4274 // @event keypress: KeyboardEvent
4275 // Fired when the user presses a key from the keyboard that produces a character value while the map is focused.
4276 // @event keydown: KeyboardEvent
4277 // Fired when the user presses a key from the keyboard while the map is focused. Unlike the `keypress` event,
4278 // the `keydown` event is fired for keys that produce a character value and for keys
4279 // that do not produce a character value.
4280 // @event keyup: KeyboardEvent
4281 // Fired when the user releases a key from the keyboard while the map is focused.
4282 onOff(this._container, 'click dblclick mousedown mouseup ' +
4283 'mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this);
4284
4285 if (this.options.trackResize) {
4286 onOff(window, 'resize', this._onResize, this);
4287 }
4288
4289 if (Browser.any3d && this.options.transform3DLimit) {
4290 (remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
4291 }
4292 },
4293
4294 _onResize: function () {
4295 cancelAnimFrame(this._resizeRequest);
4296 this._resizeRequest = requestAnimFrame(
4297 function () { this.invalidateSize({debounceMoveend: true}); }, this);
4298 },
4299
4300 _onScroll: function () {
4301 this._container.scrollTop = 0;
4302 this._container.scrollLeft = 0;
4303 },
4304
4305 _onMoveEnd: function () {
4306 var pos = this._getMapPanePos();
4307 if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
4308 // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
4309 // a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/
4310 this._resetView(this.getCenter(), this.getZoom());
4311 }
4312 },
4313
4314 _findEventTargets: function (e, type) {
4315 var targets = [],
4316 target,
4317 isHover = type === 'mouseout' || type === 'mouseover',
4318 src = e.target || e.srcElement,
4319 dragging = false;
4320
4321 while (src) {
4322 target = this._targets[stamp(src)];
4323 if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) {
4324 // Prevent firing click after you just dragged an object.
4325 dragging = true;
4326 break;
4327 }
4328 if (target && target.listens(type, true)) {
4329 if (isHover && !isExternalTarget(src, e)) { break; }
4330 targets.push(target);
4331 if (isHover) { break; }
4332 }
4333 if (src === this._container) { break; }
4334 src = src.parentNode;
4335 }
4336 if (!targets.length && !dragging && !isHover && this.listens(type, true)) {
4337 targets = [this];
4338 }
4339 return targets;
4340 },
4341
4342 _isClickDisabled: function (el) {
4343 while (el !== this._container) {
4344 if (el['_leaflet_disable_click']) { return true; }
4345 el = el.parentNode;
4346 }
4347 },
4348
4349 _handleDOMEvent: function (e) {
4350 var el = (e.target || e.srcElement);
4351 if (!this._loaded || el['_leaflet_disable_events'] || e.type === 'click' && this._isClickDisabled(el)) {
4352 return;
4353 }
4354
4355 var type = e.type;
4356
4357 if (type === 'mousedown') {
4358 // prevents outline when clicking on keyboard-focusable element
4359 preventOutline(el);
4360 }
4361
4362 this._fireDOMEvent(e, type);
4363 },
4364
4365 _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],
4366
4367 _fireDOMEvent: function (e, type, canvasTargets) {
4368
4369 if (e.type === 'click') {
4370 // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
4371 // @event preclick: MouseEvent
4372 // Fired before mouse click on the map (sometimes useful when you
4373 // want something to happen on click before any existing click
4374 // handlers start running).
4375 var synth = extend({}, e);
4376 synth.type = 'preclick';
4377 this._fireDOMEvent(synth, synth.type, canvasTargets);
4378 }
4379
4380 // Find the layer the event is propagating from and its parents.
4381 var targets = this._findEventTargets(e, type);
4382
4383 if (canvasTargets) {
4384 var filtered = []; // pick only targets with listeners
4385 for (var i = 0; i < canvasTargets.length; i++) {
4386 if (canvasTargets[i].listens(type, true)) {
4387 filtered.push(canvasTargets[i]);
4388 }
4389 }
4390 targets = filtered.concat(targets);
4391 }
4392
4393 if (!targets.length) { return; }
4394
4395 if (type === 'contextmenu') {
4396 preventDefault(e);
4397 }
4398
4399 var target = targets[0];
4400 var data = {
4401 originalEvent: e
4402 };
4403
4404 if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') {
4405 var isMarker = target.getLatLng && (!target._radius || target._radius <= 10);
4406 data.containerPoint = isMarker ?
4407 this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
4408 data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
4409 data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
4410 }
4411
4412 for (i = 0; i < targets.length; i++) {
4413 targets[i].fire(type, data, true);
4414 if (data.originalEvent._stopped ||
4415 (targets[i].options.bubblingMouseEvents === false && indexOf(this._mouseEvents, type) !== -1)) { return; }
4416 }
4417 },
4418
4419 _draggableMoved: function (obj) {
4420 obj = obj.dragging && obj.dragging.enabled() ? obj : this;
4421 return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
4422 },
4423
4424 _clearHandlers: function () {
4425 for (var i = 0, len = this._handlers.length; i < len; i++) {
4426 this._handlers[i].disable();
4427 }
4428 },
4429
4430 // @section Other Methods
4431
4432 // @method whenReady(fn: Function, context?: Object): this
4433 // Runs the given function `fn` when the map gets initialized with
4434 // a view (center and zoom) and at least one layer, or immediately
4435 // if it's already initialized, optionally passing a function context.
4436 whenReady: function (callback, context) {
4437 if (this._loaded) {
4438 callback.call(context || this, {target: this});
4439 } else {
4440 this.on('load', callback, context);
4441 }
4442 return this;
4443 },
4444
4445
4446 // private methods for getting map state
4447
4448 _getMapPanePos: function () {
4449 return getPosition(this._mapPane) || new Point(0, 0);
4450 },
4451
4452 _moved: function () {
4453 var pos = this._getMapPanePos();
4454 return pos && !pos.equals([0, 0]);
4455 },
4456
4457 _getTopLeftPoint: function (center, zoom) {
4458 var pixelOrigin = center && zoom !== undefined ?
4459 this._getNewPixelOrigin(center, zoom) :
4460 this.getPixelOrigin();
4461 return pixelOrigin.subtract(this._getMapPanePos());
4462 },
4463
4464 _getNewPixelOrigin: function (center, zoom) {
4465 var viewHalf = this.getSize()._divideBy(2);
4466 return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round();
4467 },
4468
4469 _latLngToNewLayerPoint: function (latlng, zoom, center) {
4470 var topLeft = this._getNewPixelOrigin(center, zoom);
4471 return this.project(latlng, zoom)._subtract(topLeft);
4472 },
4473
4474 _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
4475 var topLeft = this._getNewPixelOrigin(center, zoom);
4476 return toBounds([
4477 this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
4478 this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
4479 this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
4480 this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)
4481 ]);
4482 },
4483
4484 // layer point of the current center
4485 _getCenterLayerPoint: function () {
4486 return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
4487 },
4488
4489 // offset of the specified place to the current center in pixels
4490 _getCenterOffset: function (latlng) {
4491 return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
4492 },
4493
4494 // adjust center for view to get inside bounds
4495 _limitCenter: function (center, zoom, bounds) {
4496
4497 if (!bounds) { return center; }
4498
4499 var centerPoint = this.project(center, zoom),
4500 viewHalf = this.getSize().divideBy(2),
4501 viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
4502 offset = this._getBoundsOffset(viewBounds, bounds, zoom);
4503
4504 // If offset is less than a pixel, ignore.
4505 // This prevents unstable projections from getting into
4506 // an infinite loop of tiny offsets.
4507 if (offset.round().equals([0, 0])) {
4508 return center;
4509 }
4510
4511 return this.unproject(centerPoint.add(offset), zoom);
4512 },
4513
4514 // adjust offset for view to get inside bounds
4515 _limitOffset: function (offset, bounds) {
4516 if (!bounds) { return offset; }
4517
4518 var viewBounds = this.getPixelBounds(),
4519 newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
4520
4521 return offset.add(this._getBoundsOffset(newBounds, bounds));
4522 },
4523
4524 // returns offset needed for pxBounds to get inside maxBounds at a specified zoom
4525 _getBoundsOffset: function (pxBounds, maxBounds, zoom) {
4526 var projectedMaxBounds = toBounds(
4527 this.project(maxBounds.getNorthEast(), zoom),
4528 this.project(maxBounds.getSouthWest(), zoom)
4529 ),
4530 minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
4531 maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),
4532
4533 dx = this._rebound(minOffset.x, -maxOffset.x),
4534 dy = this._rebound(minOffset.y, -maxOffset.y);
4535
4536 return new Point(dx, dy);
4537 },
4538
4539 _rebound: function (left, right) {
4540 return left + right > 0 ?
4541 Math.round(left - right) / 2 :
4542 Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
4543 },
4544
4545 _limitZoom: function (zoom) {
4546 var min = this.getMinZoom(),
4547 max = this.getMaxZoom(),
4548 snap = Browser.any3d ? this.options.zoomSnap : 1;
4549 if (snap) {
4550 zoom = Math.round(zoom / snap) * snap;
4551 }
4552 return Math.max(min, Math.min(max, zoom));
4553 },
4554
4555 _onPanTransitionStep: function () {
4556 this.fire('move');
4557 },
4558
4559 _onPanTransitionEnd: function () {
4560 removeClass(this._mapPane, 'leaflet-pan-anim');
4561 this.fire('moveend');
4562 },
4563
4564 _tryAnimatedPan: function (center, options) {
4565 // difference between the new and current centers in pixels
4566 var offset = this._getCenterOffset(center)._trunc();
4567
4568 // don't animate too far unless animate: true specified in options
4569 if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
4570
4571 this.panBy(offset, options);
4572
4573 return true;
4574 },
4575
4576 _createAnimProxy: function () {
4577
4578 var proxy = this._proxy = create$1('div', 'leaflet-proxy leaflet-zoom-animated');
4579 this._panes.mapPane.appendChild(proxy);
4580
4581 this.on('zoomanim', function (e) {
4582 var prop = TRANSFORM,
4583 transform = this._proxy.style[prop];
4584
4585 setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));
4586
4587 // workaround for case when transform is the same and so transitionend event is not fired
4588 if (transform === this._proxy.style[prop] && this._animatingZoom) {
4589 this._onZoomTransitionEnd();
4590 }
4591 }, this);
4592
4593 this.on('load moveend', this._animMoveEnd, this);
4594
4595 this._on('unload', this._destroyAnimProxy, this);
4596 },
4597
4598 _destroyAnimProxy: function () {
4599 remove(this._proxy);
4600 this.off('load moveend', this._animMoveEnd, this);
4601 delete this._proxy;
4602 },
4603
4604 _animMoveEnd: function () {
4605 var c = this.getCenter(),
4606 z = this.getZoom();
4607 setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1));
4608 },
4609
4610 _catchTransitionEnd: function (e) {
4611 if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
4612 this._onZoomTransitionEnd();
4613 }
4614 },
4615
4616 _nothingToAnimate: function () {
4617 return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
4618 },
4619
4620 _tryAnimatedZoom: function (center, zoom, options) {
4621
4622 if (this._animatingZoom) { return true; }
4623
4624 options = options || {};
4625
4626 // don't animate if disabled, not supported or zoom difference is too large
4627 if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
4628 Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
4629
4630 // offset is the pixel coords of the zoom origin relative to the current center
4631 var scale = this.getZoomScale(zoom),
4632 offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);
4633
4634 // don't animate if the zoom origin isn't within one screen from the current center, unless forced
4635 if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
4636
4637 requestAnimFrame(function () {
4638 this
4639 ._moveStart(true, false)
4640 ._animateZoom(center, zoom, true);
4641 }, this);
4642
4643 return true;
4644 },
4645
4646 _animateZoom: function (center, zoom, startAnim, noUpdate) {
4647 if (!this._mapPane) { return; }
4648
4649 if (startAnim) {
4650 this._animatingZoom = true;
4651
4652 // remember what center/zoom to set after animation
4653 this._animateToCenter = center;
4654 this._animateToZoom = zoom;
4655
4656 addClass(this._mapPane, 'leaflet-zoom-anim');
4657 }
4658
4659 // @section Other Events
4660 // @event zoomanim: ZoomAnimEvent
4661 // Fired at least once per zoom animation. For continuous zoom, like pinch zooming, fired once per frame during zoom.
4662 this.fire('zoomanim', {
4663 center: center,
4664 zoom: zoom,
4665 noUpdate: noUpdate
4666 });
4667
4668 if (!this._tempFireZoomEvent) {
4669 this._tempFireZoomEvent = this._zoom !== this._animateToZoom;
4670 }
4671
4672 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
4673
4674 // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
4675 setTimeout(bind(this._onZoomTransitionEnd, this), 250);
4676 },
4677
4678 _onZoomTransitionEnd: function () {
4679 if (!this._animatingZoom) { return; }
4680
4681 if (this._mapPane) {
4682 removeClass(this._mapPane, 'leaflet-zoom-anim');
4683 }
4684
4685 this._animatingZoom = false;
4686
4687 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
4688
4689 if (this._tempFireZoomEvent) {
4690 this.fire('zoom');
4691 }
4692 delete this._tempFireZoomEvent;
4693
4694 this.fire('move');
4695
4696 this._moveEnd(true);
4697 }
4698});
4699
4700// @section
4701
4702// @factory L.map(id: String, options?: Map options)
4703// Instantiates a map object given the DOM ID of a `<div>` element
4704// and optionally an object literal with `Map options`.
4705//
4706// @alternative
4707// @factory L.map(el: HTMLElement, options?: Map options)
4708// Instantiates a map object given an instance of a `<div>` HTML element
4709// and optionally an object literal with `Map options`.
4710function createMap(id, options) {
4711 return new Map(id, options);
4712}
4713
4714/*
4715 * @class Control
4716 * @aka L.Control
4717 * @inherits Class
4718 *
4719 * L.Control is a base class for implementing map controls. Handles positioning.
4720 * All other controls extend from this class.
4721 */
4722
4723var Control = Class.extend({
4724 // @section
4725 // @aka Control Options
4726 options: {
4727 // @option position: String = 'topright'
4728 // The position of the control (one of the map corners). Possible values are `'topleft'`,
4729 // `'topright'`, `'bottomleft'` or `'bottomright'`
4730 position: 'topright'
4731 },
4732
4733 initialize: function (options) {
4734 setOptions(this, options);
4735 },
4736
4737 /* @section
4738 * Classes extending L.Control will inherit the following methods:
4739 *
4740 * @method getPosition: string
4741 * Returns the position of the control.
4742 */
4743 getPosition: function () {
4744 return this.options.position;
4745 },
4746
4747 // @method setPosition(position: string): this
4748 // Sets the position of the control.
4749 setPosition: function (position) {
4750 var map = this._map;
4751
4752 if (map) {
4753 map.removeControl(this);
4754 }
4755
4756 this.options.position = position;
4757
4758 if (map) {
4759 map.addControl(this);
4760 }
4761
4762 return this;
4763 },
4764
4765 // @method getContainer: HTMLElement
4766 // Returns the HTMLElement that contains the control.
4767 getContainer: function () {
4768 return this._container;
4769 },
4770
4771 // @method addTo(map: Map): this
4772 // Adds the control to the given map.
4773 addTo: function (map) {
4774 this.remove();
4775 this._map = map;
4776
4777 var container = this._container = this.onAdd(map),
4778 pos = this.getPosition(),
4779 corner = map._controlCorners[pos];
4780
4781 addClass(container, 'leaflet-control');
4782
4783 if (pos.indexOf('bottom') !== -1) {
4784 corner.insertBefore(container, corner.firstChild);
4785 } else {
4786 corner.appendChild(container);
4787 }
4788
4789 this._map.on('unload', this.remove, this);
4790
4791 return this;
4792 },
4793
4794 // @method remove: this
4795 // Removes the control from the map it is currently active on.
4796 remove: function () {
4797 if (!this._map) {
4798 return this;
4799 }
4800
4801 remove(this._container);
4802
4803 if (this.onRemove) {
4804 this.onRemove(this._map);
4805 }
4806
4807 this._map.off('unload', this.remove, this);
4808 this._map = null;
4809
4810 return this;
4811 },
4812
4813 _refocusOnMap: function (e) {
4814 // if map exists and event is not a keyboard event
4815 if (this._map && e && e.screenX > 0 && e.screenY > 0) {
4816 this._map.getContainer().focus();
4817 }
4818 }
4819});
4820
4821var control = function (options) {
4822 return new Control(options);
4823};
4824
4825/* @section Extension methods
4826 * @uninheritable
4827 *
4828 * Every control should extend from `L.Control` and (re-)implement the following methods.
4829 *
4830 * @method onAdd(map: Map): HTMLElement
4831 * Should return the container DOM element for the control and add listeners on relevant map events. Called on [`control.addTo(map)`](#control-addTo).
4832 *
4833 * @method onRemove(map: Map)
4834 * Optional method. Should contain all clean up code that removes the listeners previously added in [`onAdd`](#control-onadd). Called on [`control.remove()`](#control-remove).
4835 */
4836
4837/* @namespace Map
4838 * @section Methods for Layers and Controls
4839 */
4840Map.include({
4841 // @method addControl(control: Control): this
4842 // Adds the given control to the map
4843 addControl: function (control) {
4844 control.addTo(this);
4845 return this;
4846 },
4847
4848 // @method removeControl(control: Control): this
4849 // Removes the given control from the map
4850 removeControl: function (control) {
4851 control.remove();
4852 return this;
4853 },
4854
4855 _initControlPos: function () {
4856 var corners = this._controlCorners = {},
4857 l = 'leaflet-',
4858 container = this._controlContainer =
4859 create$1('div', l + 'control-container', this._container);
4860
4861 function createCorner(vSide, hSide) {
4862 var className = l + vSide + ' ' + l + hSide;
4863
4864 corners[vSide + hSide] = create$1('div', className, container);
4865 }
4866
4867 createCorner('top', 'left');
4868 createCorner('top', 'right');
4869 createCorner('bottom', 'left');
4870 createCorner('bottom', 'right');
4871 },
4872
4873 _clearControlPos: function () {
4874 for (var i in this._controlCorners) {
4875 remove(this._controlCorners[i]);
4876 }
4877 remove(this._controlContainer);
4878 delete this._controlCorners;
4879 delete this._controlContainer;
4880 }
4881});
4882
4883/*
4884 * @class Control.Layers
4885 * @aka L.Control.Layers
4886 * @inherits Control
4887 *
4888 * 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`.
4889 *
4890 * @example
4891 *
4892 * ```js
4893 * var baseLayers = {
4894 * "Mapbox": mapbox,
4895 * "OpenStreetMap": osm
4896 * };
4897 *
4898 * var overlays = {
4899 * "Marker": marker,
4900 * "Roads": roadsLayer
4901 * };
4902 *
4903 * L.control.layers(baseLayers, overlays).addTo(map);
4904 * ```
4905 *
4906 * The `baseLayers` and `overlays` parameters are object literals with layer names as keys and `Layer` objects as values:
4907 *
4908 * ```js
4909 * {
4910 * "<someName1>": layer1,
4911 * "<someName2>": layer2
4912 * }
4913 * ```
4914 *
4915 * The layer names can contain HTML, which allows you to add additional styling to the items:
4916 *
4917 * ```js
4918 * {"<img src='my-layer-icon' /> <span class='my-layer-item'>My Layer</span>": myLayer}
4919 * ```
4920 */
4921
4922var Layers = Control.extend({
4923 // @section
4924 // @aka Control.Layers options
4925 options: {
4926 // @option collapsed: Boolean = true
4927 // If `true`, the control will be collapsed into an icon and expanded on mouse hover, touch, or keyboard activation.
4928 collapsed: true,
4929 position: 'topright',
4930
4931 // @option autoZIndex: Boolean = true
4932 // 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.
4933 autoZIndex: true,
4934
4935 // @option hideSingleBase: Boolean = false
4936 // If `true`, the base layers in the control will be hidden when there is only one.
4937 hideSingleBase: false,
4938
4939 // @option sortLayers: Boolean = false
4940 // Whether to sort the layers. When `false`, layers will keep the order
4941 // in which they were added to the control.
4942 sortLayers: false,
4943
4944 // @option sortFunction: Function = *
4945 // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
4946 // that will be used for sorting the layers, when `sortLayers` is `true`.
4947 // The function receives both the `L.Layer` instances and their names, as in
4948 // `sortFunction(layerA, layerB, nameA, nameB)`.
4949 // By default, it sorts layers alphabetically by their name.
4950 sortFunction: function (layerA, layerB, nameA, nameB) {
4951 return nameA < nameB ? -1 : (nameB < nameA ? 1 : 0);
4952 }
4953 },
4954
4955 initialize: function (baseLayers, overlays, options) {
4956 setOptions(this, options);
4957
4958 this._layerControlInputs = [];
4959 this._layers = [];
4960 this._lastZIndex = 0;
4961 this._handlingClick = false;
4962
4963 for (var i in baseLayers) {
4964 this._addLayer(baseLayers[i], i);
4965 }
4966
4967 for (i in overlays) {
4968 this._addLayer(overlays[i], i, true);
4969 }
4970 },
4971
4972 onAdd: function (map) {
4973 this._initLayout();
4974 this._update();
4975
4976 this._map = map;
4977 map.on('zoomend', this._checkDisabledLayers, this);
4978
4979 for (var i = 0; i < this._layers.length; i++) {
4980 this._layers[i].layer.on('add remove', this._onLayerChange, this);
4981 }
4982
4983 return this._container;
4984 },
4985
4986 addTo: function (map) {
4987 Control.prototype.addTo.call(this, map);
4988 // Trigger expand after Layers Control has been inserted into DOM so that is now has an actual height.
4989 return this._expandIfNotCollapsed();
4990 },
4991
4992 onRemove: function () {
4993 this._map.off('zoomend', this._checkDisabledLayers, this);
4994
4995 for (var i = 0; i < this._layers.length; i++) {
4996 this._layers[i].layer.off('add remove', this._onLayerChange, this);
4997 }
4998 },
4999
5000 // @method addBaseLayer(layer: Layer, name: String): this
5001 // Adds a base layer (radio button entry) with the given name to the control.
5002 addBaseLayer: function (layer, name) {
5003 this._addLayer(layer, name);
5004 return (this._map) ? this._update() : this;
5005 },
5006
5007 // @method addOverlay(layer: Layer, name: String): this
5008 // Adds an overlay (checkbox entry) with the given name to the control.
5009 addOverlay: function (layer, name) {
5010 this._addLayer(layer, name, true);
5011 return (this._map) ? this._update() : this;
5012 },
5013
5014 // @method removeLayer(layer: Layer): this
5015 // Remove the given layer from the control.
5016 removeLayer: function (layer) {
5017 layer.off('add remove', this._onLayerChange, this);
5018
5019 var obj = this._getLayer(stamp(layer));
5020 if (obj) {
5021 this._layers.splice(this._layers.indexOf(obj), 1);
5022 }
5023 return (this._map) ? this._update() : this;
5024 },
5025
5026 // @method expand(): this
5027 // Expand the control container if collapsed.
5028 expand: function () {
5029 addClass(this._container, 'leaflet-control-layers-expanded');
5030 this._section.style.height = null;
5031 var acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50);
5032 if (acceptableHeight < this._section.clientHeight) {
5033 addClass(this._section, 'leaflet-control-layers-scrollbar');
5034 this._section.style.height = acceptableHeight + 'px';
5035 } else {
5036 removeClass(this._section, 'leaflet-control-layers-scrollbar');
5037 }
5038 this._checkDisabledLayers();
5039 return this;
5040 },
5041
5042 // @method collapse(): this
5043 // Collapse the control container if expanded.
5044 collapse: function () {
5045 removeClass(this._container, 'leaflet-control-layers-expanded');
5046 return this;
5047 },
5048
5049 _initLayout: function () {
5050 var className = 'leaflet-control-layers',
5051 container = this._container = create$1('div', className),
5052 collapsed = this.options.collapsed;
5053
5054 // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released
5055 container.setAttribute('aria-haspopup', true);
5056
5057 disableClickPropagation(container);
5058 disableScrollPropagation(container);
5059
5060 var section = this._section = create$1('section', className + '-list');
5061
5062 if (collapsed) {
5063 this._map.on('click', this.collapse, this);
5064
5065 on(container, {
5066 mouseenter: function () {
5067 on(section, 'click', preventDefault);
5068 this.expand();
5069 setTimeout(function () {
5070 off(section, 'click', preventDefault);
5071 });
5072 },
5073 mouseleave: this.collapse
5074 }, this);
5075 }
5076
5077 var link = this._layersLink = create$1('a', className + '-toggle', container);
5078 link.href = '#';
5079 link.title = 'Layers';
5080 link.setAttribute('role', 'button');
5081
5082 on(link, 'click', preventDefault); // prevent link function
5083 on(link, 'focus', this.expand, this);
5084
5085 if (!collapsed) {
5086 this.expand();
5087 }
5088
5089 this._baseLayersList = create$1('div', className + '-base', section);
5090 this._separator = create$1('div', className + '-separator', section);
5091 this._overlaysList = create$1('div', className + '-overlays', section);
5092
5093 container.appendChild(section);
5094 },
5095
5096 _getLayer: function (id) {
5097 for (var i = 0; i < this._layers.length; i++) {
5098
5099 if (this._layers[i] && stamp(this._layers[i].layer) === id) {
5100 return this._layers[i];
5101 }
5102 }
5103 },
5104
5105 _addLayer: function (layer, name, overlay) {
5106 if (this._map) {
5107 layer.on('add remove', this._onLayerChange, this);
5108 }
5109
5110 this._layers.push({
5111 layer: layer,
5112 name: name,
5113 overlay: overlay
5114 });
5115
5116 if (this.options.sortLayers) {
5117 this._layers.sort(bind(function (a, b) {
5118 return this.options.sortFunction(a.layer, b.layer, a.name, b.name);
5119 }, this));
5120 }
5121
5122 if (this.options.autoZIndex && layer.setZIndex) {
5123 this._lastZIndex++;
5124 layer.setZIndex(this._lastZIndex);
5125 }
5126
5127 this._expandIfNotCollapsed();
5128 },
5129
5130 _update: function () {
5131 if (!this._container) { return this; }
5132
5133 empty(this._baseLayersList);
5134 empty(this._overlaysList);
5135
5136 this._layerControlInputs = [];
5137 var baseLayersPresent, overlaysPresent, i, obj, baseLayersCount = 0;
5138
5139 for (i = 0; i < this._layers.length; i++) {
5140 obj = this._layers[i];
5141 this._addItem(obj);
5142 overlaysPresent = overlaysPresent || obj.overlay;
5143 baseLayersPresent = baseLayersPresent || !obj.overlay;
5144 baseLayersCount += !obj.overlay ? 1 : 0;
5145 }
5146
5147 // Hide base layers section if there's only one layer.
5148 if (this.options.hideSingleBase) {
5149 baseLayersPresent = baseLayersPresent && baseLayersCount > 1;
5150 this._baseLayersList.style.display = baseLayersPresent ? '' : 'none';
5151 }
5152
5153 this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
5154
5155 return this;
5156 },
5157
5158 _onLayerChange: function (e) {
5159 if (!this._handlingClick) {
5160 this._update();
5161 }
5162
5163 var obj = this._getLayer(stamp(e.target));
5164
5165 // @namespace Map
5166 // @section Layer events
5167 // @event baselayerchange: LayersControlEvent
5168 // Fired when the base layer is changed through the [layers control](#control-layers).
5169 // @event overlayadd: LayersControlEvent
5170 // Fired when an overlay is selected through the [layers control](#control-layers).
5171 // @event overlayremove: LayersControlEvent
5172 // Fired when an overlay is deselected through the [layers control](#control-layers).
5173 // @namespace Control.Layers
5174 var type = obj.overlay ?
5175 (e.type === 'add' ? 'overlayadd' : 'overlayremove') :
5176 (e.type === 'add' ? 'baselayerchange' : null);
5177
5178 if (type) {
5179 this._map.fire(type, obj);
5180 }
5181 },
5182
5183 // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see https://stackoverflow.com/a/119079)
5184 _createRadioElement: function (name, checked) {
5185
5186 var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' +
5187 name + '"' + (checked ? ' checked="checked"' : '') + '/>';
5188
5189 var radioFragment = document.createElement('div');
5190 radioFragment.innerHTML = radioHtml;
5191
5192 return radioFragment.firstChild;
5193 },
5194
5195 _addItem: function (obj) {
5196 var label = document.createElement('label'),
5197 checked = this._map.hasLayer(obj.layer),
5198 input;
5199
5200 if (obj.overlay) {
5201 input = document.createElement('input');
5202 input.type = 'checkbox';
5203 input.className = 'leaflet-control-layers-selector';
5204 input.defaultChecked = checked;
5205 } else {
5206 input = this._createRadioElement('leaflet-base-layers_' + stamp(this), checked);
5207 }
5208
5209 this._layerControlInputs.push(input);
5210 input.layerId = stamp(obj.layer);
5211
5212 on(input, 'click', this._onInputClick, this);
5213
5214 var name = document.createElement('span');
5215 name.innerHTML = ' ' + obj.name;
5216
5217 // Helps from preventing layer control flicker when checkboxes are disabled
5218 // https://github.com/Leaflet/Leaflet/issues/2771
5219 var holder = document.createElement('span');
5220
5221 label.appendChild(holder);
5222 holder.appendChild(input);
5223 holder.appendChild(name);
5224
5225 var container = obj.overlay ? this._overlaysList : this._baseLayersList;
5226 container.appendChild(label);
5227
5228 this._checkDisabledLayers();
5229 return label;
5230 },
5231
5232 _onInputClick: function () {
5233 var inputs = this._layerControlInputs,
5234 input, layer;
5235 var addedLayers = [],
5236 removedLayers = [];
5237
5238 this._handlingClick = true;
5239
5240 for (var i = inputs.length - 1; i >= 0; i--) {
5241 input = inputs[i];
5242 layer = this._getLayer(input.layerId).layer;
5243
5244 if (input.checked) {
5245 addedLayers.push(layer);
5246 } else if (!input.checked) {
5247 removedLayers.push(layer);
5248 }
5249 }
5250
5251 // Bugfix issue 2318: Should remove all old layers before readding new ones
5252 for (i = 0; i < removedLayers.length; i++) {
5253 if (this._map.hasLayer(removedLayers[i])) {
5254 this._map.removeLayer(removedLayers[i]);
5255 }
5256 }
5257 for (i = 0; i < addedLayers.length; i++) {
5258 if (!this._map.hasLayer(addedLayers[i])) {
5259 this._map.addLayer(addedLayers[i]);
5260 }
5261 }
5262
5263 this._handlingClick = false;
5264
5265 this._refocusOnMap();
5266 },
5267
5268 _checkDisabledLayers: function () {
5269 var inputs = this._layerControlInputs,
5270 input,
5271 layer,
5272 zoom = this._map.getZoom();
5273
5274 for (var i = inputs.length - 1; i >= 0; i--) {
5275 input = inputs[i];
5276 layer = this._getLayer(input.layerId).layer;
5277 input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) ||
5278 (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom);
5279
5280 }
5281 },
5282
5283 _expandIfNotCollapsed: function () {
5284 if (this._map && !this.options.collapsed) {
5285 this.expand();
5286 }
5287 return this;
5288 }
5289
5290});
5291
5292
5293// @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options)
5294// 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.
5295var layers = function (baseLayers, overlays, options) {
5296 return new Layers(baseLayers, overlays, options);
5297};
5298
5299/*
5300 * @class Control.Zoom
5301 * @aka L.Control.Zoom
5302 * @inherits Control
5303 *
5304 * 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`.
5305 */
5306
5307var Zoom = Control.extend({
5308 // @section
5309 // @aka Control.Zoom options
5310 options: {
5311 position: 'topleft',
5312
5313 // @option zoomInText: String = '<span aria-hidden="true">+</span>'
5314 // The text set on the 'zoom in' button.
5315 zoomInText: '<span aria-hidden="true">+</span>',
5316
5317 // @option zoomInTitle: String = 'Zoom in'
5318 // The title set on the 'zoom in' button.
5319 zoomInTitle: 'Zoom in',
5320
5321 // @option zoomOutText: String = '<span aria-hidden="true">&#x2212;</span>'
5322 // The text set on the 'zoom out' button.
5323 zoomOutText: '<span aria-hidden="true">&#x2212;</span>',
5324
5325 // @option zoomOutTitle: String = 'Zoom out'
5326 // The title set on the 'zoom out' button.
5327 zoomOutTitle: 'Zoom out'
5328 },
5329
5330 onAdd: function (map) {
5331 var zoomName = 'leaflet-control-zoom',
5332 container = create$1('div', zoomName + ' leaflet-bar'),
5333 options = this.options;
5334
5335 this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle,
5336 zoomName + '-in', container, this._zoomIn);
5337 this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle,
5338 zoomName + '-out', container, this._zoomOut);
5339
5340 this._updateDisabled();
5341 map.on('zoomend zoomlevelschange', this._updateDisabled, this);
5342
5343 return container;
5344 },
5345
5346 onRemove: function (map) {
5347 map.off('zoomend zoomlevelschange', this._updateDisabled, this);
5348 },
5349
5350 disable: function () {
5351 this._disabled = true;
5352 this._updateDisabled();
5353 return this;
5354 },
5355
5356 enable: function () {
5357 this._disabled = false;
5358 this._updateDisabled();
5359 return this;
5360 },
5361
5362 _zoomIn: function (e) {
5363 if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) {
5364 this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
5365 }
5366 },
5367
5368 _zoomOut: function (e) {
5369 if (!this._disabled && this._map._zoom > this._map.getMinZoom()) {
5370 this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
5371 }
5372 },
5373
5374 _createButton: function (html, title, className, container, fn) {
5375 var link = create$1('a', className, container);
5376 link.innerHTML = html;
5377 link.href = '#';
5378 link.title = title;
5379
5380 /*
5381 * Will force screen readers like VoiceOver to read this as "Zoom in - button"
5382 */
5383 link.setAttribute('role', 'button');
5384 link.setAttribute('aria-label', title);
5385
5386 disableClickPropagation(link);
5387 on(link, 'click', stop);
5388 on(link, 'click', fn, this);
5389 on(link, 'click', this._refocusOnMap, this);
5390
5391 return link;
5392 },
5393
5394 _updateDisabled: function () {
5395 var map = this._map,
5396 className = 'leaflet-disabled';
5397
5398 removeClass(this._zoomInButton, className);
5399 removeClass(this._zoomOutButton, className);
5400 this._zoomInButton.setAttribute('aria-disabled', 'false');
5401 this._zoomOutButton.setAttribute('aria-disabled', 'false');
5402
5403 if (this._disabled || map._zoom === map.getMinZoom()) {
5404 addClass(this._zoomOutButton, className);
5405 this._zoomOutButton.setAttribute('aria-disabled', 'true');
5406 }
5407 if (this._disabled || map._zoom === map.getMaxZoom()) {
5408 addClass(this._zoomInButton, className);
5409 this._zoomInButton.setAttribute('aria-disabled', 'true');
5410 }
5411 }
5412});
5413
5414// @namespace Map
5415// @section Control options
5416// @option zoomControl: Boolean = true
5417// Whether a [zoom control](#control-zoom) is added to the map by default.
5418Map.mergeOptions({
5419 zoomControl: true
5420});
5421
5422Map.addInitHook(function () {
5423 if (this.options.zoomControl) {
5424 // @section Controls
5425 // @property zoomControl: Control.Zoom
5426 // The default zoom control (only available if the
5427 // [`zoomControl` option](#map-zoomcontrol) was `true` when creating the map).
5428 this.zoomControl = new Zoom();
5429 this.addControl(this.zoomControl);
5430 }
5431});
5432
5433// @namespace Control.Zoom
5434// @factory L.control.zoom(options: Control.Zoom options)
5435// Creates a zoom control
5436var zoom = function (options) {
5437 return new Zoom(options);
5438};
5439
5440/*
5441 * @class Control.Scale
5442 * @aka L.Control.Scale
5443 * @inherits Control
5444 *
5445 * 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`.
5446 *
5447 * @example
5448 *
5449 * ```js
5450 * L.control.scale().addTo(map);
5451 * ```
5452 */
5453
5454var Scale = Control.extend({
5455 // @section
5456 // @aka Control.Scale options
5457 options: {
5458 position: 'bottomleft',
5459
5460 // @option maxWidth: Number = 100
5461 // Maximum width of the control in pixels. The width is set dynamically to show round values (e.g. 100, 200, 500).
5462 maxWidth: 100,
5463
5464 // @option metric: Boolean = True
5465 // Whether to show the metric scale line (m/km).
5466 metric: true,
5467
5468 // @option imperial: Boolean = True
5469 // Whether to show the imperial scale line (mi/ft).
5470 imperial: true
5471
5472 // @option updateWhenIdle: Boolean = false
5473 // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)).
5474 },
5475
5476 onAdd: function (map) {
5477 var className = 'leaflet-control-scale',
5478 container = create$1('div', className),
5479 options = this.options;
5480
5481 this._addScales(options, className + '-line', container);
5482
5483 map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
5484 map.whenReady(this._update, this);
5485
5486 return container;
5487 },
5488
5489 onRemove: function (map) {
5490 map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
5491 },
5492
5493 _addScales: function (options, className, container) {
5494 if (options.metric) {
5495 this._mScale = create$1('div', className, container);
5496 }
5497 if (options.imperial) {
5498 this._iScale = create$1('div', className, container);
5499 }
5500 },
5501
5502 _update: function () {
5503 var map = this._map,
5504 y = map.getSize().y / 2;
5505
5506 var maxMeters = map.distance(
5507 map.containerPointToLatLng([0, y]),
5508 map.containerPointToLatLng([this.options.maxWidth, y]));
5509
5510 this._updateScales(maxMeters);
5511 },
5512
5513 _updateScales: function (maxMeters) {
5514 if (this.options.metric && maxMeters) {
5515 this._updateMetric(maxMeters);
5516 }
5517 if (this.options.imperial && maxMeters) {
5518 this._updateImperial(maxMeters);
5519 }
5520 },
5521
5522 _updateMetric: function (maxMeters) {
5523 var meters = this._getRoundNum(maxMeters),
5524 label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
5525
5526 this._updateScale(this._mScale, label, meters / maxMeters);
5527 },
5528
5529 _updateImperial: function (maxMeters) {
5530 var maxFeet = maxMeters * 3.2808399,
5531 maxMiles, miles, feet;
5532
5533 if (maxFeet > 5280) {
5534 maxMiles = maxFeet / 5280;
5535 miles = this._getRoundNum(maxMiles);
5536 this._updateScale(this._iScale, miles + ' mi', miles / maxMiles);
5537
5538 } else {
5539 feet = this._getRoundNum(maxFeet);
5540 this._updateScale(this._iScale, feet + ' ft', feet / maxFeet);
5541 }
5542 },
5543
5544 _updateScale: function (scale, text, ratio) {
5545 scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px';
5546 scale.innerHTML = text;
5547 },
5548
5549 _getRoundNum: function (num) {
5550 var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
5551 d = num / pow10;
5552
5553 d = d >= 10 ? 10 :
5554 d >= 5 ? 5 :
5555 d >= 3 ? 3 :
5556 d >= 2 ? 2 : 1;
5557
5558 return pow10 * d;
5559 }
5560});
5561
5562
5563// @factory L.control.scale(options?: Control.Scale options)
5564// Creates an scale control with the given options.
5565var scale = function (options) {
5566 return new Scale(options);
5567};
5568
5569var ukrainianFlag = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg>';
5570
5571
5572/*
5573 * @class Control.Attribution
5574 * @aka L.Control.Attribution
5575 * @inherits Control
5576 *
5577 * 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.
5578 */
5579
5580var Attribution = Control.extend({
5581 // @section
5582 // @aka Control.Attribution options
5583 options: {
5584 position: 'bottomright',
5585
5586 // @option prefix: String|false = 'Leaflet'
5587 // The HTML text shown before the attributions. Pass `false` to disable.
5588 prefix: '<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">' + (Browser.inlineSvg ? ukrainianFlag + ' ' : '') + 'Leaflet</a>'
5589 },
5590
5591 initialize: function (options) {
5592 setOptions(this, options);
5593
5594 this._attributions = {};
5595 },
5596
5597 onAdd: function (map) {
5598 map.attributionControl = this;
5599 this._container = create$1('div', 'leaflet-control-attribution');
5600 disableClickPropagation(this._container);
5601
5602 // TODO ugly, refactor
5603 for (var i in map._layers) {
5604 if (map._layers[i].getAttribution) {
5605 this.addAttribution(map._layers[i].getAttribution());
5606 }
5607 }
5608
5609 this._update();
5610
5611 map.on('layeradd', this._addAttribution, this);
5612
5613 return this._container;
5614 },
5615
5616 onRemove: function (map) {
5617 map.off('layeradd', this._addAttribution, this);
5618 },
5619
5620 _addAttribution: function (ev) {
5621 if (ev.layer.getAttribution) {
5622 this.addAttribution(ev.layer.getAttribution());
5623 ev.layer.once('remove', function () {
5624 this.removeAttribution(ev.layer.getAttribution());
5625 }, this);
5626 }
5627 },
5628
5629 // @method setPrefix(prefix: String|false): this
5630 // The HTML text shown before the attributions. Pass `false` to disable.
5631 setPrefix: function (prefix) {
5632 this.options.prefix = prefix;
5633 this._update();
5634 return this;
5635 },
5636
5637 // @method addAttribution(text: String): this
5638 // Adds an attribution text (e.g. `'Vector data &copy; Mapbox'`).
5639 addAttribution: function (text) {
5640 if (!text) { return this; }
5641
5642 if (!this._attributions[text]) {
5643 this._attributions[text] = 0;
5644 }
5645 this._attributions[text]++;
5646
5647 this._update();
5648
5649 return this;
5650 },
5651
5652 // @method removeAttribution(text: String): this
5653 // Removes an attribution text.
5654 removeAttribution: function (text) {
5655 if (!text) { return this; }
5656
5657 if (this._attributions[text]) {
5658 this._attributions[text]--;
5659 this._update();
5660 }
5661
5662 return this;
5663 },
5664
5665 _update: function () {
5666 if (!this._map) { return; }
5667
5668 var attribs = [];
5669
5670 for (var i in this._attributions) {
5671 if (this._attributions[i]) {
5672 attribs.push(i);
5673 }
5674 }
5675
5676 var prefixAndAttribs = [];
5677
5678 if (this.options.prefix) {
5679 prefixAndAttribs.push(this.options.prefix);
5680 }
5681 if (attribs.length) {
5682 prefixAndAttribs.push(attribs.join(', '));
5683 }
5684
5685 this._container.innerHTML = prefixAndAttribs.join(' <span aria-hidden="true">|</span> ');
5686 }
5687});
5688
5689// @namespace Map
5690// @section Control options
5691// @option attributionControl: Boolean = true
5692// Whether a [attribution control](#control-attribution) is added to the map by default.
5693Map.mergeOptions({
5694 attributionControl: true
5695});
5696
5697Map.addInitHook(function () {
5698 if (this.options.attributionControl) {
5699 new Attribution().addTo(this);
5700 }
5701});
5702
5703// @namespace Control.Attribution
5704// @factory L.control.attribution(options: Control.Attribution options)
5705// Creates an attribution control.
5706var attribution = function (options) {
5707 return new Attribution(options);
5708};
5709
5710Control.Layers = Layers;
5711Control.Zoom = Zoom;
5712Control.Scale = Scale;
5713Control.Attribution = Attribution;
5714
5715control.layers = layers;
5716control.zoom = zoom;
5717control.scale = scale;
5718control.attribution = attribution;
5719
5720/*
5721 L.Handler is a base class for handler classes that are used internally to inject
5722 interaction features like dragging to classes like Map and Marker.
5723*/
5724
5725// @class Handler
5726// @aka L.Handler
5727// Abstract class for map interaction handlers
5728
5729var Handler = Class.extend({
5730 initialize: function (map) {
5731 this._map = map;
5732 },
5733
5734 // @method enable(): this
5735 // Enables the handler
5736 enable: function () {
5737 if (this._enabled) { return this; }
5738
5739 this._enabled = true;
5740 this.addHooks();
5741 return this;
5742 },
5743
5744 // @method disable(): this
5745 // Disables the handler
5746 disable: function () {
5747 if (!this._enabled) { return this; }
5748
5749 this._enabled = false;
5750 this.removeHooks();
5751 return this;
5752 },
5753
5754 // @method enabled(): Boolean
5755 // Returns `true` if the handler is enabled
5756 enabled: function () {
5757 return !!this._enabled;
5758 }
5759
5760 // @section Extension methods
5761 // Classes inheriting from `Handler` must implement the two following methods:
5762 // @method addHooks()
5763 // Called when the handler is enabled, should add event hooks.
5764 // @method removeHooks()
5765 // Called when the handler is disabled, should remove the event hooks added previously.
5766});
5767
5768// @section There is static function which can be called without instantiating L.Handler:
5769// @function addTo(map: Map, name: String): this
5770// Adds a new Handler to the given map with the given name.
5771Handler.addTo = function (map, name) {
5772 map.addHandler(name, this);
5773 return this;
5774};
5775
5776var Mixin = {Events: Events};
5777
5778/*
5779 * @class Draggable
5780 * @aka L.Draggable
5781 * @inherits Evented
5782 *
5783 * A class for making DOM elements draggable (including touch support).
5784 * Used internally for map and marker dragging. Only works for elements
5785 * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition).
5786 *
5787 * @example
5788 * ```js
5789 * var draggable = new L.Draggable(elementToDrag);
5790 * draggable.enable();
5791 * ```
5792 */
5793
5794var START = Browser.touch ? 'touchstart mousedown' : 'mousedown';
5795
5796var Draggable = Evented.extend({
5797
5798 options: {
5799 // @section
5800 // @aka Draggable options
5801 // @option clickTolerance: Number = 3
5802 // The max number of pixels a user can shift the mouse pointer during a click
5803 // for it to be considered a valid click (as opposed to a mouse drag).
5804 clickTolerance: 3
5805 },
5806
5807 // @constructor L.Draggable(el: HTMLElement, dragHandle?: HTMLElement, preventOutline?: Boolean, options?: Draggable options)
5808 // Creates a `Draggable` object for moving `el` when you start dragging the `dragHandle` element (equals `el` itself by default).
5809 initialize: function (element, dragStartTarget, preventOutline, options) {
5810 setOptions(this, options);
5811
5812 this._element = element;
5813 this._dragStartTarget = dragStartTarget || element;
5814 this._preventOutline = preventOutline;
5815 },
5816
5817 // @method enable()
5818 // Enables the dragging ability
5819 enable: function () {
5820 if (this._enabled) { return; }
5821
5822 on(this._dragStartTarget, START, this._onDown, this);
5823
5824 this._enabled = true;
5825 },
5826
5827 // @method disable()
5828 // Disables the dragging ability
5829 disable: function () {
5830 if (!this._enabled) { return; }
5831
5832 // If we're currently dragging this draggable,
5833 // disabling it counts as first ending the drag.
5834 if (Draggable._dragging === this) {
5835 this.finishDrag(true);
5836 }
5837
5838 off(this._dragStartTarget, START, this._onDown, this);
5839
5840 this._enabled = false;
5841 this._moved = false;
5842 },
5843
5844 _onDown: function (e) {
5845 // Ignore the event if disabled; this happens in IE11
5846 // under some circumstances, see #3666.
5847 if (!this._enabled) { return; }
5848
5849 this._moved = false;
5850
5851 if (hasClass(this._element, 'leaflet-zoom-anim')) { return; }
5852
5853 if (e.touches && e.touches.length !== 1) {
5854 // Finish dragging to avoid conflict with touchZoom
5855 if (Draggable._dragging === this) {
5856 this.finishDrag();
5857 }
5858 return;
5859 }
5860
5861 if (Draggable._dragging || e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; }
5862 Draggable._dragging = this; // Prevent dragging multiple objects at once.
5863
5864 if (this._preventOutline) {
5865 preventOutline(this._element);
5866 }
5867
5868 disableImageDrag();
5869 disableTextSelection();
5870
5871 if (this._moving) { return; }
5872
5873 // @event down: Event
5874 // Fired when a drag is about to start.
5875 this.fire('down');
5876
5877 var first = e.touches ? e.touches[0] : e,
5878 sizedParent = getSizedParentNode(this._element);
5879
5880 this._startPoint = new Point(first.clientX, first.clientY);
5881 this._startPos = getPosition(this._element);
5882
5883 // Cache the scale, so that we can continuously compensate for it during drag (_onMove).
5884 this._parentScale = getScale(sizedParent);
5885
5886 var mouseevent = e.type === 'mousedown';
5887 on(document, mouseevent ? 'mousemove' : 'touchmove', this._onMove, this);
5888 on(document, mouseevent ? 'mouseup' : 'touchend touchcancel', this._onUp, this);
5889 },
5890
5891 _onMove: function (e) {
5892 // Ignore the event if disabled; this happens in IE11
5893 // under some circumstances, see #3666.
5894 if (!this._enabled) { return; }
5895
5896 if (e.touches && e.touches.length > 1) {
5897 this._moved = true;
5898 return;
5899 }
5900
5901 var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
5902 offset = new Point(first.clientX, first.clientY)._subtract(this._startPoint);
5903
5904 if (!offset.x && !offset.y) { return; }
5905 if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) { return; }
5906
5907 // We assume that the parent container's position, border and scale do not change for the duration of the drag.
5908 // Therefore there is no need to account for the position and border (they are eliminated by the subtraction)
5909 // and we can use the cached value for the scale.
5910 offset.x /= this._parentScale.x;
5911 offset.y /= this._parentScale.y;
5912
5913 preventDefault(e);
5914
5915 if (!this._moved) {
5916 // @event dragstart: Event
5917 // Fired when a drag starts
5918 this.fire('dragstart');
5919
5920 this._moved = true;
5921
5922 addClass(document.body, 'leaflet-dragging');
5923
5924 this._lastTarget = e.target || e.srcElement;
5925 // IE and Edge do not give the <use> element, so fetch it
5926 // if necessary
5927 if (window.SVGElementInstance && this._lastTarget instanceof window.SVGElementInstance) {
5928 this._lastTarget = this._lastTarget.correspondingUseElement;
5929 }
5930 addClass(this._lastTarget, 'leaflet-drag-target');
5931 }
5932
5933 this._newPos = this._startPos.add(offset);
5934 this._moving = true;
5935
5936 this._lastEvent = e;
5937 this._updatePosition();
5938 },
5939
5940 _updatePosition: function () {
5941 var e = {originalEvent: this._lastEvent};
5942
5943 // @event predrag: Event
5944 // Fired continuously during dragging *before* each corresponding
5945 // update of the element's position.
5946 this.fire('predrag', e);
5947 setPosition(this._element, this._newPos);
5948
5949 // @event drag: Event
5950 // Fired continuously during dragging.
5951 this.fire('drag', e);
5952 },
5953
5954 _onUp: function () {
5955 // Ignore the event if disabled; this happens in IE11
5956 // under some circumstances, see #3666.
5957 if (!this._enabled) { return; }
5958 this.finishDrag();
5959 },
5960
5961 finishDrag: function (noInertia) {
5962 removeClass(document.body, 'leaflet-dragging');
5963
5964 if (this._lastTarget) {
5965 removeClass(this._lastTarget, 'leaflet-drag-target');
5966 this._lastTarget = null;
5967 }
5968
5969 off(document, 'mousemove touchmove', this._onMove, this);
5970 off(document, 'mouseup touchend touchcancel', this._onUp, this);
5971
5972 enableImageDrag();
5973 enableTextSelection();
5974
5975 if (this._moved && this._moving) {
5976
5977 // @event dragend: DragEndEvent
5978 // Fired when the drag ends.
5979 this.fire('dragend', {
5980 noInertia: noInertia,
5981 distance: this._newPos.distanceTo(this._startPos)
5982 });
5983 }
5984
5985 this._moving = false;
5986 Draggable._dragging = false;
5987 }
5988
5989});
5990
5991/*
5992 * @namespace LineUtil
5993 *
5994 * Various utility functions for polyline points processing, used by Leaflet internally to make polylines lightning-fast.
5995 */
5996
5997// Simplify polyline with vertex reduction and Douglas-Peucker simplification.
5998// Improves rendering performance dramatically by lessening the number of points to draw.
5999
6000// @function simplify(points: Point[], tolerance: Number): Point[]
6001// Dramatically reduces the number of points in a polyline while retaining
6002// its shape and returns a new array of simplified points, using the
6003// [Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm).
6004// Used for a huge performance boost when processing/displaying Leaflet polylines for
6005// each zoom level and also reducing visual noise. tolerance affects the amount of
6006// simplification (lesser value means higher quality but slower and with more points).
6007// Also released as a separated micro-library [Simplify.js](https://mourner.github.io/simplify-js/).
6008function simplify(points, tolerance) {
6009 if (!tolerance || !points.length) {
6010 return points.slice();
6011 }
6012
6013 var sqTolerance = tolerance * tolerance;
6014
6015 // stage 1: vertex reduction
6016 points = _reducePoints(points, sqTolerance);
6017
6018 // stage 2: Douglas-Peucker simplification
6019 points = _simplifyDP(points, sqTolerance);
6020
6021 return points;
6022}
6023
6024// @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number
6025// Returns the distance between point `p` and segment `p1` to `p2`.
6026function pointToSegmentDistance(p, p1, p2) {
6027 return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true));
6028}
6029
6030// @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number
6031// Returns the closest point from a point `p` on a segment `p1` to `p2`.
6032function closestPointOnSegment(p, p1, p2) {
6033 return _sqClosestPointOnSegment(p, p1, p2);
6034}
6035
6036// Ramer-Douglas-Peucker simplification, see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
6037function _simplifyDP(points, sqTolerance) {
6038
6039 var len = points.length,
6040 ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array,
6041 markers = new ArrayConstructor(len);
6042
6043 markers[0] = markers[len - 1] = 1;
6044
6045 _simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
6046
6047 var i,
6048 newPoints = [];
6049
6050 for (i = 0; i < len; i++) {
6051 if (markers[i]) {
6052 newPoints.push(points[i]);
6053 }
6054 }
6055
6056 return newPoints;
6057}
6058
6059function _simplifyDPStep(points, markers, sqTolerance, first, last) {
6060
6061 var maxSqDist = 0,
6062 index, i, sqDist;
6063
6064 for (i = first + 1; i <= last - 1; i++) {
6065 sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true);
6066
6067 if (sqDist > maxSqDist) {
6068 index = i;
6069 maxSqDist = sqDist;
6070 }
6071 }
6072
6073 if (maxSqDist > sqTolerance) {
6074 markers[index] = 1;
6075
6076 _simplifyDPStep(points, markers, sqTolerance, first, index);
6077 _simplifyDPStep(points, markers, sqTolerance, index, last);
6078 }
6079}
6080
6081// reduce points that are too close to each other to a single point
6082function _reducePoints(points, sqTolerance) {
6083 var reducedPoints = [points[0]];
6084
6085 for (var i = 1, prev = 0, len = points.length; i < len; i++) {
6086 if (_sqDist(points[i], points[prev]) > sqTolerance) {
6087 reducedPoints.push(points[i]);
6088 prev = i;
6089 }
6090 }
6091 if (prev < len - 1) {
6092 reducedPoints.push(points[len - 1]);
6093 }
6094 return reducedPoints;
6095}
6096
6097var _lastCode;
6098
6099// @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean
6100// Clips the segment a to b by rectangular bounds with the
6101// [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm)
6102// (modifying the segment points directly!). Used by Leaflet to only show polyline
6103// points that are on the screen or near, increasing performance.
6104function clipSegment(a, b, bounds, useLastCode, round) {
6105 var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds),
6106 codeB = _getBitCode(b, bounds),
6107
6108 codeOut, p, newCode;
6109
6110 // save 2nd code to avoid calculating it on the next segment
6111 _lastCode = codeB;
6112
6113 while (true) {
6114 // if a,b is inside the clip window (trivial accept)
6115 if (!(codeA | codeB)) {
6116 return [a, b];
6117 }
6118
6119 // if a,b is outside the clip window (trivial reject)
6120 if (codeA & codeB) {
6121 return false;
6122 }
6123
6124 // other cases
6125 codeOut = codeA || codeB;
6126 p = _getEdgeIntersection(a, b, codeOut, bounds, round);
6127 newCode = _getBitCode(p, bounds);
6128
6129 if (codeOut === codeA) {
6130 a = p;
6131 codeA = newCode;
6132 } else {
6133 b = p;
6134 codeB = newCode;
6135 }
6136 }
6137}
6138
6139function _getEdgeIntersection(a, b, code, bounds, round) {
6140 var dx = b.x - a.x,
6141 dy = b.y - a.y,
6142 min = bounds.min,
6143 max = bounds.max,
6144 x, y;
6145
6146 if (code & 8) { // top
6147 x = a.x + dx * (max.y - a.y) / dy;
6148 y = max.y;
6149
6150 } else if (code & 4) { // bottom
6151 x = a.x + dx * (min.y - a.y) / dy;
6152 y = min.y;
6153
6154 } else if (code & 2) { // right
6155 x = max.x;
6156 y = a.y + dy * (max.x - a.x) / dx;
6157
6158 } else if (code & 1) { // left
6159 x = min.x;
6160 y = a.y + dy * (min.x - a.x) / dx;
6161 }
6162
6163 return new Point(x, y, round);
6164}
6165
6166function _getBitCode(p, bounds) {
6167 var code = 0;
6168
6169 if (p.x < bounds.min.x) { // left
6170 code |= 1;
6171 } else if (p.x > bounds.max.x) { // right
6172 code |= 2;
6173 }
6174
6175 if (p.y < bounds.min.y) { // bottom
6176 code |= 4;
6177 } else if (p.y > bounds.max.y) { // top
6178 code |= 8;
6179 }
6180
6181 return code;
6182}
6183
6184// square distance (to avoid unnecessary Math.sqrt calls)
6185function _sqDist(p1, p2) {
6186 var dx = p2.x - p1.x,
6187 dy = p2.y - p1.y;
6188 return dx * dx + dy * dy;
6189}
6190
6191// return closest point on segment or distance to that point
6192function _sqClosestPointOnSegment(p, p1, p2, sqDist) {
6193 var x = p1.x,
6194 y = p1.y,
6195 dx = p2.x - x,
6196 dy = p2.y - y,
6197 dot = dx * dx + dy * dy,
6198 t;
6199
6200 if (dot > 0) {
6201 t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
6202
6203 if (t > 1) {
6204 x = p2.x;
6205 y = p2.y;
6206 } else if (t > 0) {
6207 x += dx * t;
6208 y += dy * t;
6209 }
6210 }
6211
6212 dx = p.x - x;
6213 dy = p.y - y;
6214
6215 return sqDist ? dx * dx + dy * dy : new Point(x, y);
6216}
6217
6218
6219// @function isFlat(latlngs: LatLng[]): Boolean
6220// Returns true if `latlngs` is a flat array, false is nested.
6221function isFlat(latlngs) {
6222 return !isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
6223}
6224
6225function _flat(latlngs) {
6226 console.warn('Deprecated use of _flat, please use L.LineUtil.isFlat instead.');
6227 return isFlat(latlngs);
6228}
6229
6230var LineUtil = {
6231 __proto__: null,
6232 simplify: simplify,
6233 pointToSegmentDistance: pointToSegmentDistance,
6234 closestPointOnSegment: closestPointOnSegment,
6235 clipSegment: clipSegment,
6236 _getEdgeIntersection: _getEdgeIntersection,
6237 _getBitCode: _getBitCode,
6238 _sqClosestPointOnSegment: _sqClosestPointOnSegment,
6239 isFlat: isFlat,
6240 _flat: _flat
6241};
6242
6243/*
6244 * @namespace PolyUtil
6245 * Various utility functions for polygon geometries.
6246 */
6247
6248/* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[]
6249 * 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)).
6250 * Used by Leaflet to only show polygon points that are on the screen or near, increasing
6251 * performance. Note that polygon points needs different algorithm for clipping
6252 * than polyline, so there's a separate method for it.
6253 */
6254function clipPolygon(points, bounds, round) {
6255 var clippedPoints,
6256 edges = [1, 4, 2, 8],
6257 i, j, k,
6258 a, b,
6259 len, edge, p;
6260
6261 for (i = 0, len = points.length; i < len; i++) {
6262 points[i]._code = _getBitCode(points[i], bounds);
6263 }
6264
6265 // for each edge (left, bottom, right, top)
6266 for (k = 0; k < 4; k++) {
6267 edge = edges[k];
6268 clippedPoints = [];
6269
6270 for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
6271 a = points[i];
6272 b = points[j];
6273
6274 // if a is inside the clip window
6275 if (!(a._code & edge)) {
6276 // if b is outside the clip window (a->b goes out of screen)
6277 if (b._code & edge) {
6278 p = _getEdgeIntersection(b, a, edge, bounds, round);
6279 p._code = _getBitCode(p, bounds);
6280 clippedPoints.push(p);
6281 }
6282 clippedPoints.push(a);
6283
6284 // else if b is inside the clip window (a->b enters the screen)
6285 } else if (!(b._code & edge)) {
6286 p = _getEdgeIntersection(b, a, edge, bounds, round);
6287 p._code = _getBitCode(p, bounds);
6288 clippedPoints.push(p);
6289 }
6290 }
6291 points = clippedPoints;
6292 }
6293
6294 return points;
6295}
6296
6297var PolyUtil = {
6298 __proto__: null,
6299 clipPolygon: clipPolygon
6300};
6301
6302/*
6303 * @namespace Projection
6304 * @section
6305 * Leaflet comes with a set of already defined Projections out of the box:
6306 *
6307 * @projection L.Projection.LonLat
6308 *
6309 * Equirectangular, or Plate Carree projection — the most simple projection,
6310 * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as
6311 * latitude. Also suitable for flat worlds, e.g. game maps. Used by the
6312 * `EPSG:4326` and `Simple` CRS.
6313 */
6314
6315var LonLat = {
6316 project: function (latlng) {
6317 return new Point(latlng.lng, latlng.lat);
6318 },
6319
6320 unproject: function (point) {
6321 return new LatLng(point.y, point.x);
6322 },
6323
6324 bounds: new Bounds([-180, -90], [180, 90])
6325};
6326
6327/*
6328 * @namespace Projection
6329 * @projection L.Projection.Mercator
6330 *
6331 * Elliptical Mercator projection — more complex than Spherical Mercator. Assumes that Earth is an ellipsoid. Used by the EPSG:3395 CRS.
6332 */
6333
6334var Mercator = {
6335 R: 6378137,
6336 R_MINOR: 6356752.314245179,
6337
6338 bounds: new Bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]),
6339
6340 project: function (latlng) {
6341 var d = Math.PI / 180,
6342 r = this.R,
6343 y = latlng.lat * d,
6344 tmp = this.R_MINOR / r,
6345 e = Math.sqrt(1 - tmp * tmp),
6346 con = e * Math.sin(y);
6347
6348 var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2);
6349 y = -r * Math.log(Math.max(ts, 1E-10));
6350
6351 return new Point(latlng.lng * d * r, y);
6352 },
6353
6354 unproject: function (point) {
6355 var d = 180 / Math.PI,
6356 r = this.R,
6357 tmp = this.R_MINOR / r,
6358 e = Math.sqrt(1 - tmp * tmp),
6359 ts = Math.exp(-point.y / r),
6360 phi = Math.PI / 2 - 2 * Math.atan(ts);
6361
6362 for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) {
6363 con = e * Math.sin(phi);
6364 con = Math.pow((1 - con) / (1 + con), e / 2);
6365 dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi;
6366 phi += dphi;
6367 }
6368
6369 return new LatLng(phi * d, point.x * d / r);
6370 }
6371};
6372
6373/*
6374 * @class Projection
6375
6376 * An object with methods for projecting geographical coordinates of the world onto
6377 * a flat surface (and back). See [Map projection](https://en.wikipedia.org/wiki/Map_projection).
6378
6379 * @property bounds: Bounds
6380 * The bounds (specified in CRS units) where the projection is valid
6381
6382 * @method project(latlng: LatLng): Point
6383 * Projects geographical coordinates into a 2D point.
6384 * Only accepts actual `L.LatLng` instances, not arrays.
6385
6386 * @method unproject(point: Point): LatLng
6387 * The inverse of `project`. Projects a 2D point into a geographical location.
6388 * Only accepts actual `L.Point` instances, not arrays.
6389
6390 * Note that the projection instances do not inherit from Leaflet's `Class` object,
6391 * and can't be instantiated. Also, new classes can't inherit from them,
6392 * and methods can't be added to them with the `include` function.
6393
6394 */
6395
6396var index = {
6397 __proto__: null,
6398 LonLat: LonLat,
6399 Mercator: Mercator,
6400 SphericalMercator: SphericalMercator
6401};
6402
6403/*
6404 * @namespace CRS
6405 * @crs L.CRS.EPSG3395
6406 *
6407 * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection.
6408 */
6409var EPSG3395 = extend({}, Earth, {
6410 code: 'EPSG:3395',
6411 projection: Mercator,
6412
6413 transformation: (function () {
6414 var scale = 0.5 / (Math.PI * Mercator.R);
6415 return toTransformation(scale, 0.5, -scale, 0.5);
6416 }())
6417});
6418
6419/*
6420 * @namespace CRS
6421 * @crs L.CRS.EPSG4326
6422 *
6423 * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection.
6424 *
6425 * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic),
6426 * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer`
6427 * with this CRS, ensure that there are two 256x256 pixel tiles covering the
6428 * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90),
6429 * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set.
6430 */
6431
6432var EPSG4326 = extend({}, Earth, {
6433 code: 'EPSG:4326',
6434 projection: LonLat,
6435 transformation: toTransformation(1 / 180, 1, -1 / 180, 0.5)
6436});
6437
6438/*
6439 * @namespace CRS
6440 * @crs L.CRS.Simple
6441 *
6442 * A simple CRS that maps longitude and latitude into `x` and `y` directly.
6443 * May be used for maps of flat surfaces (e.g. game maps). Note that the `y`
6444 * axis should still be inverted (going from bottom to top). `distance()` returns
6445 * simple euclidean distance.
6446 */
6447
6448var Simple = extend({}, CRS, {
6449 projection: LonLat,
6450 transformation: toTransformation(1, 0, -1, 0),
6451
6452 scale: function (zoom) {
6453 return Math.pow(2, zoom);
6454 },
6455
6456 zoom: function (scale) {
6457 return Math.log(scale) / Math.LN2;
6458 },
6459
6460 distance: function (latlng1, latlng2) {
6461 var dx = latlng2.lng - latlng1.lng,
6462 dy = latlng2.lat - latlng1.lat;
6463
6464 return Math.sqrt(dx * dx + dy * dy);
6465 },
6466
6467 infinite: true
6468});
6469
6470CRS.Earth = Earth;
6471CRS.EPSG3395 = EPSG3395;
6472CRS.EPSG3857 = EPSG3857;
6473CRS.EPSG900913 = EPSG900913;
6474CRS.EPSG4326 = EPSG4326;
6475CRS.Simple = Simple;
6476
6477/*
6478 * @class Layer
6479 * @inherits Evented
6480 * @aka L.Layer
6481 * @aka ILayer
6482 *
6483 * A set of methods from the Layer base class that all Leaflet layers use.
6484 * Inherits all methods, options and events from `L.Evented`.
6485 *
6486 * @example
6487 *
6488 * ```js
6489 * var layer = L.marker(latlng).addTo(map);
6490 * layer.addTo(map);
6491 * layer.remove();
6492 * ```
6493 *
6494 * @event add: Event
6495 * Fired after the layer is added to a map
6496 *
6497 * @event remove: Event
6498 * Fired after the layer is removed from a map
6499 */
6500
6501
6502var Layer = Evented.extend({
6503
6504 // Classes extending `L.Layer` will inherit the following options:
6505 options: {
6506 // @option pane: String = 'overlayPane'
6507 // 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.
6508 pane: 'overlayPane',
6509
6510 // @option attribution: String = null
6511 // 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.
6512 attribution: null,
6513
6514 bubblingMouseEvents: true
6515 },
6516
6517 /* @section
6518 * Classes extending `L.Layer` will inherit the following methods:
6519 *
6520 * @method addTo(map: Map|LayerGroup): this
6521 * Adds the layer to the given map or layer group.
6522 */
6523 addTo: function (map) {
6524 map.addLayer(this);
6525 return this;
6526 },
6527
6528 // @method remove: this
6529 // Removes the layer from the map it is currently active on.
6530 remove: function () {
6531 return this.removeFrom(this._map || this._mapToAdd);
6532 },
6533
6534 // @method removeFrom(map: Map): this
6535 // Removes the layer from the given map
6536 //
6537 // @alternative
6538 // @method removeFrom(group: LayerGroup): this
6539 // Removes the layer from the given `LayerGroup`
6540 removeFrom: function (obj) {
6541 if (obj) {
6542 obj.removeLayer(this);
6543 }
6544 return this;
6545 },
6546
6547 // @method getPane(name? : String): HTMLElement
6548 // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer.
6549 getPane: function (name) {
6550 return this._map.getPane(name ? (this.options[name] || name) : this.options.pane);
6551 },
6552
6553 addInteractiveTarget: function (targetEl) {
6554 this._map._targets[stamp(targetEl)] = this;
6555 return this;
6556 },
6557
6558 removeInteractiveTarget: function (targetEl) {
6559 delete this._map._targets[stamp(targetEl)];
6560 return this;
6561 },
6562
6563 // @method getAttribution: String
6564 // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
6565 getAttribution: function () {
6566 return this.options.attribution;
6567 },
6568
6569 _layerAdd: function (e) {
6570 var map = e.target;
6571
6572 // check in case layer gets added and then removed before the map is ready
6573 if (!map.hasLayer(this)) { return; }
6574
6575 this._map = map;
6576 this._zoomAnimated = map._zoomAnimated;
6577
6578 if (this.getEvents) {
6579 var events = this.getEvents();
6580 map.on(events, this);
6581 this.once('remove', function () {
6582 map.off(events, this);
6583 }, this);
6584 }
6585
6586 this.onAdd(map);
6587
6588 this.fire('add');
6589 map.fire('layeradd', {layer: this});
6590 }
6591});
6592
6593/* @section Extension methods
6594 * @uninheritable
6595 *
6596 * Every layer should extend from `L.Layer` and (re-)implement the following methods.
6597 *
6598 * @method onAdd(map: Map): this
6599 * 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).
6600 *
6601 * @method onRemove(map: Map): this
6602 * 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).
6603 *
6604 * @method getEvents(): Object
6605 * 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.
6606 *
6607 * @method getAttribution(): String
6608 * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.
6609 *
6610 * @method beforeAdd(map: Map): this
6611 * 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.
6612 */
6613
6614
6615/* @namespace Map
6616 * @section Layer events
6617 *
6618 * @event layeradd: LayerEvent
6619 * Fired when a new layer is added to the map.
6620 *
6621 * @event layerremove: LayerEvent
6622 * Fired when some layer is removed from the map
6623 *
6624 * @section Methods for Layers and Controls
6625 */
6626Map.include({
6627 // @method addLayer(layer: Layer): this
6628 // Adds the given layer to the map
6629 addLayer: function (layer) {
6630 if (!layer._layerAdd) {
6631 throw new Error('The provided object is not a Layer.');
6632 }
6633
6634 var id = stamp(layer);
6635 if (this._layers[id]) { return this; }
6636 this._layers[id] = layer;
6637
6638 layer._mapToAdd = this;
6639
6640 if (layer.beforeAdd) {
6641 layer.beforeAdd(this);
6642 }
6643
6644 this.whenReady(layer._layerAdd, layer);
6645
6646 return this;
6647 },
6648
6649 // @method removeLayer(layer: Layer): this
6650 // Removes the given layer from the map.
6651 removeLayer: function (layer) {
6652 var id = stamp(layer);
6653
6654 if (!this._layers[id]) { return this; }
6655
6656 if (this._loaded) {
6657 layer.onRemove(this);
6658 }
6659
6660 delete this._layers[id];
6661
6662 if (this._loaded) {
6663 this.fire('layerremove', {layer: layer});
6664 layer.fire('remove');
6665 }
6666
6667 layer._map = layer._mapToAdd = null;
6668
6669 return this;
6670 },
6671
6672 // @method hasLayer(layer: Layer): Boolean
6673 // Returns `true` if the given layer is currently added to the map
6674 hasLayer: function (layer) {
6675 return stamp(layer) in this._layers;
6676 },
6677
6678 /* @method eachLayer(fn: Function, context?: Object): this
6679 * Iterates over the layers of the map, optionally specifying context of the iterator function.
6680 * ```
6681 * map.eachLayer(function(layer){
6682 * layer.bindPopup('Hello');
6683 * });
6684 * ```
6685 */
6686 eachLayer: function (method, context) {
6687 for (var i in this._layers) {
6688 method.call(context, this._layers[i]);
6689 }
6690 return this;
6691 },
6692
6693 _addLayers: function (layers) {
6694 layers = layers ? (isArray(layers) ? layers : [layers]) : [];
6695
6696 for (var i = 0, len = layers.length; i < len; i++) {
6697 this.addLayer(layers[i]);
6698 }
6699 },
6700
6701 _addZoomLimit: function (layer) {
6702 if (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {
6703 this._zoomBoundLayers[stamp(layer)] = layer;
6704 this._updateZoomLevels();
6705 }
6706 },
6707
6708 _removeZoomLimit: function (layer) {
6709 var id = stamp(layer);
6710
6711 if (this._zoomBoundLayers[id]) {
6712 delete this._zoomBoundLayers[id];
6713 this._updateZoomLevels();
6714 }
6715 },
6716
6717 _updateZoomLevels: function () {
6718 var minZoom = Infinity,
6719 maxZoom = -Infinity,
6720 oldZoomSpan = this._getZoomSpan();
6721
6722 for (var i in this._zoomBoundLayers) {
6723 var options = this._zoomBoundLayers[i].options;
6724
6725 minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom);
6726 maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom);
6727 }
6728
6729 this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom;
6730 this._layersMinZoom = minZoom === Infinity ? undefined : minZoom;
6731
6732 // @section Map state change events
6733 // @event zoomlevelschange: Event
6734 // Fired when the number of zoomlevels on the map is changed due
6735 // to adding or removing a layer.
6736 if (oldZoomSpan !== this._getZoomSpan()) {
6737 this.fire('zoomlevelschange');
6738 }
6739
6740 if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
6741 this.setZoom(this._layersMaxZoom);
6742 }
6743 if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
6744 this.setZoom(this._layersMinZoom);
6745 }
6746 }
6747});
6748
6749/*
6750 * @class LayerGroup
6751 * @aka L.LayerGroup
6752 * @inherits Interactive layer
6753 *
6754 * Used to group several layers and handle them as one. If you add it to the map,
6755 * any layers added or removed from the group will be added/removed on the map as
6756 * well. Extends `Layer`.
6757 *
6758 * @example
6759 *
6760 * ```js
6761 * L.layerGroup([marker1, marker2])
6762 * .addLayer(polyline)
6763 * .addTo(map);
6764 * ```
6765 */
6766
6767var LayerGroup = Layer.extend({
6768
6769 initialize: function (layers, options) {
6770 setOptions(this, options);
6771
6772 this._layers = {};
6773
6774 var i, len;
6775
6776 if (layers) {
6777 for (i = 0, len = layers.length; i < len; i++) {
6778 this.addLayer(layers[i]);
6779 }
6780 }
6781 },
6782
6783 // @method addLayer(layer: Layer): this
6784 // Adds the given layer to the group.
6785 addLayer: function (layer) {
6786 var id = this.getLayerId(layer);
6787
6788 this._layers[id] = layer;
6789
6790 if (this._map) {
6791 this._map.addLayer(layer);
6792 }
6793
6794 return this;
6795 },
6796
6797 // @method removeLayer(layer: Layer): this
6798 // Removes the given layer from the group.
6799 // @alternative
6800 // @method removeLayer(id: Number): this
6801 // Removes the layer with the given internal ID from the group.
6802 removeLayer: function (layer) {
6803 var id = layer in this._layers ? layer : this.getLayerId(layer);
6804
6805 if (this._map && this._layers[id]) {
6806 this._map.removeLayer(this._layers[id]);
6807 }
6808
6809 delete this._layers[id];
6810
6811 return this;
6812 },
6813
6814 // @method hasLayer(layer: Layer): Boolean
6815 // Returns `true` if the given layer is currently added to the group.
6816 // @alternative
6817 // @method hasLayer(id: Number): Boolean
6818 // Returns `true` if the given internal ID is currently added to the group.
6819 hasLayer: function (layer) {
6820 var layerId = typeof layer === 'number' ? layer : this.getLayerId(layer);
6821 return layerId in this._layers;
6822 },
6823
6824 // @method clearLayers(): this
6825 // Removes all the layers from the group.
6826 clearLayers: function () {
6827 return this.eachLayer(this.removeLayer, this);
6828 },
6829
6830 // @method invoke(methodName: String, …): this
6831 // Calls `methodName` on every layer contained in this group, passing any
6832 // additional parameters. Has no effect if the layers contained do not
6833 // implement `methodName`.
6834 invoke: function (methodName) {
6835 var args = Array.prototype.slice.call(arguments, 1),
6836 i, layer;
6837
6838 for (i in this._layers) {
6839 layer = this._layers[i];
6840
6841 if (layer[methodName]) {
6842 layer[methodName].apply(layer, args);
6843 }
6844 }
6845
6846 return this;
6847 },
6848
6849 onAdd: function (map) {
6850 this.eachLayer(map.addLayer, map);
6851 },
6852
6853 onRemove: function (map) {
6854 this.eachLayer(map.removeLayer, map);
6855 },
6856
6857 // @method eachLayer(fn: Function, context?: Object): this
6858 // Iterates over the layers of the group, optionally specifying context of the iterator function.
6859 // ```js
6860 // group.eachLayer(function (layer) {
6861 // layer.bindPopup('Hello');
6862 // });
6863 // ```
6864 eachLayer: function (method, context) {
6865 for (var i in this._layers) {
6866 method.call(context, this._layers[i]);
6867 }
6868 return this;
6869 },
6870
6871 // @method getLayer(id: Number): Layer
6872 // Returns the layer with the given internal ID.
6873 getLayer: function (id) {
6874 return this._layers[id];
6875 },
6876
6877 // @method getLayers(): Layer[]
6878 // Returns an array of all the layers added to the group.
6879 getLayers: function () {
6880 var layers = [];
6881 this.eachLayer(layers.push, layers);
6882 return layers;
6883 },
6884
6885 // @method setZIndex(zIndex: Number): this
6886 // Calls `setZIndex` on every layer contained in this group, passing the z-index.
6887 setZIndex: function (zIndex) {
6888 return this.invoke('setZIndex', zIndex);
6889 },
6890
6891 // @method getLayerId(layer: Layer): Number
6892 // Returns the internal ID for a layer
6893 getLayerId: function (layer) {
6894 return stamp(layer);
6895 }
6896});
6897
6898
6899// @factory L.layerGroup(layers?: Layer[], options?: Object)
6900// Create a layer group, optionally given an initial set of layers and an `options` object.
6901var layerGroup = function (layers, options) {
6902 return new LayerGroup(layers, options);
6903};
6904
6905/*
6906 * @class FeatureGroup
6907 * @aka L.FeatureGroup
6908 * @inherits LayerGroup
6909 *
6910 * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers:
6911 * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip))
6912 * * Events are propagated to the `FeatureGroup`, so if the group has an event
6913 * handler, it will handle events from any of the layers. This includes mouse events
6914 * and custom events.
6915 * * Has `layeradd` and `layerremove` events
6916 *
6917 * @example
6918 *
6919 * ```js
6920 * L.featureGroup([marker1, marker2, polyline])
6921 * .bindPopup('Hello world!')
6922 * .on('click', function() { alert('Clicked on a member of the group!'); })
6923 * .addTo(map);
6924 * ```
6925 */
6926
6927var FeatureGroup = LayerGroup.extend({
6928
6929 addLayer: function (layer) {
6930 if (this.hasLayer(layer)) {
6931 return this;
6932 }
6933
6934 layer.addEventParent(this);
6935
6936 LayerGroup.prototype.addLayer.call(this, layer);
6937
6938 // @event layeradd: LayerEvent
6939 // Fired when a layer is added to this `FeatureGroup`
6940 return this.fire('layeradd', {layer: layer});
6941 },
6942
6943 removeLayer: function (layer) {
6944 if (!this.hasLayer(layer)) {
6945 return this;
6946 }
6947 if (layer in this._layers) {
6948 layer = this._layers[layer];
6949 }
6950
6951 layer.removeEventParent(this);
6952
6953 LayerGroup.prototype.removeLayer.call(this, layer);
6954
6955 // @event layerremove: LayerEvent
6956 // Fired when a layer is removed from this `FeatureGroup`
6957 return this.fire('layerremove', {layer: layer});
6958 },
6959
6960 // @method setStyle(style: Path options): this
6961 // Sets the given path options to each layer of the group that has a `setStyle` method.
6962 setStyle: function (style) {
6963 return this.invoke('setStyle', style);
6964 },
6965
6966 // @method bringToFront(): this
6967 // Brings the layer group to the top of all other layers
6968 bringToFront: function () {
6969 return this.invoke('bringToFront');
6970 },
6971
6972 // @method bringToBack(): this
6973 // Brings the layer group to the back of all other layers
6974 bringToBack: function () {
6975 return this.invoke('bringToBack');
6976 },
6977
6978 // @method getBounds(): LatLngBounds
6979 // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children).
6980 getBounds: function () {
6981 var bounds = new LatLngBounds();
6982
6983 for (var id in this._layers) {
6984 var layer = this._layers[id];
6985 bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
6986 }
6987 return bounds;
6988 }
6989});
6990
6991// @factory L.featureGroup(layers?: Layer[], options?: Object)
6992// Create a feature group, optionally given an initial set of layers and an `options` object.
6993var featureGroup = function (layers, options) {
6994 return new FeatureGroup(layers, options);
6995};
6996
6997/*
6998 * @class Icon
6999 * @aka L.Icon
7000 *
7001 * Represents an icon to provide when creating a marker.
7002 *
7003 * @example
7004 *
7005 * ```js
7006 * var myIcon = L.icon({
7007 * iconUrl: 'my-icon.png',
7008 * iconRetinaUrl: 'my-icon@2x.png',
7009 * iconSize: [38, 95],
7010 * iconAnchor: [22, 94],
7011 * popupAnchor: [-3, -76],
7012 * shadowUrl: 'my-icon-shadow.png',
7013 * shadowRetinaUrl: 'my-icon-shadow@2x.png',
7014 * shadowSize: [68, 95],
7015 * shadowAnchor: [22, 94]
7016 * });
7017 *
7018 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
7019 * ```
7020 *
7021 * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
7022 *
7023 */
7024
7025var Icon = Class.extend({
7026
7027 /* @section
7028 * @aka Icon options
7029 *
7030 * @option iconUrl: String = null
7031 * **(required)** The URL to the icon image (absolute or relative to your script path).
7032 *
7033 * @option iconRetinaUrl: String = null
7034 * The URL to a retina sized version of the icon image (absolute or relative to your
7035 * script path). Used for Retina screen devices.
7036 *
7037 * @option iconSize: Point = null
7038 * Size of the icon image in pixels.
7039 *
7040 * @option iconAnchor: Point = null
7041 * The coordinates of the "tip" of the icon (relative to its top left corner). The icon
7042 * will be aligned so that this point is at the marker's geographical location. Centered
7043 * by default if size is specified, also can be set in CSS with negative margins.
7044 *
7045 * @option popupAnchor: Point = [0, 0]
7046 * The coordinates of the point from which popups will "open", relative to the icon anchor.
7047 *
7048 * @option tooltipAnchor: Point = [0, 0]
7049 * The coordinates of the point from which tooltips will "open", relative to the icon anchor.
7050 *
7051 * @option shadowUrl: String = null
7052 * The URL to the icon shadow image. If not specified, no shadow image will be created.
7053 *
7054 * @option shadowRetinaUrl: String = null
7055 *
7056 * @option shadowSize: Point = null
7057 * Size of the shadow image in pixels.
7058 *
7059 * @option shadowAnchor: Point = null
7060 * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
7061 * as iconAnchor if not specified).
7062 *
7063 * @option className: String = ''
7064 * A custom class name to assign to both icon and shadow images. Empty by default.
7065 */
7066
7067 options: {
7068 popupAnchor: [0, 0],
7069 tooltipAnchor: [0, 0],
7070
7071 // @option crossOrigin: Boolean|String = false
7072 // Whether the crossOrigin attribute will be added to the tiles.
7073 // 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.
7074 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
7075 crossOrigin: false
7076 },
7077
7078 initialize: function (options) {
7079 setOptions(this, options);
7080 },
7081
7082 // @method createIcon(oldIcon?: HTMLElement): HTMLElement
7083 // Called internally when the icon has to be shown, returns a `<img>` HTML element
7084 // styled according to the options.
7085 createIcon: function (oldIcon) {
7086 return this._createIcon('icon', oldIcon);
7087 },
7088
7089 // @method createShadow(oldIcon?: HTMLElement): HTMLElement
7090 // As `createIcon`, but for the shadow beneath it.
7091 createShadow: function (oldIcon) {
7092 return this._createIcon('shadow', oldIcon);
7093 },
7094
7095 _createIcon: function (name, oldIcon) {
7096 var src = this._getIconUrl(name);
7097
7098 if (!src) {
7099 if (name === 'icon') {
7100 throw new Error('iconUrl not set in Icon options (see the docs).');
7101 }
7102 return null;
7103 }
7104
7105 var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
7106 this._setIconStyles(img, name);
7107
7108 if (this.options.crossOrigin || this.options.crossOrigin === '') {
7109 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
7110 }
7111
7112 return img;
7113 },
7114
7115 _setIconStyles: function (img, name) {
7116 var options = this.options;
7117 var sizeOption = options[name + 'Size'];
7118
7119 if (typeof sizeOption === 'number') {
7120 sizeOption = [sizeOption, sizeOption];
7121 }
7122
7123 var size = toPoint(sizeOption),
7124 anchor = toPoint(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
7125 size && size.divideBy(2, true));
7126
7127 img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
7128
7129 if (anchor) {
7130 img.style.marginLeft = (-anchor.x) + 'px';
7131 img.style.marginTop = (-anchor.y) + 'px';
7132 }
7133
7134 if (size) {
7135 img.style.width = size.x + 'px';
7136 img.style.height = size.y + 'px';
7137 }
7138 },
7139
7140 _createImg: function (src, el) {
7141 el = el || document.createElement('img');
7142 el.src = src;
7143 return el;
7144 },
7145
7146 _getIconUrl: function (name) {
7147 return Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
7148 }
7149});
7150
7151
7152// @factory L.icon(options: Icon options)
7153// Creates an icon instance with the given options.
7154function icon(options) {
7155 return new Icon(options);
7156}
7157
7158/*
7159 * @miniclass Icon.Default (Icon)
7160 * @aka L.Icon.Default
7161 * @section
7162 *
7163 * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
7164 * no icon is specified. Points to the blue marker image distributed with Leaflet
7165 * releases.
7166 *
7167 * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options`
7168 * (which is a set of `Icon options`).
7169 *
7170 * If you want to _completely_ replace the default icon, override the
7171 * `L.Marker.prototype.options.icon` with your own icon instead.
7172 */
7173
7174var IconDefault = Icon.extend({
7175
7176 options: {
7177 iconUrl: 'marker-icon.png',
7178 iconRetinaUrl: 'marker-icon-2x.png',
7179 shadowUrl: 'marker-shadow.png',
7180 iconSize: [25, 41],
7181 iconAnchor: [12, 41],
7182 popupAnchor: [1, -34],
7183 tooltipAnchor: [16, -28],
7184 shadowSize: [41, 41]
7185 },
7186
7187 _getIconUrl: function (name) {
7188 if (typeof IconDefault.imagePath !== 'string') { // Deprecated, backwards-compatibility only
7189 IconDefault.imagePath = this._detectIconPath();
7190 }
7191
7192 // @option imagePath: String
7193 // `Icon.Default` will try to auto-detect the location of the
7194 // blue icon images. If you are placing these images in a non-standard
7195 // way, set this option to point to the right path.
7196 return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name);
7197 },
7198
7199 _stripUrl: function (path) { // separate function to use in tests
7200 var strip = function (str, re, idx) {
7201 var match = re.exec(str);
7202 return match && match[idx];
7203 };
7204 path = strip(path, /^url\((['"])?(.+)\1\)$/, 2);
7205 return path && strip(path, /^(.*)marker-icon\.png$/, 1);
7206 },
7207
7208 _detectIconPath: function () {
7209 var el = create$1('div', 'leaflet-default-icon-path', document.body);
7210 var path = getStyle(el, 'background-image') ||
7211 getStyle(el, 'backgroundImage'); // IE8
7212
7213 document.body.removeChild(el);
7214 path = this._stripUrl(path);
7215 if (path) { return path; }
7216 var link = document.querySelector('link[href$="leaflet.css"]');
7217 if (!link) { return ''; }
7218 return link.href.substring(0, link.href.length - 'leaflet.css'.length - 1);
7219 }
7220});
7221
7222/*
7223 * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
7224 */
7225
7226
7227/* @namespace Marker
7228 * @section Interaction handlers
7229 *
7230 * 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:
7231 *
7232 * ```js
7233 * marker.dragging.disable();
7234 * ```
7235 *
7236 * @property dragging: Handler
7237 * Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)).
7238 */
7239
7240var MarkerDrag = Handler.extend({
7241 initialize: function (marker) {
7242 this._marker = marker;
7243 },
7244
7245 addHooks: function () {
7246 var icon = this._marker._icon;
7247
7248 if (!this._draggable) {
7249 this._draggable = new Draggable(icon, icon, true);
7250 }
7251
7252 this._draggable.on({
7253 dragstart: this._onDragStart,
7254 predrag: this._onPreDrag,
7255 drag: this._onDrag,
7256 dragend: this._onDragEnd
7257 }, this).enable();
7258
7259 addClass(icon, 'leaflet-marker-draggable');
7260 },
7261
7262 removeHooks: function () {
7263 this._draggable.off({
7264 dragstart: this._onDragStart,
7265 predrag: this._onPreDrag,
7266 drag: this._onDrag,
7267 dragend: this._onDragEnd
7268 }, this).disable();
7269
7270 if (this._marker._icon) {
7271 removeClass(this._marker._icon, 'leaflet-marker-draggable');
7272 }
7273 },
7274
7275 moved: function () {
7276 return this._draggable && this._draggable._moved;
7277 },
7278
7279 _adjustPan: function (e) {
7280 var marker = this._marker,
7281 map = marker._map,
7282 speed = this._marker.options.autoPanSpeed,
7283 padding = this._marker.options.autoPanPadding,
7284 iconPos = getPosition(marker._icon),
7285 bounds = map.getPixelBounds(),
7286 origin = map.getPixelOrigin();
7287
7288 var panBounds = toBounds(
7289 bounds.min._subtract(origin).add(padding),
7290 bounds.max._subtract(origin).subtract(padding)
7291 );
7292
7293 if (!panBounds.contains(iconPos)) {
7294 // Compute incremental movement
7295 var movement = toPoint(
7296 (Math.max(panBounds.max.x, iconPos.x) - panBounds.max.x) / (bounds.max.x - panBounds.max.x) -
7297 (Math.min(panBounds.min.x, iconPos.x) - panBounds.min.x) / (bounds.min.x - panBounds.min.x),
7298
7299 (Math.max(panBounds.max.y, iconPos.y) - panBounds.max.y) / (bounds.max.y - panBounds.max.y) -
7300 (Math.min(panBounds.min.y, iconPos.y) - panBounds.min.y) / (bounds.min.y - panBounds.min.y)
7301 ).multiplyBy(speed);
7302
7303 map.panBy(movement, {animate: false});
7304
7305 this._draggable._newPos._add(movement);
7306 this._draggable._startPos._add(movement);
7307
7308 setPosition(marker._icon, this._draggable._newPos);
7309 this._onDrag(e);
7310
7311 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
7312 }
7313 },
7314
7315 _onDragStart: function () {
7316 // @section Dragging events
7317 // @event dragstart: Event
7318 // Fired when the user starts dragging the marker.
7319
7320 // @event movestart: Event
7321 // Fired when the marker starts moving (because of dragging).
7322
7323 this._oldLatLng = this._marker.getLatLng();
7324
7325 // When using ES6 imports it could not be set when `Popup` was not imported as well
7326 this._marker.closePopup && this._marker.closePopup();
7327
7328 this._marker
7329 .fire('movestart')
7330 .fire('dragstart');
7331 },
7332
7333 _onPreDrag: function (e) {
7334 if (this._marker.options.autoPan) {
7335 cancelAnimFrame(this._panRequest);
7336 this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
7337 }
7338 },
7339
7340 _onDrag: function (e) {
7341 var marker = this._marker,
7342 shadow = marker._shadow,
7343 iconPos = getPosition(marker._icon),
7344 latlng = marker._map.layerPointToLatLng(iconPos);
7345
7346 // update shadow position
7347 if (shadow) {
7348 setPosition(shadow, iconPos);
7349 }
7350
7351 marker._latlng = latlng;
7352 e.latlng = latlng;
7353 e.oldLatLng = this._oldLatLng;
7354
7355 // @event drag: Event
7356 // Fired repeatedly while the user drags the marker.
7357 marker
7358 .fire('move', e)
7359 .fire('drag', e);
7360 },
7361
7362 _onDragEnd: function (e) {
7363 // @event dragend: DragEndEvent
7364 // Fired when the user stops dragging the marker.
7365
7366 cancelAnimFrame(this._panRequest);
7367
7368 // @event moveend: Event
7369 // Fired when the marker stops moving (because of dragging).
7370 delete this._oldLatLng;
7371 this._marker
7372 .fire('moveend')
7373 .fire('dragend', e);
7374 }
7375});
7376
7377/*
7378 * @class Marker
7379 * @inherits Interactive layer
7380 * @aka L.Marker
7381 * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
7382 *
7383 * @example
7384 *
7385 * ```js
7386 * L.marker([50.5, 30.5]).addTo(map);
7387 * ```
7388 */
7389
7390var Marker = Layer.extend({
7391
7392 // @section
7393 // @aka Marker options
7394 options: {
7395 // @option icon: Icon = *
7396 // Icon instance to use for rendering the marker.
7397 // See [Icon documentation](#L.Icon) for details on how to customize the marker icon.
7398 // If not specified, a common instance of `L.Icon.Default` is used.
7399 icon: new IconDefault(),
7400
7401 // Option inherited from "Interactive layer" abstract class
7402 interactive: true,
7403
7404 // @option keyboard: Boolean = true
7405 // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
7406 keyboard: true,
7407
7408 // @option title: String = ''
7409 // Text for the browser tooltip that appear on marker hover (no tooltip by default).
7410 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
7411 title: '',
7412
7413 // @option alt: String = 'Marker'
7414 // Text for the `alt` attribute of the icon image.
7415 // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
7416 alt: 'Marker',
7417
7418 // @option zIndexOffset: Number = 0
7419 // 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).
7420 zIndexOffset: 0,
7421
7422 // @option opacity: Number = 1.0
7423 // The opacity of the marker.
7424 opacity: 1,
7425
7426 // @option riseOnHover: Boolean = false
7427 // If `true`, the marker will get on top of others when you hover the mouse over it.
7428 riseOnHover: false,
7429
7430 // @option riseOffset: Number = 250
7431 // The z-index offset used for the `riseOnHover` feature.
7432 riseOffset: 250,
7433
7434 // @option pane: String = 'markerPane'
7435 // `Map pane` where the markers icon will be added.
7436 pane: 'markerPane',
7437
7438 // @option shadowPane: String = 'shadowPane'
7439 // `Map pane` where the markers shadow will be added.
7440 shadowPane: 'shadowPane',
7441
7442 // @option bubblingMouseEvents: Boolean = false
7443 // When `true`, a mouse event on this marker will trigger the same event on the map
7444 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
7445 bubblingMouseEvents: false,
7446
7447 // @option autoPanOnFocus: Boolean = true
7448 // When `true`, the map will pan whenever the marker is focused (via
7449 // e.g. pressing `tab` on the keyboard) to ensure the marker is
7450 // visible within the map's bounds
7451 autoPanOnFocus: true,
7452
7453 // @section Draggable marker options
7454 // @option draggable: Boolean = false
7455 // Whether the marker is draggable with mouse/touch or not.
7456 draggable: false,
7457
7458 // @option autoPan: Boolean = false
7459 // Whether to pan the map when dragging this marker near its edge or not.
7460 autoPan: false,
7461
7462 // @option autoPanPadding: Point = Point(50, 50)
7463 // Distance (in pixels to the left/right and to the top/bottom) of the
7464 // map edge to start panning the map.
7465 autoPanPadding: [50, 50],
7466
7467 // @option autoPanSpeed: Number = 10
7468 // Number of pixels the map should pan by.
7469 autoPanSpeed: 10
7470 },
7471
7472 /* @section
7473 *
7474 * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
7475 */
7476
7477 initialize: function (latlng, options) {
7478 setOptions(this, options);
7479 this._latlng = toLatLng(latlng);
7480 },
7481
7482 onAdd: function (map) {
7483 this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
7484
7485 if (this._zoomAnimated) {
7486 map.on('zoomanim', this._animateZoom, this);
7487 }
7488
7489 this._initIcon();
7490 this.update();
7491 },
7492
7493 onRemove: function (map) {
7494 if (this.dragging && this.dragging.enabled()) {
7495 this.options.draggable = true;
7496 this.dragging.removeHooks();
7497 }
7498 delete this.dragging;
7499
7500 if (this._zoomAnimated) {
7501 map.off('zoomanim', this._animateZoom, this);
7502 }
7503
7504 this._removeIcon();
7505 this._removeShadow();
7506 },
7507
7508 getEvents: function () {
7509 return {
7510 zoom: this.update,
7511 viewreset: this.update
7512 };
7513 },
7514
7515 // @method getLatLng: LatLng
7516 // Returns the current geographical position of the marker.
7517 getLatLng: function () {
7518 return this._latlng;
7519 },
7520
7521 // @method setLatLng(latlng: LatLng): this
7522 // Changes the marker position to the given point.
7523 setLatLng: function (latlng) {
7524 var oldLatLng = this._latlng;
7525 this._latlng = toLatLng(latlng);
7526 this.update();
7527
7528 // @event move: Event
7529 // 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`.
7530 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
7531 },
7532
7533 // @method setZIndexOffset(offset: Number): this
7534 // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
7535 setZIndexOffset: function (offset) {
7536 this.options.zIndexOffset = offset;
7537 return this.update();
7538 },
7539
7540 // @method getIcon: Icon
7541 // Returns the current icon used by the marker
7542 getIcon: function () {
7543 return this.options.icon;
7544 },
7545
7546 // @method setIcon(icon: Icon): this
7547 // Changes the marker icon.
7548 setIcon: function (icon) {
7549
7550 this.options.icon = icon;
7551
7552 if (this._map) {
7553 this._initIcon();
7554 this.update();
7555 }
7556
7557 if (this._popup) {
7558 this.bindPopup(this._popup, this._popup.options);
7559 }
7560
7561 return this;
7562 },
7563
7564 getElement: function () {
7565 return this._icon;
7566 },
7567
7568 update: function () {
7569
7570 if (this._icon && this._map) {
7571 var pos = this._map.latLngToLayerPoint(this._latlng).round();
7572 this._setPos(pos);
7573 }
7574
7575 return this;
7576 },
7577
7578 _initIcon: function () {
7579 var options = this.options,
7580 classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
7581
7582 var icon = options.icon.createIcon(this._icon),
7583 addIcon = false;
7584
7585 // if we're not reusing the icon, remove the old one and init new one
7586 if (icon !== this._icon) {
7587 if (this._icon) {
7588 this._removeIcon();
7589 }
7590 addIcon = true;
7591
7592 if (options.title) {
7593 icon.title = options.title;
7594 }
7595
7596 if (icon.tagName === 'IMG') {
7597 icon.alt = options.alt || '';
7598 }
7599 }
7600
7601 addClass(icon, classToAdd);
7602
7603 if (options.keyboard) {
7604 icon.tabIndex = '0';
7605 icon.setAttribute('role', 'button');
7606 }
7607
7608 this._icon = icon;
7609
7610 if (options.riseOnHover) {
7611 this.on({
7612 mouseover: this._bringToFront,
7613 mouseout: this._resetZIndex
7614 });
7615 }
7616
7617 if (this.options.autoPanOnFocus) {
7618 on(icon, 'focus', this._panOnFocus, this);
7619 }
7620
7621 var newShadow = options.icon.createShadow(this._shadow),
7622 addShadow = false;
7623
7624 if (newShadow !== this._shadow) {
7625 this._removeShadow();
7626 addShadow = true;
7627 }
7628
7629 if (newShadow) {
7630 addClass(newShadow, classToAdd);
7631 newShadow.alt = '';
7632 }
7633 this._shadow = newShadow;
7634
7635
7636 if (options.opacity < 1) {
7637 this._updateOpacity();
7638 }
7639
7640
7641 if (addIcon) {
7642 this.getPane().appendChild(this._icon);
7643 }
7644 this._initInteraction();
7645 if (newShadow && addShadow) {
7646 this.getPane(options.shadowPane).appendChild(this._shadow);
7647 }
7648 },
7649
7650 _removeIcon: function () {
7651 if (this.options.riseOnHover) {
7652 this.off({
7653 mouseover: this._bringToFront,
7654 mouseout: this._resetZIndex
7655 });
7656 }
7657
7658 if (this.options.autoPanOnFocus) {
7659 off(this._icon, 'focus', this._panOnFocus, this);
7660 }
7661
7662 remove(this._icon);
7663 this.removeInteractiveTarget(this._icon);
7664
7665 this._icon = null;
7666 },
7667
7668 _removeShadow: function () {
7669 if (this._shadow) {
7670 remove(this._shadow);
7671 }
7672 this._shadow = null;
7673 },
7674
7675 _setPos: function (pos) {
7676
7677 if (this._icon) {
7678 setPosition(this._icon, pos);
7679 }
7680
7681 if (this._shadow) {
7682 setPosition(this._shadow, pos);
7683 }
7684
7685 this._zIndex = pos.y + this.options.zIndexOffset;
7686
7687 this._resetZIndex();
7688 },
7689
7690 _updateZIndex: function (offset) {
7691 if (this._icon) {
7692 this._icon.style.zIndex = this._zIndex + offset;
7693 }
7694 },
7695
7696 _animateZoom: function (opt) {
7697 var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
7698
7699 this._setPos(pos);
7700 },
7701
7702 _initInteraction: function () {
7703
7704 if (!this.options.interactive) { return; }
7705
7706 addClass(this._icon, 'leaflet-interactive');
7707
7708 this.addInteractiveTarget(this._icon);
7709
7710 if (MarkerDrag) {
7711 var draggable = this.options.draggable;
7712 if (this.dragging) {
7713 draggable = this.dragging.enabled();
7714 this.dragging.disable();
7715 }
7716
7717 this.dragging = new MarkerDrag(this);
7718
7719 if (draggable) {
7720 this.dragging.enable();
7721 }
7722 }
7723 },
7724
7725 // @method setOpacity(opacity: Number): this
7726 // Changes the opacity of the marker.
7727 setOpacity: function (opacity) {
7728 this.options.opacity = opacity;
7729 if (this._map) {
7730 this._updateOpacity();
7731 }
7732
7733 return this;
7734 },
7735
7736 _updateOpacity: function () {
7737 var opacity = this.options.opacity;
7738
7739 if (this._icon) {
7740 setOpacity(this._icon, opacity);
7741 }
7742
7743 if (this._shadow) {
7744 setOpacity(this._shadow, opacity);
7745 }
7746 },
7747
7748 _bringToFront: function () {
7749 this._updateZIndex(this.options.riseOffset);
7750 },
7751
7752 _resetZIndex: function () {
7753 this._updateZIndex(0);
7754 },
7755
7756 _panOnFocus: function () {
7757 var map = this._map;
7758 if (!map) { return; }
7759
7760 var iconOpts = this.options.icon.options;
7761 var size = iconOpts.iconSize ? toPoint(iconOpts.iconSize) : toPoint(0, 0);
7762 var anchor = iconOpts.iconAnchor ? toPoint(iconOpts.iconAnchor) : toPoint(0, 0);
7763
7764 map.panInside(this._latlng, {
7765 paddingTopLeft: anchor,
7766 paddingBottomRight: size.subtract(anchor)
7767 });
7768 },
7769
7770 _getPopupAnchor: function () {
7771 return this.options.icon.options.popupAnchor;
7772 },
7773
7774 _getTooltipAnchor: function () {
7775 return this.options.icon.options.tooltipAnchor;
7776 }
7777});
7778
7779
7780// factory L.marker(latlng: LatLng, options? : Marker options)
7781
7782// @factory L.marker(latlng: LatLng, options? : Marker options)
7783// Instantiates a Marker object given a geographical point and optionally an options object.
7784function marker(latlng, options) {
7785 return new Marker(latlng, options);
7786}
7787
7788/*
7789 * @class Path
7790 * @aka L.Path
7791 * @inherits Interactive layer
7792 *
7793 * An abstract class that contains options and constants shared between vector
7794 * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`.
7795 */
7796
7797var Path = Layer.extend({
7798
7799 // @section
7800 // @aka Path options
7801 options: {
7802 // @option stroke: Boolean = true
7803 // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles.
7804 stroke: true,
7805
7806 // @option color: String = '#3388ff'
7807 // Stroke color
7808 color: '#3388ff',
7809
7810 // @option weight: Number = 3
7811 // Stroke width in pixels
7812 weight: 3,
7813
7814 // @option opacity: Number = 1.0
7815 // Stroke opacity
7816 opacity: 1,
7817
7818 // @option lineCap: String= 'round'
7819 // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke.
7820 lineCap: 'round',
7821
7822 // @option lineJoin: String = 'round'
7823 // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke.
7824 lineJoin: 'round',
7825
7826 // @option dashArray: String = null
7827 // 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).
7828 dashArray: null,
7829
7830 // @option dashOffset: String = null
7831 // 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).
7832 dashOffset: null,
7833
7834 // @option fill: Boolean = depends
7835 // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles.
7836 fill: false,
7837
7838 // @option fillColor: String = *
7839 // Fill color. Defaults to the value of the [`color`](#path-color) option
7840 fillColor: null,
7841
7842 // @option fillOpacity: Number = 0.2
7843 // Fill opacity.
7844 fillOpacity: 0.2,
7845
7846 // @option fillRule: String = 'evenodd'
7847 // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined.
7848 fillRule: 'evenodd',
7849
7850 // className: '',
7851
7852 // Option inherited from "Interactive layer" abstract class
7853 interactive: true,
7854
7855 // @option bubblingMouseEvents: Boolean = true
7856 // When `true`, a mouse event on this path will trigger the same event on the map
7857 // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
7858 bubblingMouseEvents: true
7859 },
7860
7861 beforeAdd: function (map) {
7862 // Renderer is set here because we need to call renderer.getEvents
7863 // before this.getEvents.
7864 this._renderer = map.getRenderer(this);
7865 },
7866
7867 onAdd: function () {
7868 this._renderer._initPath(this);
7869 this._reset();
7870 this._renderer._addPath(this);
7871 },
7872
7873 onRemove: function () {
7874 this._renderer._removePath(this);
7875 },
7876
7877 // @method redraw(): this
7878 // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses.
7879 redraw: function () {
7880 if (this._map) {
7881 this._renderer._updatePath(this);
7882 }
7883 return this;
7884 },
7885
7886 // @method setStyle(style: Path options): this
7887 // Changes the appearance of a Path based on the options in the `Path options` object.
7888 setStyle: function (style) {
7889 setOptions(this, style);
7890 if (this._renderer) {
7891 this._renderer._updateStyle(this);
7892 if (this.options.stroke && style && Object.prototype.hasOwnProperty.call(style, 'weight')) {
7893 this._updateBounds();
7894 }
7895 }
7896 return this;
7897 },
7898
7899 // @method bringToFront(): this
7900 // Brings the layer to the top of all path layers.
7901 bringToFront: function () {
7902 if (this._renderer) {
7903 this._renderer._bringToFront(this);
7904 }
7905 return this;
7906 },
7907
7908 // @method bringToBack(): this
7909 // Brings the layer to the bottom of all path layers.
7910 bringToBack: function () {
7911 if (this._renderer) {
7912 this._renderer._bringToBack(this);
7913 }
7914 return this;
7915 },
7916
7917 getElement: function () {
7918 return this._path;
7919 },
7920
7921 _reset: function () {
7922 // defined in child classes
7923 this._project();
7924 this._update();
7925 },
7926
7927 _clickTolerance: function () {
7928 // used when doing hit detection for Canvas layers
7929 return (this.options.stroke ? this.options.weight / 2 : 0) +
7930 (this._renderer.options.tolerance || 0);
7931 }
7932});
7933
7934/*
7935 * @class CircleMarker
7936 * @aka L.CircleMarker
7937 * @inherits Path
7938 *
7939 * A circle of a fixed size with radius specified in pixels. Extends `Path`.
7940 */
7941
7942var CircleMarker = Path.extend({
7943
7944 // @section
7945 // @aka CircleMarker options
7946 options: {
7947 fill: true,
7948
7949 // @option radius: Number = 10
7950 // Radius of the circle marker, in pixels
7951 radius: 10
7952 },
7953
7954 initialize: function (latlng, options) {
7955 setOptions(this, options);
7956 this._latlng = toLatLng(latlng);
7957 this._radius = this.options.radius;
7958 },
7959
7960 // @method setLatLng(latLng: LatLng): this
7961 // Sets the position of a circle marker to a new location.
7962 setLatLng: function (latlng) {
7963 var oldLatLng = this._latlng;
7964 this._latlng = toLatLng(latlng);
7965 this.redraw();
7966
7967 // @event move: Event
7968 // Fired when the marker is moved via [`setLatLng`](#circlemarker-setlatlng). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
7969 return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
7970 },
7971
7972 // @method getLatLng(): LatLng
7973 // Returns the current geographical position of the circle marker
7974 getLatLng: function () {
7975 return this._latlng;
7976 },
7977
7978 // @method setRadius(radius: Number): this
7979 // Sets the radius of a circle marker. Units are in pixels.
7980 setRadius: function (radius) {
7981 this.options.radius = this._radius = radius;
7982 return this.redraw();
7983 },
7984
7985 // @method getRadius(): Number
7986 // Returns the current radius of the circle
7987 getRadius: function () {
7988 return this._radius;
7989 },
7990
7991 setStyle : function (options) {
7992 var radius = options && options.radius || this._radius;
7993 Path.prototype.setStyle.call(this, options);
7994 this.setRadius(radius);
7995 return this;
7996 },
7997
7998 _project: function () {
7999 this._point = this._map.latLngToLayerPoint(this._latlng);
8000 this._updateBounds();
8001 },
8002
8003 _updateBounds: function () {
8004 var r = this._radius,
8005 r2 = this._radiusY || r,
8006 w = this._clickTolerance(),
8007 p = [r + w, r2 + w];
8008 this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p));
8009 },
8010
8011 _update: function () {
8012 if (this._map) {
8013 this._updatePath();
8014 }
8015 },
8016
8017 _updatePath: function () {
8018 this._renderer._updateCircle(this);
8019 },
8020
8021 _empty: function () {
8022 return this._radius && !this._renderer._bounds.intersects(this._pxBounds);
8023 },
8024
8025 // Needed by the `Canvas` renderer for interactivity
8026 _containsPoint: function (p) {
8027 return p.distanceTo(this._point) <= this._radius + this._clickTolerance();
8028 }
8029});
8030
8031
8032// @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options)
8033// Instantiates a circle marker object given a geographical point, and an optional options object.
8034function circleMarker(latlng, options) {
8035 return new CircleMarker(latlng, options);
8036}
8037
8038/*
8039 * @class Circle
8040 * @aka L.Circle
8041 * @inherits CircleMarker
8042 *
8043 * A class for drawing circle overlays on a map. Extends `CircleMarker`.
8044 *
8045 * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion).
8046 *
8047 * @example
8048 *
8049 * ```js
8050 * L.circle([50.5, 30.5], {radius: 200}).addTo(map);
8051 * ```
8052 */
8053
8054var Circle = CircleMarker.extend({
8055
8056 initialize: function (latlng, options, legacyOptions) {
8057 if (typeof options === 'number') {
8058 // Backwards compatibility with 0.7.x factory (latlng, radius, options?)
8059 options = extend({}, legacyOptions, {radius: options});
8060 }
8061 setOptions(this, options);
8062 this._latlng = toLatLng(latlng);
8063
8064 if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); }
8065
8066 // @section
8067 // @aka Circle options
8068 // @option radius: Number; Radius of the circle, in meters.
8069 this._mRadius = this.options.radius;
8070 },
8071
8072 // @method setRadius(radius: Number): this
8073 // Sets the radius of a circle. Units are in meters.
8074 setRadius: function (radius) {
8075 this._mRadius = radius;
8076 return this.redraw();
8077 },
8078
8079 // @method getRadius(): Number
8080 // Returns the current radius of a circle. Units are in meters.
8081 getRadius: function () {
8082 return this._mRadius;
8083 },
8084
8085 // @method getBounds(): LatLngBounds
8086 // Returns the `LatLngBounds` of the path.
8087 getBounds: function () {
8088 var half = [this._radius, this._radiusY || this._radius];
8089
8090 return new LatLngBounds(
8091 this._map.layerPointToLatLng(this._point.subtract(half)),
8092 this._map.layerPointToLatLng(this._point.add(half)));
8093 },
8094
8095 setStyle: Path.prototype.setStyle,
8096
8097 _project: function () {
8098
8099 var lng = this._latlng.lng,
8100 lat = this._latlng.lat,
8101 map = this._map,
8102 crs = map.options.crs;
8103
8104 if (crs.distance === Earth.distance) {
8105 var d = Math.PI / 180,
8106 latR = (this._mRadius / Earth.R) / d,
8107 top = map.project([lat + latR, lng]),
8108 bottom = map.project([lat - latR, lng]),
8109 p = top.add(bottom).divideBy(2),
8110 lat2 = map.unproject(p).lat,
8111 lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) /
8112 (Math.cos(lat * d) * Math.cos(lat2 * d))) / d;
8113
8114 if (isNaN(lngR) || lngR === 0) {
8115 lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425
8116 }
8117
8118 this._point = p.subtract(map.getPixelOrigin());
8119 this._radius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x;
8120 this._radiusY = p.y - top.y;
8121
8122 } else {
8123 var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0]));
8124
8125 this._point = map.latLngToLayerPoint(this._latlng);
8126 this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x;
8127 }
8128
8129 this._updateBounds();
8130 }
8131});
8132
8133// @factory L.circle(latlng: LatLng, options?: Circle options)
8134// Instantiates a circle object given a geographical point, and an options object
8135// which contains the circle radius.
8136// @alternative
8137// @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options)
8138// Obsolete way of instantiating a circle, for compatibility with 0.7.x code.
8139// Do not use in new applications or plugins.
8140function circle(latlng, options, legacyOptions) {
8141 return new Circle(latlng, options, legacyOptions);
8142}
8143
8144/*
8145 * @class Polyline
8146 * @aka L.Polyline
8147 * @inherits Path
8148 *
8149 * A class for drawing polyline overlays on a map. Extends `Path`.
8150 *
8151 * @example
8152 *
8153 * ```js
8154 * // create a red polyline from an array of LatLng points
8155 * var latlngs = [
8156 * [45.51, -122.68],
8157 * [37.77, -122.43],
8158 * [34.04, -118.2]
8159 * ];
8160 *
8161 * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
8162 *
8163 * // zoom the map to the polyline
8164 * map.fitBounds(polyline.getBounds());
8165 * ```
8166 *
8167 * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape:
8168 *
8169 * ```js
8170 * // create a red polyline from an array of arrays of LatLng points
8171 * var latlngs = [
8172 * [[45.51, -122.68],
8173 * [37.77, -122.43],
8174 * [34.04, -118.2]],
8175 * [[40.78, -73.91],
8176 * [41.83, -87.62],
8177 * [32.76, -96.72]]
8178 * ];
8179 * ```
8180 */
8181
8182
8183var Polyline = Path.extend({
8184
8185 // @section
8186 // @aka Polyline options
8187 options: {
8188 // @option smoothFactor: Number = 1.0
8189 // How much to simplify the polyline on each zoom level. More means
8190 // better performance and smoother look, and less means more accurate representation.
8191 smoothFactor: 1.0,
8192
8193 // @option noClip: Boolean = false
8194 // Disable polyline clipping.
8195 noClip: false
8196 },
8197
8198 initialize: function (latlngs, options) {
8199 setOptions(this, options);
8200 this._setLatLngs(latlngs);
8201 },
8202
8203 // @method getLatLngs(): LatLng[]
8204 // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
8205 getLatLngs: function () {
8206 return this._latlngs;
8207 },
8208
8209 // @method setLatLngs(latlngs: LatLng[]): this
8210 // Replaces all the points in the polyline with the given array of geographical points.
8211 setLatLngs: function (latlngs) {
8212 this._setLatLngs(latlngs);
8213 return this.redraw();
8214 },
8215
8216 // @method isEmpty(): Boolean
8217 // Returns `true` if the Polyline has no LatLngs.
8218 isEmpty: function () {
8219 return !this._latlngs.length;
8220 },
8221
8222 // @method closestLayerPoint(p: Point): Point
8223 // Returns the point closest to `p` on the Polyline.
8224 closestLayerPoint: function (p) {
8225 var minDistance = Infinity,
8226 minPoint = null,
8227 closest = _sqClosestPointOnSegment,
8228 p1, p2;
8229
8230 for (var j = 0, jLen = this._parts.length; j < jLen; j++) {
8231 var points = this._parts[j];
8232
8233 for (var i = 1, len = points.length; i < len; i++) {
8234 p1 = points[i - 1];
8235 p2 = points[i];
8236
8237 var sqDist = closest(p, p1, p2, true);
8238
8239 if (sqDist < minDistance) {
8240 minDistance = sqDist;
8241 minPoint = closest(p, p1, p2);
8242 }
8243 }
8244 }
8245 if (minPoint) {
8246 minPoint.distance = Math.sqrt(minDistance);
8247 }
8248 return minPoint;
8249 },
8250
8251 // @method getCenter(): LatLng
8252 // Returns the center ([centroid](https://en.wikipedia.org/wiki/Centroid)) of the polyline.
8253 getCenter: function () {
8254 // throws error when not yet added to map as this center calculation requires projected coordinates
8255 if (!this._map) {
8256 throw new Error('Must add layer to map before using getCenter()');
8257 }
8258
8259 var i, halfDist, segDist, dist, p1, p2, ratio,
8260 points = this._rings[0],
8261 len = points.length;
8262
8263 if (!len) { return null; }
8264
8265 // polyline centroid algorithm; only uses the first ring if there are multiple
8266
8267 for (i = 0, halfDist = 0; i < len - 1; i++) {
8268 halfDist += points[i].distanceTo(points[i + 1]) / 2;
8269 }
8270
8271 // The line is so small in the current view that all points are on the same pixel.
8272 if (halfDist === 0) {
8273 return this._map.layerPointToLatLng(points[0]);
8274 }
8275
8276 for (i = 0, dist = 0; i < len - 1; i++) {
8277 p1 = points[i];
8278 p2 = points[i + 1];
8279 segDist = p1.distanceTo(p2);
8280 dist += segDist;
8281
8282 if (dist > halfDist) {
8283 ratio = (dist - halfDist) / segDist;
8284 return this._map.layerPointToLatLng([
8285 p2.x - ratio * (p2.x - p1.x),
8286 p2.y - ratio * (p2.y - p1.y)
8287 ]);
8288 }
8289 }
8290 },
8291
8292 // @method getBounds(): LatLngBounds
8293 // Returns the `LatLngBounds` of the path.
8294 getBounds: function () {
8295 return this._bounds;
8296 },
8297
8298 // @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this
8299 // Adds a given point to the polyline. By default, adds to the first ring of
8300 // the polyline in case of a multi-polyline, but can be overridden by passing
8301 // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)).
8302 addLatLng: function (latlng, latlngs) {
8303 latlngs = latlngs || this._defaultShape();
8304 latlng = toLatLng(latlng);
8305 latlngs.push(latlng);
8306 this._bounds.extend(latlng);
8307 return this.redraw();
8308 },
8309
8310 _setLatLngs: function (latlngs) {
8311 this._bounds = new LatLngBounds();
8312 this._latlngs = this._convertLatLngs(latlngs);
8313 },
8314
8315 _defaultShape: function () {
8316 return isFlat(this._latlngs) ? this._latlngs : this._latlngs[0];
8317 },
8318
8319 // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way
8320 _convertLatLngs: function (latlngs) {
8321 var result = [],
8322 flat = isFlat(latlngs);
8323
8324 for (var i = 0, len = latlngs.length; i < len; i++) {
8325 if (flat) {
8326 result[i] = toLatLng(latlngs[i]);
8327 this._bounds.extend(result[i]);
8328 } else {
8329 result[i] = this._convertLatLngs(latlngs[i]);
8330 }
8331 }
8332
8333 return result;
8334 },
8335
8336 _project: function () {
8337 var pxBounds = new Bounds();
8338 this._rings = [];
8339 this._projectLatlngs(this._latlngs, this._rings, pxBounds);
8340
8341 if (this._bounds.isValid() && pxBounds.isValid()) {
8342 this._rawPxBounds = pxBounds;
8343 this._updateBounds();
8344 }
8345 },
8346
8347 _updateBounds: function () {
8348 var w = this._clickTolerance(),
8349 p = new Point(w, w);
8350
8351 if (!this._rawPxBounds) {
8352 return;
8353 }
8354
8355 this._pxBounds = new Bounds([
8356 this._rawPxBounds.min.subtract(p),
8357 this._rawPxBounds.max.add(p)
8358 ]);
8359 },
8360
8361 // recursively turns latlngs into a set of rings with projected coordinates
8362 _projectLatlngs: function (latlngs, result, projectedBounds) {
8363 var flat = latlngs[0] instanceof LatLng,
8364 len = latlngs.length,
8365 i, ring;
8366
8367 if (flat) {
8368 ring = [];
8369 for (i = 0; i < len; i++) {
8370 ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
8371 projectedBounds.extend(ring[i]);
8372 }
8373 result.push(ring);
8374 } else {
8375 for (i = 0; i < len; i++) {
8376 this._projectLatlngs(latlngs[i], result, projectedBounds);
8377 }
8378 }
8379 },
8380
8381 // clip polyline by renderer bounds so that we have less to render for performance
8382 _clipPoints: function () {
8383 var bounds = this._renderer._bounds;
8384
8385 this._parts = [];
8386 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
8387 return;
8388 }
8389
8390 if (this.options.noClip) {
8391 this._parts = this._rings;
8392 return;
8393 }
8394
8395 var parts = this._parts,
8396 i, j, k, len, len2, segment, points;
8397
8398 for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
8399 points = this._rings[i];
8400
8401 for (j = 0, len2 = points.length; j < len2 - 1; j++) {
8402 segment = clipSegment(points[j], points[j + 1], bounds, j, true);
8403
8404 if (!segment) { continue; }
8405
8406 parts[k] = parts[k] || [];
8407 parts[k].push(segment[0]);
8408
8409 // if segment goes out of screen, or it's the last one, it's the end of the line part
8410 if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
8411 parts[k].push(segment[1]);
8412 k++;
8413 }
8414 }
8415 }
8416 },
8417
8418 // simplify each clipped part of the polyline for performance
8419 _simplifyPoints: function () {
8420 var parts = this._parts,
8421 tolerance = this.options.smoothFactor;
8422
8423 for (var i = 0, len = parts.length; i < len; i++) {
8424 parts[i] = simplify(parts[i], tolerance);
8425 }
8426 },
8427
8428 _update: function () {
8429 if (!this._map) { return; }
8430
8431 this._clipPoints();
8432 this._simplifyPoints();
8433 this._updatePath();
8434 },
8435
8436 _updatePath: function () {
8437 this._renderer._updatePoly(this);
8438 },
8439
8440 // Needed by the `Canvas` renderer for interactivity
8441 _containsPoint: function (p, closed) {
8442 var i, j, k, len, len2, part,
8443 w = this._clickTolerance();
8444
8445 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
8446
8447 // hit detection for polylines
8448 for (i = 0, len = this._parts.length; i < len; i++) {
8449 part = this._parts[i];
8450
8451 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
8452 if (!closed && (j === 0)) { continue; }
8453
8454 if (pointToSegmentDistance(p, part[k], part[j]) <= w) {
8455 return true;
8456 }
8457 }
8458 }
8459 return false;
8460 }
8461});
8462
8463// @factory L.polyline(latlngs: LatLng[], options?: Polyline options)
8464// Instantiates a polyline object given an array of geographical points and
8465// optionally an options object. You can create a `Polyline` object with
8466// multiple separate lines (`MultiPolyline`) by passing an array of arrays
8467// of geographic points.
8468function polyline(latlngs, options) {
8469 return new Polyline(latlngs, options);
8470}
8471
8472// Retrocompat. Allow plugins to support Leaflet versions before and after 1.1.
8473Polyline._flat = _flat;
8474
8475/*
8476 * @class Polygon
8477 * @aka L.Polygon
8478 * @inherits Polyline
8479 *
8480 * A class for drawing polygon overlays on a map. Extends `Polyline`.
8481 *
8482 * 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.
8483 *
8484 *
8485 * @example
8486 *
8487 * ```js
8488 * // create a red polygon from an array of LatLng points
8489 * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
8490 *
8491 * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
8492 *
8493 * // zoom the map to the polygon
8494 * map.fitBounds(polygon.getBounds());
8495 * ```
8496 *
8497 * 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:
8498 *
8499 * ```js
8500 * var latlngs = [
8501 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
8502 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
8503 * ];
8504 * ```
8505 *
8506 * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape.
8507 *
8508 * ```js
8509 * var latlngs = [
8510 * [ // first polygon
8511 * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
8512 * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
8513 * ],
8514 * [ // second polygon
8515 * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
8516 * ]
8517 * ];
8518 * ```
8519 */
8520
8521var Polygon = Polyline.extend({
8522
8523 options: {
8524 fill: true
8525 },
8526
8527 isEmpty: function () {
8528 return !this._latlngs.length || !this._latlngs[0].length;
8529 },
8530
8531 getCenter: function () {
8532 // throws error when not yet added to map as this center calculation requires projected coordinates
8533 if (!this._map) {
8534 throw new Error('Must add layer to map before using getCenter()');
8535 }
8536
8537 var i, j, p1, p2, f, area, x, y, center,
8538 points = this._rings[0],
8539 len = points.length;
8540
8541 if (!len) { return null; }
8542
8543 // polygon centroid algorithm; only uses the first ring if there are multiple
8544
8545 area = x = y = 0;
8546
8547 for (i = 0, j = len - 1; i < len; j = i++) {
8548 p1 = points[i];
8549 p2 = points[j];
8550
8551 f = p1.y * p2.x - p2.y * p1.x;
8552 x += (p1.x + p2.x) * f;
8553 y += (p1.y + p2.y) * f;
8554 area += f * 3;
8555 }
8556
8557 if (area === 0) {
8558 // Polygon is so small that all points are on same pixel.
8559 center = points[0];
8560 } else {
8561 center = [x / area, y / area];
8562 }
8563 return this._map.layerPointToLatLng(center);
8564 },
8565
8566 _convertLatLngs: function (latlngs) {
8567 var result = Polyline.prototype._convertLatLngs.call(this, latlngs),
8568 len = result.length;
8569
8570 // remove last point if it equals first one
8571 if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) {
8572 result.pop();
8573 }
8574 return result;
8575 },
8576
8577 _setLatLngs: function (latlngs) {
8578 Polyline.prototype._setLatLngs.call(this, latlngs);
8579 if (isFlat(this._latlngs)) {
8580 this._latlngs = [this._latlngs];
8581 }
8582 },
8583
8584 _defaultShape: function () {
8585 return isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0];
8586 },
8587
8588 _clipPoints: function () {
8589 // polygons need a different clipping algorithm so we redefine that
8590
8591 var bounds = this._renderer._bounds,
8592 w = this.options.weight,
8593 p = new Point(w, w);
8594
8595 // increase clip padding by stroke width to avoid stroke on clip edges
8596 bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p));
8597
8598 this._parts = [];
8599 if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
8600 return;
8601 }
8602
8603 if (this.options.noClip) {
8604 this._parts = this._rings;
8605 return;
8606 }
8607
8608 for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
8609 clipped = clipPolygon(this._rings[i], bounds, true);
8610 if (clipped.length) {
8611 this._parts.push(clipped);
8612 }
8613 }
8614 },
8615
8616 _updatePath: function () {
8617 this._renderer._updatePoly(this, true);
8618 },
8619
8620 // Needed by the `Canvas` renderer for interactivity
8621 _containsPoint: function (p) {
8622 var inside = false,
8623 part, p1, p2, i, j, k, len, len2;
8624
8625 if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
8626
8627 // ray casting algorithm for detecting if point is in polygon
8628 for (i = 0, len = this._parts.length; i < len; i++) {
8629 part = this._parts[i];
8630
8631 for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
8632 p1 = part[j];
8633 p2 = part[k];
8634
8635 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)) {
8636 inside = !inside;
8637 }
8638 }
8639 }
8640
8641 // also check if it's on polygon stroke
8642 return inside || Polyline.prototype._containsPoint.call(this, p, true);
8643 }
8644
8645});
8646
8647
8648// @factory L.polygon(latlngs: LatLng[], options?: Polyline options)
8649function polygon(latlngs, options) {
8650 return new Polygon(latlngs, options);
8651}
8652
8653/*
8654 * @class GeoJSON
8655 * @aka L.GeoJSON
8656 * @inherits FeatureGroup
8657 *
8658 * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse
8659 * GeoJSON data and display it on the map. Extends `FeatureGroup`.
8660 *
8661 * @example
8662 *
8663 * ```js
8664 * L.geoJSON(data, {
8665 * style: function (feature) {
8666 * return {color: feature.properties.color};
8667 * }
8668 * }).bindPopup(function (layer) {
8669 * return layer.feature.properties.description;
8670 * }).addTo(map);
8671 * ```
8672 */
8673
8674var GeoJSON = FeatureGroup.extend({
8675
8676 /* @section
8677 * @aka GeoJSON options
8678 *
8679 * @option pointToLayer: Function = *
8680 * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally
8681 * called when data is added, passing the GeoJSON point feature and its `LatLng`.
8682 * The default is to spawn a default `Marker`:
8683 * ```js
8684 * function(geoJsonPoint, latlng) {
8685 * return L.marker(latlng);
8686 * }
8687 * ```
8688 *
8689 * @option style: Function = *
8690 * A `Function` defining the `Path options` for styling GeoJSON lines and polygons,
8691 * called internally when data is added.
8692 * The default value is to not override any defaults:
8693 * ```js
8694 * function (geoJsonFeature) {
8695 * return {}
8696 * }
8697 * ```
8698 *
8699 * @option onEachFeature: Function = *
8700 * A `Function` that will be called once for each created `Feature`, after it has
8701 * been created and styled. Useful for attaching events and popups to features.
8702 * The default is to do nothing with the newly created layers:
8703 * ```js
8704 * function (feature, layer) {}
8705 * ```
8706 *
8707 * @option filter: Function = *
8708 * A `Function` that will be used to decide whether to include a feature or not.
8709 * The default is to include all features:
8710 * ```js
8711 * function (geoJsonFeature) {
8712 * return true;
8713 * }
8714 * ```
8715 * Note: dynamically changing the `filter` option will have effect only on newly
8716 * added data. It will _not_ re-evaluate already included features.
8717 *
8718 * @option coordsToLatLng: Function = *
8719 * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s.
8720 * The default is the `coordsToLatLng` static method.
8721 *
8722 * @option markersInheritOptions: Boolean = false
8723 * Whether default Markers for "Point" type Features inherit from group options.
8724 */
8725
8726 initialize: function (geojson, options) {
8727 setOptions(this, options);
8728
8729 this._layers = {};
8730
8731 if (geojson) {
8732 this.addData(geojson);
8733 }
8734 },
8735
8736 // @method addData( <GeoJSON> data ): this
8737 // Adds a GeoJSON object to the layer.
8738 addData: function (geojson) {
8739 var features = isArray(geojson) ? geojson : geojson.features,
8740 i, len, feature;
8741
8742 if (features) {
8743 for (i = 0, len = features.length; i < len; i++) {
8744 // only add this if geometry or geometries are set and not null
8745 feature = features[i];
8746 if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
8747 this.addData(feature);
8748 }
8749 }
8750 return this;
8751 }
8752
8753 var options = this.options;
8754
8755 if (options.filter && !options.filter(geojson)) { return this; }
8756
8757 var layer = geometryToLayer(geojson, options);
8758 if (!layer) {
8759 return this;
8760 }
8761 layer.feature = asFeature(geojson);
8762
8763 layer.defaultOptions = layer.options;
8764 this.resetStyle(layer);
8765
8766 if (options.onEachFeature) {
8767 options.onEachFeature(geojson, layer);
8768 }
8769
8770 return this.addLayer(layer);
8771 },
8772
8773 // @method resetStyle( <Path> layer? ): this
8774 // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
8775 // If `layer` is omitted, the style of all features in the current layer is reset.
8776 resetStyle: function (layer) {
8777 if (layer === undefined) {
8778 return this.eachLayer(this.resetStyle, this);
8779 }
8780 // reset any custom styles
8781 layer.options = extend({}, layer.defaultOptions);
8782 this._setLayerStyle(layer, this.options.style);
8783 return this;
8784 },
8785
8786 // @method setStyle( <Function> style ): this
8787 // Changes styles of GeoJSON vector layers with the given style function.
8788 setStyle: function (style) {
8789 return this.eachLayer(function (layer) {
8790 this._setLayerStyle(layer, style);
8791 }, this);
8792 },
8793
8794 _setLayerStyle: function (layer, style) {
8795 if (layer.setStyle) {
8796 if (typeof style === 'function') {
8797 style = style(layer.feature);
8798 }
8799 layer.setStyle(style);
8800 }
8801 }
8802});
8803
8804// @section
8805// There are several static functions which can be called without instantiating L.GeoJSON:
8806
8807// @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer
8808// Creates a `Layer` from a given GeoJSON feature. Can use a custom
8809// [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng)
8810// functions if provided as options.
8811function geometryToLayer(geojson, options) {
8812
8813 var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
8814 coords = geometry ? geometry.coordinates : null,
8815 layers = [],
8816 pointToLayer = options && options.pointToLayer,
8817 _coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng,
8818 latlng, latlngs, i, len;
8819
8820 if (!coords && !geometry) {
8821 return null;
8822 }
8823
8824 switch (geometry.type) {
8825 case 'Point':
8826 latlng = _coordsToLatLng(coords);
8827 return _pointToLayer(pointToLayer, geojson, latlng, options);
8828
8829 case 'MultiPoint':
8830 for (i = 0, len = coords.length; i < len; i++) {
8831 latlng = _coordsToLatLng(coords[i]);
8832 layers.push(_pointToLayer(pointToLayer, geojson, latlng, options));
8833 }
8834 return new FeatureGroup(layers);
8835
8836 case 'LineString':
8837 case 'MultiLineString':
8838 latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng);
8839 return new Polyline(latlngs, options);
8840
8841 case 'Polygon':
8842 case 'MultiPolygon':
8843 latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng);
8844 return new Polygon(latlngs, options);
8845
8846 case 'GeometryCollection':
8847 for (i = 0, len = geometry.geometries.length; i < len; i++) {
8848 var layer = geometryToLayer({
8849 geometry: geometry.geometries[i],
8850 type: 'Feature',
8851 properties: geojson.properties
8852 }, options);
8853
8854 if (layer) {
8855 layers.push(layer);
8856 }
8857 }
8858 return new FeatureGroup(layers);
8859
8860 default:
8861 throw new Error('Invalid GeoJSON object.');
8862 }
8863}
8864
8865function _pointToLayer(pointToLayerFn, geojson, latlng, options) {
8866 return pointToLayerFn ?
8867 pointToLayerFn(geojson, latlng) :
8868 new Marker(latlng, options && options.markersInheritOptions && options);
8869}
8870
8871// @function coordsToLatLng(coords: Array): LatLng
8872// Creates a `LatLng` object from an array of 2 numbers (longitude, latitude)
8873// or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points.
8874function coordsToLatLng(coords) {
8875 return new LatLng(coords[1], coords[0], coords[2]);
8876}
8877
8878// @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array
8879// Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array.
8880// `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default).
8881// Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function.
8882function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) {
8883 var latlngs = [];
8884
8885 for (var i = 0, len = coords.length, latlng; i < len; i++) {
8886 latlng = levelsDeep ?
8887 coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) :
8888 (_coordsToLatLng || coordsToLatLng)(coords[i]);
8889
8890 latlngs.push(latlng);
8891 }
8892
8893 return latlngs;
8894}
8895
8896// @function latLngToCoords(latlng: LatLng, precision?: Number|false): Array
8897// Reverse of [`coordsToLatLng`](#geojson-coordstolatlng)
8898// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
8899function latLngToCoords(latlng, precision) {
8900 latlng = toLatLng(latlng);
8901 return latlng.alt !== undefined ?
8902 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision), formatNum(latlng.alt, precision)] :
8903 [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision)];
8904}
8905
8906// @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean, precision?: Number|false): Array
8907// Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs)
8908// `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.
8909// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
8910function latLngsToCoords(latlngs, levelsDeep, closed, precision) {
8911 var coords = [];
8912
8913 for (var i = 0, len = latlngs.length; i < len; i++) {
8914 coords.push(levelsDeep ?
8915 latLngsToCoords(latlngs[i], levelsDeep - 1, closed, precision) :
8916 latLngToCoords(latlngs[i], precision));
8917 }
8918
8919 if (!levelsDeep && closed) {
8920 coords.push(coords[0]);
8921 }
8922
8923 return coords;
8924}
8925
8926function getFeature(layer, newGeometry) {
8927 return layer.feature ?
8928 extend({}, layer.feature, {geometry: newGeometry}) :
8929 asFeature(newGeometry);
8930}
8931
8932// @function asFeature(geojson: Object): Object
8933// Normalize GeoJSON geometries/features into GeoJSON features.
8934function asFeature(geojson) {
8935 if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') {
8936 return geojson;
8937 }
8938
8939 return {
8940 type: 'Feature',
8941 properties: {},
8942 geometry: geojson
8943 };
8944}
8945
8946var PointToGeoJSON = {
8947 toGeoJSON: function (precision) {
8948 return getFeature(this, {
8949 type: 'Point',
8950 coordinates: latLngToCoords(this.getLatLng(), precision)
8951 });
8952 }
8953};
8954
8955// @namespace Marker
8956// @section Other methods
8957// @method toGeoJSON(precision?: Number|false): Object
8958// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
8959// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature).
8960Marker.include(PointToGeoJSON);
8961
8962// @namespace CircleMarker
8963// @method toGeoJSON(precision?: Number|false): Object
8964// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
8965// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature).
8966Circle.include(PointToGeoJSON);
8967CircleMarker.include(PointToGeoJSON);
8968
8969
8970// @namespace Polyline
8971// @method toGeoJSON(precision?: Number|false): Object
8972// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
8973// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature).
8974Polyline.include({
8975 toGeoJSON: function (precision) {
8976 var multi = !isFlat(this._latlngs);
8977
8978 var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision);
8979
8980 return getFeature(this, {
8981 type: (multi ? 'Multi' : '') + 'LineString',
8982 coordinates: coords
8983 });
8984 }
8985});
8986
8987// @namespace Polygon
8988// @method toGeoJSON(precision?: Number|false): Object
8989// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
8990// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature).
8991Polygon.include({
8992 toGeoJSON: function (precision) {
8993 var holes = !isFlat(this._latlngs),
8994 multi = holes && !isFlat(this._latlngs[0]);
8995
8996 var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision);
8997
8998 if (!holes) {
8999 coords = [coords];
9000 }
9001
9002 return getFeature(this, {
9003 type: (multi ? 'Multi' : '') + 'Polygon',
9004 coordinates: coords
9005 });
9006 }
9007});
9008
9009
9010// @namespace LayerGroup
9011LayerGroup.include({
9012 toMultiPoint: function (precision) {
9013 var coords = [];
9014
9015 this.eachLayer(function (layer) {
9016 coords.push(layer.toGeoJSON(precision).geometry.coordinates);
9017 });
9018
9019 return getFeature(this, {
9020 type: 'MultiPoint',
9021 coordinates: coords
9022 });
9023 },
9024
9025 // @method toGeoJSON(precision?: Number|false): Object
9026 // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
9027 // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`).
9028 toGeoJSON: function (precision) {
9029
9030 var type = this.feature && this.feature.geometry && this.feature.geometry.type;
9031
9032 if (type === 'MultiPoint') {
9033 return this.toMultiPoint(precision);
9034 }
9035
9036 var isGeometryCollection = type === 'GeometryCollection',
9037 jsons = [];
9038
9039 this.eachLayer(function (layer) {
9040 if (layer.toGeoJSON) {
9041 var json = layer.toGeoJSON(precision);
9042 if (isGeometryCollection) {
9043 jsons.push(json.geometry);
9044 } else {
9045 var feature = asFeature(json);
9046 // Squash nested feature collections
9047 if (feature.type === 'FeatureCollection') {
9048 jsons.push.apply(jsons, feature.features);
9049 } else {
9050 jsons.push(feature);
9051 }
9052 }
9053 }
9054 });
9055
9056 if (isGeometryCollection) {
9057 return getFeature(this, {
9058 geometries: jsons,
9059 type: 'GeometryCollection'
9060 });
9061 }
9062
9063 return {
9064 type: 'FeatureCollection',
9065 features: jsons
9066 };
9067 }
9068});
9069
9070// @namespace GeoJSON
9071// @factory L.geoJSON(geojson?: Object, options?: GeoJSON options)
9072// Creates a GeoJSON layer. Optionally accepts an object in
9073// [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map
9074// (you can alternatively add it later with `addData` method) and an `options` object.
9075function geoJSON(geojson, options) {
9076 return new GeoJSON(geojson, options);
9077}
9078
9079// Backward compatibility.
9080var geoJson = geoJSON;
9081
9082/*
9083 * @class ImageOverlay
9084 * @aka L.ImageOverlay
9085 * @inherits Interactive layer
9086 *
9087 * Used to load and display a single image over specific bounds of the map. Extends `Layer`.
9088 *
9089 * @example
9090 *
9091 * ```js
9092 * var imageUrl = 'https://maps.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
9093 * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
9094 * L.imageOverlay(imageUrl, imageBounds).addTo(map);
9095 * ```
9096 */
9097
9098var ImageOverlay = Layer.extend({
9099
9100 // @section
9101 // @aka ImageOverlay options
9102 options: {
9103 // @option opacity: Number = 1.0
9104 // The opacity of the image overlay.
9105 opacity: 1,
9106
9107 // @option alt: String = ''
9108 // Text for the `alt` attribute of the image (useful for accessibility).
9109 alt: '',
9110
9111 // @option interactive: Boolean = false
9112 // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
9113 interactive: false,
9114
9115 // @option crossOrigin: Boolean|String = false
9116 // Whether the crossOrigin attribute will be added to the image.
9117 // 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.
9118 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
9119 crossOrigin: false,
9120
9121 // @option errorOverlayUrl: String = ''
9122 // URL to the overlay image to show in place of the overlay that failed to load.
9123 errorOverlayUrl: '',
9124
9125 // @option zIndex: Number = 1
9126 // The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the overlay layer.
9127 zIndex: 1,
9128
9129 // @option className: String = ''
9130 // A custom class name to assign to the image. Empty by default.
9131 className: ''
9132 },
9133
9134 initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
9135 this._url = url;
9136 this._bounds = toLatLngBounds(bounds);
9137
9138 setOptions(this, options);
9139 },
9140
9141 onAdd: function () {
9142 if (!this._image) {
9143 this._initImage();
9144
9145 if (this.options.opacity < 1) {
9146 this._updateOpacity();
9147 }
9148 }
9149
9150 if (this.options.interactive) {
9151 addClass(this._image, 'leaflet-interactive');
9152 this.addInteractiveTarget(this._image);
9153 }
9154
9155 this.getPane().appendChild(this._image);
9156 this._reset();
9157 },
9158
9159 onRemove: function () {
9160 remove(this._image);
9161 if (this.options.interactive) {
9162 this.removeInteractiveTarget(this._image);
9163 }
9164 },
9165
9166 // @method setOpacity(opacity: Number): this
9167 // Sets the opacity of the overlay.
9168 setOpacity: function (opacity) {
9169 this.options.opacity = opacity;
9170
9171 if (this._image) {
9172 this._updateOpacity();
9173 }
9174 return this;
9175 },
9176
9177 setStyle: function (styleOpts) {
9178 if (styleOpts.opacity) {
9179 this.setOpacity(styleOpts.opacity);
9180 }
9181 return this;
9182 },
9183
9184 // @method bringToFront(): this
9185 // Brings the layer to the top of all overlays.
9186 bringToFront: function () {
9187 if (this._map) {
9188 toFront(this._image);
9189 }
9190 return this;
9191 },
9192
9193 // @method bringToBack(): this
9194 // Brings the layer to the bottom of all overlays.
9195 bringToBack: function () {
9196 if (this._map) {
9197 toBack(this._image);
9198 }
9199 return this;
9200 },
9201
9202 // @method setUrl(url: String): this
9203 // Changes the URL of the image.
9204 setUrl: function (url) {
9205 this._url = url;
9206
9207 if (this._image) {
9208 this._image.src = url;
9209 }
9210 return this;
9211 },
9212
9213 // @method setBounds(bounds: LatLngBounds): this
9214 // Update the bounds that this ImageOverlay covers
9215 setBounds: function (bounds) {
9216 this._bounds = toLatLngBounds(bounds);
9217
9218 if (this._map) {
9219 this._reset();
9220 }
9221 return this;
9222 },
9223
9224 getEvents: function () {
9225 var events = {
9226 zoom: this._reset,
9227 viewreset: this._reset
9228 };
9229
9230 if (this._zoomAnimated) {
9231 events.zoomanim = this._animateZoom;
9232 }
9233
9234 return events;
9235 },
9236
9237 // @method setZIndex(value: Number): this
9238 // Changes the [zIndex](#imageoverlay-zindex) of the image overlay.
9239 setZIndex: function (value) {
9240 this.options.zIndex = value;
9241 this._updateZIndex();
9242 return this;
9243 },
9244
9245 // @method getBounds(): LatLngBounds
9246 // Get the bounds that this ImageOverlay covers
9247 getBounds: function () {
9248 return this._bounds;
9249 },
9250
9251 // @method getElement(): HTMLElement
9252 // Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement)
9253 // used by this overlay.
9254 getElement: function () {
9255 return this._image;
9256 },
9257
9258 _initImage: function () {
9259 var wasElementSupplied = this._url.tagName === 'IMG';
9260 var img = this._image = wasElementSupplied ? this._url : create$1('img');
9261
9262 addClass(img, 'leaflet-image-layer');
9263 if (this._zoomAnimated) { addClass(img, 'leaflet-zoom-animated'); }
9264 if (this.options.className) { addClass(img, this.options.className); }
9265
9266 img.onselectstart = falseFn;
9267 img.onmousemove = falseFn;
9268
9269 // @event load: Event
9270 // Fired when the ImageOverlay layer has loaded its image
9271 img.onload = bind(this.fire, this, 'load');
9272 img.onerror = bind(this._overlayOnError, this, 'error');
9273
9274 if (this.options.crossOrigin || this.options.crossOrigin === '') {
9275 img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
9276 }
9277
9278 if (this.options.zIndex) {
9279 this._updateZIndex();
9280 }
9281
9282 if (wasElementSupplied) {
9283 this._url = img.src;
9284 return;
9285 }
9286
9287 img.src = this._url;
9288 img.alt = this.options.alt;
9289 },
9290
9291 _animateZoom: function (e) {
9292 var scale = this._map.getZoomScale(e.zoom),
9293 offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min;
9294
9295 setTransform(this._image, offset, scale);
9296 },
9297
9298 _reset: function () {
9299 var image = this._image,
9300 bounds = new Bounds(
9301 this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
9302 this._map.latLngToLayerPoint(this._bounds.getSouthEast())),
9303 size = bounds.getSize();
9304
9305 setPosition(image, bounds.min);
9306
9307 image.style.width = size.x + 'px';
9308 image.style.height = size.y + 'px';
9309 },
9310
9311 _updateOpacity: function () {
9312 setOpacity(this._image, this.options.opacity);
9313 },
9314
9315 _updateZIndex: function () {
9316 if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) {
9317 this._image.style.zIndex = this.options.zIndex;
9318 }
9319 },
9320
9321 _overlayOnError: function () {
9322 // @event error: Event
9323 // Fired when the ImageOverlay layer fails to load its image
9324 this.fire('error');
9325
9326 var errorUrl = this.options.errorOverlayUrl;
9327 if (errorUrl && this._url !== errorUrl) {
9328 this._url = errorUrl;
9329 this._image.src = errorUrl;
9330 }
9331 },
9332
9333 // @method getCenter(): LatLng
9334 // Returns the center of the ImageOverlay.
9335 getCenter: function () {
9336 return this._bounds.getCenter();
9337 }
9338});
9339
9340// @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
9341// Instantiates an image overlay object given the URL of the image and the
9342// geographical bounds it is tied to.
9343var imageOverlay = function (url, bounds, options) {
9344 return new ImageOverlay(url, bounds, options);
9345};
9346
9347/*
9348 * @class VideoOverlay
9349 * @aka L.VideoOverlay
9350 * @inherits ImageOverlay
9351 *
9352 * Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`.
9353 *
9354 * A video overlay uses the [`<video>`](https://developer.mozilla.org/docs/Web/HTML/Element/video)
9355 * HTML5 element.
9356 *
9357 * @example
9358 *
9359 * ```js
9360 * var videoUrl = 'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
9361 * videoBounds = [[ 32, -130], [ 13, -100]];
9362 * L.videoOverlay(videoUrl, videoBounds ).addTo(map);
9363 * ```
9364 */
9365
9366var VideoOverlay = ImageOverlay.extend({
9367
9368 // @section
9369 // @aka VideoOverlay options
9370 options: {
9371 // @option autoplay: Boolean = true
9372 // Whether the video starts playing automatically when loaded.
9373 // On some browsers autoplay will only work with `muted: true`
9374 autoplay: true,
9375
9376 // @option loop: Boolean = true
9377 // Whether the video will loop back to the beginning when played.
9378 loop: true,
9379
9380 // @option keepAspectRatio: Boolean = true
9381 // Whether the video will save aspect ratio after the projection.
9382 // Relevant for supported browsers. See [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
9383 keepAspectRatio: true,
9384
9385 // @option muted: Boolean = false
9386 // Whether the video starts on mute when loaded.
9387 muted: false,
9388
9389 // @option playsInline: Boolean = true
9390 // Mobile browsers will play the video right where it is instead of open it up in fullscreen mode.
9391 playsInline: true
9392 },
9393
9394 _initImage: function () {
9395 var wasElementSupplied = this._url.tagName === 'VIDEO';
9396 var vid = this._image = wasElementSupplied ? this._url : create$1('video');
9397
9398 addClass(vid, 'leaflet-image-layer');
9399 if (this._zoomAnimated) { addClass(vid, 'leaflet-zoom-animated'); }
9400 if (this.options.className) { addClass(vid, this.options.className); }
9401
9402 vid.onselectstart = falseFn;
9403 vid.onmousemove = falseFn;
9404
9405 // @event load: Event
9406 // Fired when the video has finished loading the first frame
9407 vid.onloadeddata = bind(this.fire, this, 'load');
9408
9409 if (wasElementSupplied) {
9410 var sourceElements = vid.getElementsByTagName('source');
9411 var sources = [];
9412 for (var j = 0; j < sourceElements.length; j++) {
9413 sources.push(sourceElements[j].src);
9414 }
9415
9416 this._url = (sourceElements.length > 0) ? sources : [vid.src];
9417 return;
9418 }
9419
9420 if (!isArray(this._url)) { this._url = [this._url]; }
9421
9422 if (!this.options.keepAspectRatio && Object.prototype.hasOwnProperty.call(vid.style, 'objectFit')) {
9423 vid.style['objectFit'] = 'fill';
9424 }
9425 vid.autoplay = !!this.options.autoplay;
9426 vid.loop = !!this.options.loop;
9427 vid.muted = !!this.options.muted;
9428 vid.playsInline = !!this.options.playsInline;
9429 for (var i = 0; i < this._url.length; i++) {
9430 var source = create$1('source');
9431 source.src = this._url[i];
9432 vid.appendChild(source);
9433 }
9434 }
9435
9436 // @method getElement(): HTMLVideoElement
9437 // Returns the instance of [`HTMLVideoElement`](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement)
9438 // used by this overlay.
9439});
9440
9441
9442// @factory L.videoOverlay(video: String|Array|HTMLVideoElement, bounds: LatLngBounds, options?: VideoOverlay options)
9443// Instantiates an image overlay object given the URL of the video (or array of URLs, or even a video element) and the
9444// geographical bounds it is tied to.
9445
9446function videoOverlay(video, bounds, options) {
9447 return new VideoOverlay(video, bounds, options);
9448}
9449
9450/*
9451 * @class SVGOverlay
9452 * @aka L.SVGOverlay
9453 * @inherits ImageOverlay
9454 *
9455 * Used to load, display and provide DOM access to an SVG file over specific bounds of the map. Extends `ImageOverlay`.
9456 *
9457 * An SVG overlay uses the [`<svg>`](https://developer.mozilla.org/docs/Web/SVG/Element/svg) element.
9458 *
9459 * @example
9460 *
9461 * ```js
9462 * var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
9463 * svgElement.setAttribute('xmlns', "http://www.w3.org/2000/svg");
9464 * svgElement.setAttribute('viewBox', "0 0 200 200");
9465 * 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"/>';
9466 * var svgElementBounds = [ [ 32, -130 ], [ 13, -100 ] ];
9467 * L.svgOverlay(svgElement, svgElementBounds).addTo(map);
9468 * ```
9469 */
9470
9471var SVGOverlay = ImageOverlay.extend({
9472 _initImage: function () {
9473 var el = this._image = this._url;
9474
9475 addClass(el, 'leaflet-image-layer');
9476 if (this._zoomAnimated) { addClass(el, 'leaflet-zoom-animated'); }
9477 if (this.options.className) { addClass(el, this.options.className); }
9478
9479 el.onselectstart = falseFn;
9480 el.onmousemove = falseFn;
9481 }
9482
9483 // @method getElement(): SVGElement
9484 // Returns the instance of [`SVGElement`](https://developer.mozilla.org/docs/Web/API/SVGElement)
9485 // used by this overlay.
9486});
9487
9488
9489// @factory L.svgOverlay(svg: String|SVGElement, bounds: LatLngBounds, options?: SVGOverlay options)
9490// Instantiates an image overlay object given an SVG element and the geographical bounds it is tied to.
9491// A viewBox attribute is required on the SVG element to zoom in and out properly.
9492
9493function svgOverlay(el, bounds, options) {
9494 return new SVGOverlay(el, bounds, options);
9495}
9496
9497/*
9498 * @class DivOverlay
9499 * @inherits Interactive layer
9500 * @aka L.DivOverlay
9501 * Base model for L.Popup and L.Tooltip. Inherit from it for custom overlays like plugins.
9502 */
9503
9504// @namespace DivOverlay
9505var DivOverlay = Layer.extend({
9506
9507 // @section
9508 // @aka DivOverlay options
9509 options: {
9510 // @option interactive: Boolean = false
9511 // If true, the popup/tooltip will listen to the mouse events.
9512 interactive: false,
9513
9514 // @option offset: Point = Point(0, 0)
9515 // The offset of the overlay position.
9516 offset: [0, 0],
9517
9518 // @option className: String = ''
9519 // A custom CSS class name to assign to the overlay.
9520 className: '',
9521
9522 // @option pane: String = undefined
9523 // `Map pane` where the overlay will be added.
9524 pane: undefined
9525 },
9526
9527 initialize: function (options, source) {
9528 setOptions(this, options);
9529
9530 this._source = source;
9531 },
9532
9533 // @method openOn(map: Map): this
9534 // Adds the overlay to the map.
9535 // Alternative to `map.openPopup(popup)`/`.openTooltip(tooltip)`.
9536 openOn: function (map) {
9537 map = arguments.length ? map : this._source._map; // experimental, not the part of public api
9538 if (!map.hasLayer(this)) {
9539 map.addLayer(this);
9540 }
9541 return this;
9542 },
9543
9544 // @method close(): this
9545 // Closes the overlay.
9546 // Alternative to `map.closePopup(popup)`/`.closeTooltip(tooltip)`
9547 // and `layer.closePopup()`/`.closeTooltip()`.
9548 close: function () {
9549 if (this._map) {
9550 this._map.removeLayer(this);
9551 }
9552 return this;
9553 },
9554
9555 // @method toggle(layer?: Layer): this
9556 // Opens or closes the overlay bound to layer depending on its current state.
9557 // Argument may be omitted only for overlay bound to layer.
9558 // Alternative to `layer.togglePopup()`/`.toggleTooltip()`.
9559 toggle: function (layer) {
9560 if (this._map) {
9561 this.close();
9562 } else {
9563 if (arguments.length) {
9564 this._source = layer;
9565 } else {
9566 layer = this._source;
9567 }
9568 this._prepareOpen();
9569
9570 // open the overlay on the map
9571 this.openOn(layer._map);
9572 }
9573 return this;
9574 },
9575
9576 onAdd: function (map) {
9577 this._zoomAnimated = map._zoomAnimated;
9578
9579 if (!this._container) {
9580 this._initLayout();
9581 }
9582
9583 if (map._fadeAnimated) {
9584 setOpacity(this._container, 0);
9585 }
9586
9587 clearTimeout(this._removeTimeout);
9588 this.getPane().appendChild(this._container);
9589 this.update();
9590
9591 if (map._fadeAnimated) {
9592 setOpacity(this._container, 1);
9593 }
9594
9595 this.bringToFront();
9596
9597 if (this.options.interactive) {
9598 addClass(this._container, 'leaflet-interactive');
9599 this.addInteractiveTarget(this._container);
9600 }
9601 },
9602
9603 onRemove: function (map) {
9604 if (map._fadeAnimated) {
9605 setOpacity(this._container, 0);
9606 this._removeTimeout = setTimeout(bind(remove, undefined, this._container), 200);
9607 } else {
9608 remove(this._container);
9609 }
9610
9611 if (this.options.interactive) {
9612 removeClass(this._container, 'leaflet-interactive');
9613 this.removeInteractiveTarget(this._container);
9614 }
9615 },
9616
9617 // @namespace DivOverlay
9618 // @method getLatLng: LatLng
9619 // Returns the geographical point of the overlay.
9620 getLatLng: function () {
9621 return this._latlng;
9622 },
9623
9624 // @method setLatLng(latlng: LatLng): this
9625 // Sets the geographical point where the overlay will open.
9626 setLatLng: function (latlng) {
9627 this._latlng = toLatLng(latlng);
9628 if (this._map) {
9629 this._updatePosition();
9630 this._adjustPan();
9631 }
9632 return this;
9633 },
9634
9635 // @method getContent: String|HTMLElement
9636 // Returns the content of the overlay.
9637 getContent: function () {
9638 return this._content;
9639 },
9640
9641 // @method setContent(htmlContent: String|HTMLElement|Function): this
9642 // Sets the HTML content of the overlay. If a function is passed the source layer will be passed to the function.
9643 // The function should return a `String` or `HTMLElement` to be used in the overlay.
9644 setContent: function (content) {
9645 this._content = content;
9646 this.update();
9647 return this;
9648 },
9649
9650 // @method getElement: String|HTMLElement
9651 // Returns the HTML container of the overlay.
9652 getElement: function () {
9653 return this._container;
9654 },
9655
9656 // @method update: null
9657 // Updates the overlay content, layout and position. Useful for updating the overlay after something inside changed, e.g. image loaded.
9658 update: function () {
9659 if (!this._map) { return; }
9660
9661 this._container.style.visibility = 'hidden';
9662
9663 this._updateContent();
9664 this._updateLayout();
9665 this._updatePosition();
9666
9667 this._container.style.visibility = '';
9668
9669 this._adjustPan();
9670 },
9671
9672 getEvents: function () {
9673 var events = {
9674 zoom: this._updatePosition,
9675 viewreset: this._updatePosition
9676 };
9677
9678 if (this._zoomAnimated) {
9679 events.zoomanim = this._animateZoom;
9680 }
9681 return events;
9682 },
9683
9684 // @method isOpen: Boolean
9685 // Returns `true` when the overlay is visible on the map.
9686 isOpen: function () {
9687 return !!this._map && this._map.hasLayer(this);
9688 },
9689
9690 // @method bringToFront: this
9691 // Brings this overlay in front of other overlays (in the same map pane).
9692 bringToFront: function () {
9693 if (this._map) {
9694 toFront(this._container);
9695 }
9696 return this;
9697 },
9698
9699 // @method bringToBack: this
9700 // Brings this overlay to the back of other overlays (in the same map pane).
9701 bringToBack: function () {
9702 if (this._map) {
9703 toBack(this._container);
9704 }
9705 return this;
9706 },
9707
9708 // prepare bound overlay to open: update latlng pos / content source (for FeatureGroup)
9709 _prepareOpen: function (latlng) {
9710 var source = this._source;
9711 if (!source._map) { return false; }
9712
9713 if (source instanceof FeatureGroup) {
9714 source = null;
9715 var layers = this._source._layers;
9716 for (var id in layers) {
9717 if (layers[id]._map) {
9718 source = layers[id];
9719 break;
9720 }
9721 }
9722 if (!source) { return false; } // Unable to get source layer.
9723
9724 // set overlay source to this layer
9725 this._source = source;
9726 }
9727
9728 if (!latlng) {
9729 if (source.getCenter) {
9730 latlng = source.getCenter();
9731 } else if (source.getLatLng) {
9732 latlng = source.getLatLng();
9733 } else if (source.getBounds) {
9734 latlng = source.getBounds().getCenter();
9735 } else {
9736 throw new Error('Unable to get source layer LatLng.');
9737 }
9738 }
9739 this.setLatLng(latlng);
9740
9741 if (this._map) {
9742 // update the overlay (content, layout, etc...)
9743 this.update();
9744 }
9745
9746 return true;
9747 },
9748
9749 _updateContent: function () {
9750 if (!this._content) { return; }
9751
9752 var node = this._contentNode;
9753 var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
9754
9755 if (typeof content === 'string') {
9756 node.innerHTML = content;
9757 } else {
9758 while (node.hasChildNodes()) {
9759 node.removeChild(node.firstChild);
9760 }
9761 node.appendChild(content);
9762 }
9763
9764 // @namespace DivOverlay
9765 // @section DivOverlay events
9766 // @event contentupdate: Event
9767 // Fired when the content of the overlay is updated
9768 this.fire('contentupdate');
9769 },
9770
9771 _updatePosition: function () {
9772 if (!this._map) { return; }
9773
9774 var pos = this._map.latLngToLayerPoint(this._latlng),
9775 offset = toPoint(this.options.offset),
9776 anchor = this._getAnchor();
9777
9778 if (this._zoomAnimated) {
9779 setPosition(this._container, pos.add(anchor));
9780 } else {
9781 offset = offset.add(pos).add(anchor);
9782 }
9783
9784 var bottom = this._containerBottom = -offset.y,
9785 left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x;
9786
9787 // bottom position the overlay in case the height of the overlay changes (images loading etc)
9788 this._container.style.bottom = bottom + 'px';
9789 this._container.style.left = left + 'px';
9790 },
9791
9792 _getAnchor: function () {
9793 return [0, 0];
9794 }
9795
9796});
9797
9798Map.include({
9799 _initOverlay: function (OverlayClass, content, latlng, options) {
9800 var overlay = content;
9801 if (!(overlay instanceof OverlayClass)) {
9802 overlay = new OverlayClass(options).setContent(content);
9803 }
9804 if (latlng) {
9805 overlay.setLatLng(latlng);
9806 }
9807 return overlay;
9808 }
9809});
9810
9811
9812Layer.include({
9813 _initOverlay: function (OverlayClass, old, content, options) {
9814 var overlay = content;
9815 if (overlay instanceof OverlayClass) {
9816 setOptions(overlay, options);
9817 overlay._source = this;
9818 } else {
9819 overlay = (old && !options) ? old : new OverlayClass(options, this);
9820 overlay.setContent(content);
9821 }
9822 return overlay;
9823 }
9824});
9825
9826/*
9827 * @class Popup
9828 * @inherits DivOverlay
9829 * @aka L.Popup
9830 * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
9831 * open popups while making sure that only one popup is open at one time
9832 * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
9833 *
9834 * @example
9835 *
9836 * If you want to just bind a popup to marker click and then open it, it's really easy:
9837 *
9838 * ```js
9839 * marker.bindPopup(popupContent).openPopup();
9840 * ```
9841 * Path overlays like polylines also have a `bindPopup` method.
9842 * Here's a more complicated way to open a popup on a map:
9843 *
9844 * ```js
9845 * var popup = L.popup()
9846 * .setLatLng(latlng)
9847 * .setContent('<p>Hello world!<br />This is a nice popup.</p>')
9848 * .openOn(map);
9849 * ```
9850 */
9851
9852
9853// @namespace Popup
9854var Popup = DivOverlay.extend({
9855
9856 // @section
9857 // @aka Popup options
9858 options: {
9859 // @option pane: String = 'popupPane'
9860 // `Map pane` where the popup will be added.
9861 pane: 'popupPane',
9862
9863 // @option offset: Point = Point(0, 7)
9864 // The offset of the popup position.
9865 offset: [0, 7],
9866
9867 // @option maxWidth: Number = 300
9868 // Max width of the popup, in pixels.
9869 maxWidth: 300,
9870
9871 // @option minWidth: Number = 50
9872 // Min width of the popup, in pixels.
9873 minWidth: 50,
9874
9875 // @option maxHeight: Number = null
9876 // If set, creates a scrollable container of the given height
9877 // inside a popup if its content exceeds it.
9878 maxHeight: null,
9879
9880 // @option autoPan: Boolean = true
9881 // Set it to `false` if you don't want the map to do panning animation
9882 // to fit the opened popup.
9883 autoPan: true,
9884
9885 // @option autoPanPaddingTopLeft: Point = null
9886 // The margin between the popup and the top left corner of the map
9887 // view after autopanning was performed.
9888 autoPanPaddingTopLeft: null,
9889
9890 // @option autoPanPaddingBottomRight: Point = null
9891 // The margin between the popup and the bottom right corner of the map
9892 // view after autopanning was performed.
9893 autoPanPaddingBottomRight: null,
9894
9895 // @option autoPanPadding: Point = Point(5, 5)
9896 // Equivalent of setting both top left and bottom right autopan padding to the same value.
9897 autoPanPadding: [5, 5],
9898
9899 // @option keepInView: Boolean = false
9900 // Set it to `true` if you want to prevent users from panning the popup
9901 // off of the screen while it is open.
9902 keepInView: false,
9903
9904 // @option closeButton: Boolean = true
9905 // Controls the presence of a close button in the popup.
9906 closeButton: true,
9907
9908 // @option autoClose: Boolean = true
9909 // Set it to `false` if you want to override the default behavior of
9910 // the popup closing when another popup is opened.
9911 autoClose: true,
9912
9913 // @option closeOnEscapeKey: Boolean = true
9914 // Set it to `false` if you want to override the default behavior of
9915 // the ESC key for closing of the popup.
9916 closeOnEscapeKey: true,
9917
9918 // @option closeOnClick: Boolean = *
9919 // Set it if you want to override the default behavior of the popup closing when user clicks
9920 // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option.
9921
9922 // @option className: String = ''
9923 // A custom CSS class name to assign to the popup.
9924 className: ''
9925 },
9926
9927 // @namespace Popup
9928 // @method openOn(map: Map): this
9929 // Alternative to `map.openPopup(popup)`.
9930 // Adds the popup to the map and closes the previous one.
9931 openOn: function (map) {
9932 map = arguments.length ? map : this._source._map; // experimental, not the part of public api
9933
9934 if (!map.hasLayer(this) && map._popup && map._popup.options.autoClose) {
9935 map.removeLayer(map._popup);
9936 }
9937 map._popup = this;
9938
9939 return DivOverlay.prototype.openOn.call(this, map);
9940 },
9941
9942 onAdd: function (map) {
9943 DivOverlay.prototype.onAdd.call(this, map);
9944
9945 // @namespace Map
9946 // @section Popup events
9947 // @event popupopen: PopupEvent
9948 // Fired when a popup is opened in the map
9949 map.fire('popupopen', {popup: this});
9950
9951 if (this._source) {
9952 // @namespace Layer
9953 // @section Popup events
9954 // @event popupopen: PopupEvent
9955 // Fired when a popup bound to this layer is opened
9956 this._source.fire('popupopen', {popup: this}, true);
9957 // For non-path layers, we toggle the popup when clicking
9958 // again the layer, so prevent the map to reopen it.
9959 if (!(this._source instanceof Path)) {
9960 this._source.on('preclick', stopPropagation);
9961 }
9962 }
9963 },
9964
9965 onRemove: function (map) {
9966 DivOverlay.prototype.onRemove.call(this, map);
9967
9968 // @namespace Map
9969 // @section Popup events
9970 // @event popupclose: PopupEvent
9971 // Fired when a popup in the map is closed
9972 map.fire('popupclose', {popup: this});
9973
9974 if (this._source) {
9975 // @namespace Layer
9976 // @section Popup events
9977 // @event popupclose: PopupEvent
9978 // Fired when a popup bound to this layer is closed
9979 this._source.fire('popupclose', {popup: this}, true);
9980 if (!(this._source instanceof Path)) {
9981 this._source.off('preclick', stopPropagation);
9982 }
9983 }
9984 },
9985
9986 getEvents: function () {
9987 var events = DivOverlay.prototype.getEvents.call(this);
9988
9989 if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
9990 events.preclick = this.close;
9991 }
9992
9993 if (this.options.keepInView) {
9994 events.moveend = this._adjustPan;
9995 }
9996
9997 return events;
9998 },
9999
10000 _initLayout: function () {
10001 var prefix = 'leaflet-popup',
10002 container = this._container = create$1('div',
10003 prefix + ' ' + (this.options.className || '') +
10004 ' leaflet-zoom-animated');
10005
10006 var wrapper = this._wrapper = create$1('div', prefix + '-content-wrapper', container);
10007 this._contentNode = create$1('div', prefix + '-content', wrapper);
10008
10009 disableClickPropagation(container);
10010 disableScrollPropagation(this._contentNode);
10011 on(container, 'contextmenu', stopPropagation);
10012
10013 this._tipContainer = create$1('div', prefix + '-tip-container', container);
10014 this._tip = create$1('div', prefix + '-tip', this._tipContainer);
10015
10016 if (this.options.closeButton) {
10017 var closeButton = this._closeButton = create$1('a', prefix + '-close-button', container);
10018 closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
10019 closeButton.setAttribute('aria-label', 'Close popup');
10020 closeButton.href = '#close';
10021 closeButton.innerHTML = '<span aria-hidden="true">&#215;</span>';
10022
10023 on(closeButton, 'click', this.close, this);
10024 }
10025 },
10026
10027 _updateLayout: function () {
10028 var container = this._contentNode,
10029 style = container.style;
10030
10031 style.width = '';
10032 style.whiteSpace = 'nowrap';
10033
10034 var width = container.offsetWidth;
10035 width = Math.min(width, this.options.maxWidth);
10036 width = Math.max(width, this.options.minWidth);
10037
10038 style.width = (width + 1) + 'px';
10039 style.whiteSpace = '';
10040
10041 style.height = '';
10042
10043 var height = container.offsetHeight,
10044 maxHeight = this.options.maxHeight,
10045 scrolledClass = 'leaflet-popup-scrolled';
10046
10047 if (maxHeight && height > maxHeight) {
10048 style.height = maxHeight + 'px';
10049 addClass(container, scrolledClass);
10050 } else {
10051 removeClass(container, scrolledClass);
10052 }
10053
10054 this._containerWidth = this._container.offsetWidth;
10055 },
10056
10057 _animateZoom: function (e) {
10058 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
10059 anchor = this._getAnchor();
10060 setPosition(this._container, pos.add(anchor));
10061 },
10062
10063 _adjustPan: function (e) {
10064 if (!this.options.autoPan) { return; }
10065 if (this._map._panAnim) { this._map._panAnim.stop(); }
10066
10067 var map = this._map,
10068 marginBottom = parseInt(getStyle(this._container, 'marginBottom'), 10) || 0,
10069 containerHeight = this._container.offsetHeight + marginBottom,
10070 containerWidth = this._containerWidth,
10071 layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom);
10072
10073 layerPos._add(getPosition(this._container));
10074
10075 var containerPos = map.layerPointToContainerPoint(layerPos),
10076 padding = toPoint(this.options.autoPanPadding),
10077 paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding),
10078 paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding),
10079 size = map.getSize(),
10080 dx = 0,
10081 dy = 0;
10082
10083 if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
10084 dx = containerPos.x + containerWidth - size.x + paddingBR.x;
10085 }
10086 if (containerPos.x - dx - paddingTL.x < 0) { // left
10087 dx = containerPos.x - paddingTL.x;
10088 }
10089 if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
10090 dy = containerPos.y + containerHeight - size.y + paddingBR.y;
10091 }
10092 if (containerPos.y - dy - paddingTL.y < 0) { // top
10093 dy = containerPos.y - paddingTL.y;
10094 }
10095
10096 // @namespace Map
10097 // @section Popup events
10098 // @event autopanstart: Event
10099 // Fired when the map starts autopanning when opening a popup.
10100 if (dx || dy) {
10101 map
10102 .fire('autopanstart')
10103 .panBy([dx, dy], {animate: e && e.type === 'moveend'});
10104 }
10105 },
10106
10107 _getAnchor: function () {
10108 // Where should we anchor the popup on the source layer?
10109 return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
10110 }
10111
10112});
10113
10114// @namespace Popup
10115// @factory L.popup(options?: Popup options, source?: Layer)
10116// 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.
10117var popup = function (options, source) {
10118 return new Popup(options, source);
10119};
10120
10121
10122/* @namespace Map
10123 * @section Interaction Options
10124 * @option closePopupOnClick: Boolean = true
10125 * Set it to `false` if you don't want popups to close when user clicks the map.
10126 */
10127Map.mergeOptions({
10128 closePopupOnClick: true
10129});
10130
10131
10132// @namespace Map
10133// @section Methods for Layers and Controls
10134Map.include({
10135 // @method openPopup(popup: Popup): this
10136 // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
10137 // @alternative
10138 // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
10139 // Creates a popup with the specified content and options and opens it in the given point on a map.
10140 openPopup: function (popup, latlng, options) {
10141 this._initOverlay(Popup, popup, latlng, options)
10142 .openOn(this);
10143
10144 return this;
10145 },
10146
10147 // @method closePopup(popup?: Popup): this
10148 // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
10149 closePopup: function (popup) {
10150 popup = arguments.length ? popup : this._popup;
10151 if (popup) {
10152 popup.close();
10153 }
10154 return this;
10155 }
10156});
10157
10158/*
10159 * @namespace Layer
10160 * @section Popup methods example
10161 *
10162 * All layers share a set of methods convenient for binding popups to it.
10163 *
10164 * ```js
10165 * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
10166 * layer.openPopup();
10167 * layer.closePopup();
10168 * ```
10169 *
10170 * 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.
10171 */
10172
10173// @section Popup methods
10174Layer.include({
10175
10176 // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
10177 // Binds a popup to the layer with the passed `content` and sets up the
10178 // necessary event listeners. If a `Function` is passed it will receive
10179 // the layer as the first argument and should return a `String` or `HTMLElement`.
10180 bindPopup: function (content, options) {
10181 this._popup = this._initOverlay(Popup, this._popup, content, options);
10182 if (!this._popupHandlersAdded) {
10183 this.on({
10184 click: this._openPopup,
10185 keypress: this._onKeyPress,
10186 remove: this.closePopup,
10187 move: this._movePopup
10188 });
10189 this._popupHandlersAdded = true;
10190 }
10191
10192 return this;
10193 },
10194
10195 // @method unbindPopup(): this
10196 // Removes the popup previously bound with `bindPopup`.
10197 unbindPopup: function () {
10198 if (this._popup) {
10199 this.off({
10200 click: this._openPopup,
10201 keypress: this._onKeyPress,
10202 remove: this.closePopup,
10203 move: this._movePopup
10204 });
10205 this._popupHandlersAdded = false;
10206 this._popup = null;
10207 }
10208 return this;
10209 },
10210
10211 // @method openPopup(latlng?: LatLng): this
10212 // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed.
10213 openPopup: function (latlng) {
10214 if (this._popup && this._popup._prepareOpen(latlng)) {
10215 // open the popup on the map
10216 this._popup.openOn(this._map);
10217 }
10218 return this;
10219 },
10220
10221 // @method closePopup(): this
10222 // Closes the popup bound to this layer if it is open.
10223 closePopup: function () {
10224 if (this._popup) {
10225 this._popup.close();
10226 }
10227 return this;
10228 },
10229
10230 // @method togglePopup(): this
10231 // Opens or closes the popup bound to this layer depending on its current state.
10232 togglePopup: function () {
10233 if (this._popup) {
10234 this._popup.toggle(this);
10235 }
10236 return this;
10237 },
10238
10239 // @method isPopupOpen(): boolean
10240 // Returns `true` if the popup bound to this layer is currently open.
10241 isPopupOpen: function () {
10242 return (this._popup ? this._popup.isOpen() : false);
10243 },
10244
10245 // @method setPopupContent(content: String|HTMLElement|Popup): this
10246 // Sets the content of the popup bound to this layer.
10247 setPopupContent: function (content) {
10248 if (this._popup) {
10249 this._popup.setContent(content);
10250 }
10251 return this;
10252 },
10253
10254 // @method getPopup(): Popup
10255 // Returns the popup bound to this layer.
10256 getPopup: function () {
10257 return this._popup;
10258 },
10259
10260 _openPopup: function (e) {
10261 if (!this._popup || !this._map) {
10262 return;
10263 }
10264 // prevent map click
10265 stop(e);
10266
10267 var target = e.layer || e.target;
10268 if (this._popup._source === target && !(target instanceof Path)) {
10269 // treat it like a marker and figure out
10270 // if we should toggle it open/closed
10271 if (this._map.hasLayer(this._popup)) {
10272 this.closePopup();
10273 } else {
10274 this.openPopup(e.latlng);
10275 }
10276 return;
10277 }
10278 this._popup._source = target;
10279 this.openPopup(e.latlng);
10280 },
10281
10282 _movePopup: function (e) {
10283 this._popup.setLatLng(e.latlng);
10284 },
10285
10286 _onKeyPress: function (e) {
10287 if (e.originalEvent.keyCode === 13) {
10288 this._openPopup(e);
10289 }
10290 }
10291});
10292
10293/*
10294 * @class Tooltip
10295 * @inherits DivOverlay
10296 * @aka L.Tooltip
10297 * Used to display small texts on top of map layers.
10298 *
10299 * @example
10300 *
10301 * ```js
10302 * marker.bindTooltip("my tooltip text").openTooltip();
10303 * ```
10304 * Note about tooltip offset. Leaflet takes two options in consideration
10305 * for computing tooltip offsetting:
10306 * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
10307 * Add a positive x offset to move the tooltip to the right, and a positive y offset to
10308 * move it to the bottom. Negatives will move to the left and top.
10309 * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
10310 * should adapt this value if you use a custom icon.
10311 */
10312
10313
10314// @namespace Tooltip
10315var Tooltip = DivOverlay.extend({
10316
10317 // @section
10318 // @aka Tooltip options
10319 options: {
10320 // @option pane: String = 'tooltipPane'
10321 // `Map pane` where the tooltip will be added.
10322 pane: 'tooltipPane',
10323
10324 // @option offset: Point = Point(0, 0)
10325 // Optional offset of the tooltip position.
10326 offset: [0, 0],
10327
10328 // @option direction: String = 'auto'
10329 // Direction where to open the tooltip. Possible values are: `right`, `left`,
10330 // `top`, `bottom`, `center`, `auto`.
10331 // `auto` will dynamically switch between `right` and `left` according to the tooltip
10332 // position on the map.
10333 direction: 'auto',
10334
10335 // @option permanent: Boolean = false
10336 // Whether to open the tooltip permanently or only on mouseover.
10337 permanent: false,
10338
10339 // @option sticky: Boolean = false
10340 // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
10341 sticky: false,
10342
10343 // @option opacity: Number = 0.9
10344 // Tooltip container opacity.
10345 opacity: 0.9
10346 },
10347
10348 onAdd: function (map) {
10349 DivOverlay.prototype.onAdd.call(this, map);
10350 this.setOpacity(this.options.opacity);
10351
10352 // @namespace Map
10353 // @section Tooltip events
10354 // @event tooltipopen: TooltipEvent
10355 // Fired when a tooltip is opened in the map.
10356 map.fire('tooltipopen', {tooltip: this});
10357
10358 if (this._source) {
10359 this.addEventParent(this._source);
10360
10361 // @namespace Layer
10362 // @section Tooltip events
10363 // @event tooltipopen: TooltipEvent
10364 // Fired when a tooltip bound to this layer is opened.
10365 this._source.fire('tooltipopen', {tooltip: this}, true);
10366 }
10367 },
10368
10369 onRemove: function (map) {
10370 DivOverlay.prototype.onRemove.call(this, map);
10371
10372 // @namespace Map
10373 // @section Tooltip events
10374 // @event tooltipclose: TooltipEvent
10375 // Fired when a tooltip in the map is closed.
10376 map.fire('tooltipclose', {tooltip: this});
10377
10378 if (this._source) {
10379 this.removeEventParent(this._source);
10380
10381 // @namespace Layer
10382 // @section Tooltip events
10383 // @event tooltipclose: TooltipEvent
10384 // Fired when a tooltip bound to this layer is closed.
10385 this._source.fire('tooltipclose', {tooltip: this}, true);
10386 }
10387 },
10388
10389 getEvents: function () {
10390 var events = DivOverlay.prototype.getEvents.call(this);
10391
10392 if (!this.options.permanent) {
10393 events.preclick = this.close;
10394 }
10395
10396 return events;
10397 },
10398
10399 _initLayout: function () {
10400 var prefix = 'leaflet-tooltip',
10401 className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
10402
10403 this._contentNode = this._container = create$1('div', className);
10404 },
10405
10406 _updateLayout: function () {},
10407
10408 _adjustPan: function () {},
10409
10410 _setPosition: function (pos) {
10411 var subX, subY,
10412 map = this._map,
10413 container = this._container,
10414 centerPoint = map.latLngToContainerPoint(map.getCenter()),
10415 tooltipPoint = map.layerPointToContainerPoint(pos),
10416 direction = this.options.direction,
10417 tooltipWidth = container.offsetWidth,
10418 tooltipHeight = container.offsetHeight,
10419 offset = toPoint(this.options.offset),
10420 anchor = this._getAnchor();
10421
10422 if (direction === 'top') {
10423 subX = tooltipWidth / 2;
10424 subY = tooltipHeight;
10425 } else if (direction === 'bottom') {
10426 subX = tooltipWidth / 2;
10427 subY = 0;
10428 } else if (direction === 'center') {
10429 subX = tooltipWidth / 2;
10430 subY = tooltipHeight / 2;
10431 } else if (direction === 'right') {
10432 subX = 0;
10433 subY = tooltipHeight / 2;
10434 } else if (direction === 'left') {
10435 subX = tooltipWidth;
10436 subY = tooltipHeight / 2;
10437 } else if (tooltipPoint.x < centerPoint.x) {
10438 direction = 'right';
10439 subX = 0;
10440 subY = tooltipHeight / 2;
10441 } else {
10442 direction = 'left';
10443 subX = tooltipWidth + (offset.x + anchor.x) * 2;
10444 subY = tooltipHeight / 2;
10445 }
10446
10447 pos = pos.subtract(toPoint(subX, subY, true)).add(offset).add(anchor);
10448
10449 removeClass(container, 'leaflet-tooltip-right');
10450 removeClass(container, 'leaflet-tooltip-left');
10451 removeClass(container, 'leaflet-tooltip-top');
10452 removeClass(container, 'leaflet-tooltip-bottom');
10453 addClass(container, 'leaflet-tooltip-' + direction);
10454 setPosition(container, pos);
10455 },
10456
10457 _updatePosition: function () {
10458 var pos = this._map.latLngToLayerPoint(this._latlng);
10459 this._setPosition(pos);
10460 },
10461
10462 setOpacity: function (opacity) {
10463 this.options.opacity = opacity;
10464
10465 if (this._container) {
10466 setOpacity(this._container, opacity);
10467 }
10468 },
10469
10470 _animateZoom: function (e) {
10471 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
10472 this._setPosition(pos);
10473 },
10474
10475 _getAnchor: function () {
10476 // Where should we anchor the tooltip on the source layer?
10477 return toPoint(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
10478 }
10479
10480});
10481
10482// @namespace Tooltip
10483// @factory L.tooltip(options?: Tooltip options, source?: Layer)
10484// 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.
10485var tooltip = function (options, source) {
10486 return new Tooltip(options, source);
10487};
10488
10489// @namespace Map
10490// @section Methods for Layers and Controls
10491Map.include({
10492
10493 // @method openTooltip(tooltip: Tooltip): this
10494 // Opens the specified tooltip.
10495 // @alternative
10496 // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
10497 // Creates a tooltip with the specified content and options and open it.
10498 openTooltip: function (tooltip, latlng, options) {
10499 this._initOverlay(Tooltip, tooltip, latlng, options)
10500 .openOn(this);
10501
10502 return this;
10503 },
10504
10505 // @method closeTooltip(tooltip: Tooltip): this
10506 // Closes the tooltip given as parameter.
10507 closeTooltip: function (tooltip) {
10508 tooltip.close();
10509 return this;
10510 }
10511
10512});
10513
10514/*
10515 * @namespace Layer
10516 * @section Tooltip methods example
10517 *
10518 * All layers share a set of methods convenient for binding tooltips to it.
10519 *
10520 * ```js
10521 * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
10522 * layer.openTooltip();
10523 * layer.closeTooltip();
10524 * ```
10525 */
10526
10527// @section Tooltip methods
10528Layer.include({
10529
10530 // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
10531 // Binds a tooltip to the layer with the passed `content` and sets up the
10532 // necessary event listeners. If a `Function` is passed it will receive
10533 // the layer as the first argument and should return a `String` or `HTMLElement`.
10534 bindTooltip: function (content, options) {
10535
10536 if (this._tooltip && this.isTooltipOpen()) {
10537 this.unbindTooltip();
10538 }
10539
10540 this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options);
10541 this._initTooltipInteractions();
10542
10543 if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
10544 this.openTooltip();
10545 }
10546
10547 return this;
10548 },
10549
10550 // @method unbindTooltip(): this
10551 // Removes the tooltip previously bound with `bindTooltip`.
10552 unbindTooltip: function () {
10553 if (this._tooltip) {
10554 this._initTooltipInteractions(true);
10555 this.closeTooltip();
10556 this._tooltip = null;
10557 }
10558 return this;
10559 },
10560
10561 _initTooltipInteractions: function (remove) {
10562 if (!remove && this._tooltipHandlersAdded) { return; }
10563 var onOff = remove ? 'off' : 'on',
10564 events = {
10565 remove: this.closeTooltip,
10566 move: this._moveTooltip
10567 };
10568 if (!this._tooltip.options.permanent) {
10569 events.mouseover = this._openTooltip;
10570 events.mouseout = this.closeTooltip;
10571 events.click = this._openTooltip;
10572 } else {
10573 events.add = this._openTooltip;
10574 }
10575 if (this._tooltip.options.sticky) {
10576 events.mousemove = this._moveTooltip;
10577 }
10578 this[onOff](events);
10579 this._tooltipHandlersAdded = !remove;
10580 },
10581
10582 // @method openTooltip(latlng?: LatLng): this
10583 // Opens the bound tooltip at the specified `latlng` or at the default tooltip anchor if no `latlng` is passed.
10584 openTooltip: function (latlng) {
10585 if (this._tooltip && this._tooltip._prepareOpen(latlng)) {
10586 // open the tooltip on the map
10587 this._tooltip.openOn(this._map);
10588 }
10589 return this;
10590 },
10591
10592 // @method closeTooltip(): this
10593 // Closes the tooltip bound to this layer if it is open.
10594 closeTooltip: function () {
10595 if (this._tooltip) {
10596 return this._tooltip.close();
10597 }
10598 },
10599
10600 // @method toggleTooltip(): this
10601 // Opens or closes the tooltip bound to this layer depending on its current state.
10602 toggleTooltip: function () {
10603 if (this._tooltip) {
10604 this._tooltip.toggle(this);
10605 }
10606 return this;
10607 },
10608
10609 // @method isTooltipOpen(): boolean
10610 // Returns `true` if the tooltip bound to this layer is currently open.
10611 isTooltipOpen: function () {
10612 return this._tooltip.isOpen();
10613 },
10614
10615 // @method setTooltipContent(content: String|HTMLElement|Tooltip): this
10616 // Sets the content of the tooltip bound to this layer.
10617 setTooltipContent: function (content) {
10618 if (this._tooltip) {
10619 this._tooltip.setContent(content);
10620 }
10621 return this;
10622 },
10623
10624 // @method getTooltip(): Tooltip
10625 // Returns the tooltip bound to this layer.
10626 getTooltip: function () {
10627 return this._tooltip;
10628 },
10629
10630 _openTooltip: function (e) {
10631 if (!this._tooltip || !this._map || (this._map.dragging && this._map.dragging.moving())) {
10632 return;
10633 }
10634 this._tooltip._source = e.layer || e.target;
10635
10636 this.openTooltip(this._tooltip.options.sticky ? e.latlng : undefined);
10637 },
10638
10639 _moveTooltip: function (e) {
10640 var latlng = e.latlng, containerPoint, layerPoint;
10641 if (this._tooltip.options.sticky && e.originalEvent) {
10642 containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
10643 layerPoint = this._map.containerPointToLayerPoint(containerPoint);
10644 latlng = this._map.layerPointToLatLng(layerPoint);
10645 }
10646 this._tooltip.setLatLng(latlng);
10647 }
10648});
10649
10650/*
10651 * @class DivIcon
10652 * @aka L.DivIcon
10653 * @inherits Icon
10654 *
10655 * Represents a lightweight icon for markers that uses a simple `<div>`
10656 * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
10657 *
10658 * @example
10659 * ```js
10660 * var myIcon = L.divIcon({className: 'my-div-icon'});
10661 * // you can set .my-div-icon styles in CSS
10662 *
10663 * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
10664 * ```
10665 *
10666 * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
10667 */
10668
10669var DivIcon = Icon.extend({
10670 options: {
10671 // @section
10672 // @aka DivIcon options
10673 iconSize: [12, 12], // also can be set through CSS
10674
10675 // iconAnchor: (Point),
10676 // popupAnchor: (Point),
10677
10678 // @option html: String|HTMLElement = ''
10679 // Custom HTML code to put inside the div element, empty by default. Alternatively,
10680 // an instance of `HTMLElement`.
10681 html: false,
10682
10683 // @option bgPos: Point = [0, 0]
10684 // Optional relative position of the background, in pixels
10685 bgPos: null,
10686
10687 className: 'leaflet-div-icon'
10688 },
10689
10690 createIcon: function (oldIcon) {
10691 var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
10692 options = this.options;
10693
10694 if (options.html instanceof Element) {
10695 empty(div);
10696 div.appendChild(options.html);
10697 } else {
10698 div.innerHTML = options.html !== false ? options.html : '';
10699 }
10700
10701 if (options.bgPos) {
10702 var bgPos = toPoint(options.bgPos);
10703 div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
10704 }
10705 this._setIconStyles(div, 'icon');
10706
10707 return div;
10708 },
10709
10710 createShadow: function () {
10711 return null;
10712 }
10713});
10714
10715// @factory L.divIcon(options: DivIcon options)
10716// Creates a `DivIcon` instance with the given options.
10717function divIcon(options) {
10718 return new DivIcon(options);
10719}
10720
10721Icon.Default = IconDefault;
10722
10723/*
10724 * @class GridLayer
10725 * @inherits Layer
10726 * @aka L.GridLayer
10727 *
10728 * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
10729 * 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.
10730 *
10731 *
10732 * @section Synchronous usage
10733 * @example
10734 *
10735 * 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.
10736 *
10737 * ```js
10738 * var CanvasLayer = L.GridLayer.extend({
10739 * createTile: function(coords){
10740 * // create a <canvas> element for drawing
10741 * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
10742 *
10743 * // setup tile width and height according to the options
10744 * var size = this.getTileSize();
10745 * tile.width = size.x;
10746 * tile.height = size.y;
10747 *
10748 * // get a canvas context and draw something on it using coords.x, coords.y and coords.z
10749 * var ctx = tile.getContext('2d');
10750 *
10751 * // return the tile so it can be rendered on screen
10752 * return tile;
10753 * }
10754 * });
10755 * ```
10756 *
10757 * @section Asynchronous usage
10758 * @example
10759 *
10760 * 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.
10761 *
10762 * ```js
10763 * var CanvasLayer = L.GridLayer.extend({
10764 * createTile: function(coords, done){
10765 * var error;
10766 *
10767 * // create a <canvas> element for drawing
10768 * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
10769 *
10770 * // setup tile width and height according to the options
10771 * var size = this.getTileSize();
10772 * tile.width = size.x;
10773 * tile.height = size.y;
10774 *
10775 * // draw something asynchronously and pass the tile to the done() callback
10776 * setTimeout(function() {
10777 * done(error, tile);
10778 * }, 1000);
10779 *
10780 * return tile;
10781 * }
10782 * });
10783 * ```
10784 *
10785 * @section
10786 */
10787
10788
10789var GridLayer = Layer.extend({
10790
10791 // @section
10792 // @aka GridLayer options
10793 options: {
10794 // @option tileSize: Number|Point = 256
10795 // Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
10796 tileSize: 256,
10797
10798 // @option opacity: Number = 1.0
10799 // Opacity of the tiles. Can be used in the `createTile()` function.
10800 opacity: 1,
10801
10802 // @option updateWhenIdle: Boolean = (depends)
10803 // Load new tiles only when panning ends.
10804 // `true` by default on mobile browsers, in order to avoid too many requests and keep smooth navigation.
10805 // `false` otherwise in order to display new tiles _during_ panning, since it is easy to pan outside the
10806 // [`keepBuffer`](#gridlayer-keepbuffer) option in desktop browsers.
10807 updateWhenIdle: Browser.mobile,
10808
10809 // @option updateWhenZooming: Boolean = true
10810 // 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.
10811 updateWhenZooming: true,
10812
10813 // @option updateInterval: Number = 200
10814 // Tiles will not update more than once every `updateInterval` milliseconds when panning.
10815 updateInterval: 200,
10816
10817 // @option zIndex: Number = 1
10818 // The explicit zIndex of the tile layer.
10819 zIndex: 1,
10820
10821 // @option bounds: LatLngBounds = undefined
10822 // If set, tiles will only be loaded inside the set `LatLngBounds`.
10823 bounds: null,
10824
10825 // @option minZoom: Number = 0
10826 // The minimum zoom level down to which this layer will be displayed (inclusive).
10827 minZoom: 0,
10828
10829 // @option maxZoom: Number = undefined
10830 // The maximum zoom level up to which this layer will be displayed (inclusive).
10831 maxZoom: undefined,
10832
10833 // @option maxNativeZoom: Number = undefined
10834 // Maximum zoom number the tile source has available. If it is specified,
10835 // the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
10836 // from `maxNativeZoom` level and auto-scaled.
10837 maxNativeZoom: undefined,
10838
10839 // @option minNativeZoom: Number = undefined
10840 // Minimum zoom number the tile source has available. If it is specified,
10841 // the tiles on all zoom levels lower than `minNativeZoom` will be loaded
10842 // from `minNativeZoom` level and auto-scaled.
10843 minNativeZoom: undefined,
10844
10845 // @option noWrap: Boolean = false
10846 // Whether the layer is wrapped around the antimeridian. If `true`, the
10847 // GridLayer will only be displayed once at low zoom levels. Has no
10848 // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
10849 // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
10850 // tiles outside the CRS limits.
10851 noWrap: false,
10852
10853 // @option pane: String = 'tilePane'
10854 // `Map pane` where the grid layer will be added.
10855 pane: 'tilePane',
10856
10857 // @option className: String = ''
10858 // A custom class name to assign to the tile layer. Empty by default.
10859 className: '',
10860
10861 // @option keepBuffer: Number = 2
10862 // When panning the map, keep this many rows and columns of tiles before unloading them.
10863 keepBuffer: 2
10864 },
10865
10866 initialize: function (options) {
10867 setOptions(this, options);
10868 },
10869
10870 onAdd: function () {
10871 this._initContainer();
10872
10873 this._levels = {};
10874 this._tiles = {};
10875
10876 this._resetView(); // implicit _update() call
10877 },
10878
10879 beforeAdd: function (map) {
10880 map._addZoomLimit(this);
10881 },
10882
10883 onRemove: function (map) {
10884 this._removeAllTiles();
10885 remove(this._container);
10886 map._removeZoomLimit(this);
10887 this._container = null;
10888 this._tileZoom = undefined;
10889 },
10890
10891 // @method bringToFront: this
10892 // Brings the tile layer to the top of all tile layers.
10893 bringToFront: function () {
10894 if (this._map) {
10895 toFront(this._container);
10896 this._setAutoZIndex(Math.max);
10897 }
10898 return this;
10899 },
10900
10901 // @method bringToBack: this
10902 // Brings the tile layer to the bottom of all tile layers.
10903 bringToBack: function () {
10904 if (this._map) {
10905 toBack(this._container);
10906 this._setAutoZIndex(Math.min);
10907 }
10908 return this;
10909 },
10910
10911 // @method getContainer: HTMLElement
10912 // Returns the HTML element that contains the tiles for this layer.
10913 getContainer: function () {
10914 return this._container;
10915 },
10916
10917 // @method setOpacity(opacity: Number): this
10918 // Changes the [opacity](#gridlayer-opacity) of the grid layer.
10919 setOpacity: function (opacity) {
10920 this.options.opacity = opacity;
10921 this._updateOpacity();
10922 return this;
10923 },
10924
10925 // @method setZIndex(zIndex: Number): this
10926 // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
10927 setZIndex: function (zIndex) {
10928 this.options.zIndex = zIndex;
10929 this._updateZIndex();
10930
10931 return this;
10932 },
10933
10934 // @method isLoading: Boolean
10935 // Returns `true` if any tile in the grid layer has not finished loading.
10936 isLoading: function () {
10937 return this._loading;
10938 },
10939
10940 // @method redraw: this
10941 // Causes the layer to clear all the tiles and request them again.
10942 redraw: function () {
10943 if (this._map) {
10944 this._removeAllTiles();
10945 var tileZoom = this._clampZoom(this._map.getZoom());
10946 if (tileZoom !== this._tileZoom) {
10947 this._tileZoom = tileZoom;
10948 this._updateLevels();
10949 }
10950 this._update();
10951 }
10952 return this;
10953 },
10954
10955 getEvents: function () {
10956 var events = {
10957 viewprereset: this._invalidateAll,
10958 viewreset: this._resetView,
10959 zoom: this._resetView,
10960 moveend: this._onMoveEnd
10961 };
10962
10963 if (!this.options.updateWhenIdle) {
10964 // update tiles on move, but not more often than once per given interval
10965 if (!this._onMove) {
10966 this._onMove = throttle(this._onMoveEnd, this.options.updateInterval, this);
10967 }
10968
10969 events.move = this._onMove;
10970 }
10971
10972 if (this._zoomAnimated) {
10973 events.zoomanim = this._animateZoom;
10974 }
10975
10976 return events;
10977 },
10978
10979 // @section Extension methods
10980 // Layers extending `GridLayer` shall reimplement the following method.
10981 // @method createTile(coords: Object, done?: Function): HTMLElement
10982 // Called only internally, must be overridden by classes extending `GridLayer`.
10983 // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
10984 // is specified, it must be called when the tile has finished loading and drawing.
10985 createTile: function () {
10986 return document.createElement('div');
10987 },
10988
10989 // @section
10990 // @method getTileSize: Point
10991 // Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
10992 getTileSize: function () {
10993 var s = this.options.tileSize;
10994 return s instanceof Point ? s : new Point(s, s);
10995 },
10996
10997 _updateZIndex: function () {
10998 if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
10999 this._container.style.zIndex = this.options.zIndex;
11000 }
11001 },
11002
11003 _setAutoZIndex: function (compare) {
11004 // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
11005
11006 var layers = this.getPane().children,
11007 edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
11008
11009 for (var i = 0, len = layers.length, zIndex; i < len; i++) {
11010
11011 zIndex = layers[i].style.zIndex;
11012
11013 if (layers[i] !== this._container && zIndex) {
11014 edgeZIndex = compare(edgeZIndex, +zIndex);
11015 }
11016 }
11017
11018 if (isFinite(edgeZIndex)) {
11019 this.options.zIndex = edgeZIndex + compare(-1, 1);
11020 this._updateZIndex();
11021 }
11022 },
11023
11024 _updateOpacity: function () {
11025 if (!this._map) { return; }
11026
11027 // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
11028 if (Browser.ielt9) { return; }
11029
11030 setOpacity(this._container, this.options.opacity);
11031
11032 var now = +new Date(),
11033 nextFrame = false,
11034 willPrune = false;
11035
11036 for (var key in this._tiles) {
11037 var tile = this._tiles[key];
11038 if (!tile.current || !tile.loaded) { continue; }
11039
11040 var fade = Math.min(1, (now - tile.loaded) / 200);
11041
11042 setOpacity(tile.el, fade);
11043 if (fade < 1) {
11044 nextFrame = true;
11045 } else {
11046 if (tile.active) {
11047 willPrune = true;
11048 } else {
11049 this._onOpaqueTile(tile);
11050 }
11051 tile.active = true;
11052 }
11053 }
11054
11055 if (willPrune && !this._noPrune) { this._pruneTiles(); }
11056
11057 if (nextFrame) {
11058 cancelAnimFrame(this._fadeFrame);
11059 this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
11060 }
11061 },
11062
11063 _onOpaqueTile: falseFn,
11064
11065 _initContainer: function () {
11066 if (this._container) { return; }
11067
11068 this._container = create$1('div', 'leaflet-layer ' + (this.options.className || ''));
11069 this._updateZIndex();
11070
11071 if (this.options.opacity < 1) {
11072 this._updateOpacity();
11073 }
11074
11075 this.getPane().appendChild(this._container);
11076 },
11077
11078 _updateLevels: function () {
11079
11080 var zoom = this._tileZoom,
11081 maxZoom = this.options.maxZoom;
11082
11083 if (zoom === undefined) { return undefined; }
11084
11085 for (var z in this._levels) {
11086 z = Number(z);
11087 if (this._levels[z].el.children.length || z === zoom) {
11088 this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
11089 this._onUpdateLevel(z);
11090 } else {
11091 remove(this._levels[z].el);
11092 this._removeTilesAtZoom(z);
11093 this._onRemoveLevel(z);
11094 delete this._levels[z];
11095 }
11096 }
11097
11098 var level = this._levels[zoom],
11099 map = this._map;
11100
11101 if (!level) {
11102 level = this._levels[zoom] = {};
11103
11104 level.el = create$1('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
11105 level.el.style.zIndex = maxZoom;
11106
11107 level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
11108 level.zoom = zoom;
11109
11110 this._setZoomTransform(level, map.getCenter(), map.getZoom());
11111
11112 // force the browser to consider the newly added element for transition
11113 falseFn(level.el.offsetWidth);
11114
11115 this._onCreateLevel(level);
11116 }
11117
11118 this._level = level;
11119
11120 return level;
11121 },
11122
11123 _onUpdateLevel: falseFn,
11124
11125 _onRemoveLevel: falseFn,
11126
11127 _onCreateLevel: falseFn,
11128
11129 _pruneTiles: function () {
11130 if (!this._map) {
11131 return;
11132 }
11133
11134 var key, tile;
11135
11136 var zoom = this._map.getZoom();
11137 if (zoom > this.options.maxZoom ||
11138 zoom < this.options.minZoom) {
11139 this._removeAllTiles();
11140 return;
11141 }
11142
11143 for (key in this._tiles) {
11144 tile = this._tiles[key];
11145 tile.retain = tile.current;
11146 }
11147
11148 for (key in this._tiles) {
11149 tile = this._tiles[key];
11150 if (tile.current && !tile.active) {
11151 var coords = tile.coords;
11152 if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
11153 this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
11154 }
11155 }
11156 }
11157
11158 for (key in this._tiles) {
11159 if (!this._tiles[key].retain) {
11160 this._removeTile(key);
11161 }
11162 }
11163 },
11164
11165 _removeTilesAtZoom: function (zoom) {
11166 for (var key in this._tiles) {
11167 if (this._tiles[key].coords.z !== zoom) {
11168 continue;
11169 }
11170 this._removeTile(key);
11171 }
11172 },
11173
11174 _removeAllTiles: function () {
11175 for (var key in this._tiles) {
11176 this._removeTile(key);
11177 }
11178 },
11179
11180 _invalidateAll: function () {
11181 for (var z in this._levels) {
11182 remove(this._levels[z].el);
11183 this._onRemoveLevel(Number(z));
11184 delete this._levels[z];
11185 }
11186 this._removeAllTiles();
11187
11188 this._tileZoom = undefined;
11189 },
11190
11191 _retainParent: function (x, y, z, minZoom) {
11192 var x2 = Math.floor(x / 2),
11193 y2 = Math.floor(y / 2),
11194 z2 = z - 1,
11195 coords2 = new Point(+x2, +y2);
11196 coords2.z = +z2;
11197
11198 var key = this._tileCoordsToKey(coords2),
11199 tile = this._tiles[key];
11200
11201 if (tile && tile.active) {
11202 tile.retain = true;
11203 return true;
11204
11205 } else if (tile && tile.loaded) {
11206 tile.retain = true;
11207 }
11208
11209 if (z2 > minZoom) {
11210 return this._retainParent(x2, y2, z2, minZoom);
11211 }
11212
11213 return false;
11214 },
11215
11216 _retainChildren: function (x, y, z, maxZoom) {
11217
11218 for (var i = 2 * x; i < 2 * x + 2; i++) {
11219 for (var j = 2 * y; j < 2 * y + 2; j++) {
11220
11221 var coords = new Point(i, j);
11222 coords.z = z + 1;
11223
11224 var key = this._tileCoordsToKey(coords),
11225 tile = this._tiles[key];
11226
11227 if (tile && tile.active) {
11228 tile.retain = true;
11229 continue;
11230
11231 } else if (tile && tile.loaded) {
11232 tile.retain = true;
11233 }
11234
11235 if (z + 1 < maxZoom) {
11236 this._retainChildren(i, j, z + 1, maxZoom);
11237 }
11238 }
11239 }
11240 },
11241
11242 _resetView: function (e) {
11243 var animating = e && (e.pinch || e.flyTo);
11244 this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
11245 },
11246
11247 _animateZoom: function (e) {
11248 this._setView(e.center, e.zoom, true, e.noUpdate);
11249 },
11250
11251 _clampZoom: function (zoom) {
11252 var options = this.options;
11253
11254 if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) {
11255 return options.minNativeZoom;
11256 }
11257
11258 if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) {
11259 return options.maxNativeZoom;
11260 }
11261
11262 return zoom;
11263 },
11264
11265 _setView: function (center, zoom, noPrune, noUpdate) {
11266 var tileZoom = Math.round(zoom);
11267 if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
11268 (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
11269 tileZoom = undefined;
11270 } else {
11271 tileZoom = this._clampZoom(tileZoom);
11272 }
11273
11274 var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);
11275
11276 if (!noUpdate || tileZoomChanged) {
11277
11278 this._tileZoom = tileZoom;
11279
11280 if (this._abortLoading) {
11281 this._abortLoading();
11282 }
11283
11284 this._updateLevels();
11285 this._resetGrid();
11286
11287 if (tileZoom !== undefined) {
11288 this._update(center);
11289 }
11290
11291 if (!noPrune) {
11292 this._pruneTiles();
11293 }
11294
11295 // Flag to prevent _updateOpacity from pruning tiles during
11296 // a zoom anim or a pinch gesture
11297 this._noPrune = !!noPrune;
11298 }
11299
11300 this._setZoomTransforms(center, zoom);
11301 },
11302
11303 _setZoomTransforms: function (center, zoom) {
11304 for (var i in this._levels) {
11305 this._setZoomTransform(this._levels[i], center, zoom);
11306 }
11307 },
11308
11309 _setZoomTransform: function (level, center, zoom) {
11310 var scale = this._map.getZoomScale(zoom, level.zoom),
11311 translate = level.origin.multiplyBy(scale)
11312 .subtract(this._map._getNewPixelOrigin(center, zoom)).round();
11313
11314 if (Browser.any3d) {
11315 setTransform(level.el, translate, scale);
11316 } else {
11317 setPosition(level.el, translate);
11318 }
11319 },
11320
11321 _resetGrid: function () {
11322 var map = this._map,
11323 crs = map.options.crs,
11324 tileSize = this._tileSize = this.getTileSize(),
11325 tileZoom = this._tileZoom;
11326
11327 var bounds = this._map.getPixelWorldBounds(this._tileZoom);
11328 if (bounds) {
11329 this._globalTileRange = this._pxBoundsToTileRange(bounds);
11330 }
11331
11332 this._wrapX = crs.wrapLng && !this.options.noWrap && [
11333 Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
11334 Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
11335 ];
11336 this._wrapY = crs.wrapLat && !this.options.noWrap && [
11337 Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
11338 Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
11339 ];
11340 },
11341
11342 _onMoveEnd: function () {
11343 if (!this._map || this._map._animatingZoom) { return; }
11344
11345 this._update();
11346 },
11347
11348 _getTiledPixelBounds: function (center) {
11349 var map = this._map,
11350 mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
11351 scale = map.getZoomScale(mapZoom, this._tileZoom),
11352 pixelCenter = map.project(center, this._tileZoom).floor(),
11353 halfSize = map.getSize().divideBy(scale * 2);
11354
11355 return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
11356 },
11357
11358 // Private method to load tiles in the grid's active zoom level according to map bounds
11359 _update: function (center) {
11360 var map = this._map;
11361 if (!map) { return; }
11362 var zoom = this._clampZoom(map.getZoom());
11363
11364 if (center === undefined) { center = map.getCenter(); }
11365 if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom
11366
11367 var pixelBounds = this._getTiledPixelBounds(center),
11368 tileRange = this._pxBoundsToTileRange(pixelBounds),
11369 tileCenter = tileRange.getCenter(),
11370 queue = [],
11371 margin = this.options.keepBuffer,
11372 noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
11373 tileRange.getTopRight().add([margin, -margin]));
11374
11375 // Sanity check: panic if the tile range contains Infinity somewhere.
11376 if (!(isFinite(tileRange.min.x) &&
11377 isFinite(tileRange.min.y) &&
11378 isFinite(tileRange.max.x) &&
11379 isFinite(tileRange.max.y))) { throw new Error('Attempted to load an infinite number of tiles'); }
11380
11381 for (var key in this._tiles) {
11382 var c = this._tiles[key].coords;
11383 if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) {
11384 this._tiles[key].current = false;
11385 }
11386 }
11387
11388 // _update just loads more tiles. If the tile zoom level differs too much
11389 // from the map's, let _setView reset levels and prune old tiles.
11390 if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }
11391
11392 // create a queue of coordinates to load tiles from
11393 for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
11394 for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
11395 var coords = new Point(i, j);
11396 coords.z = this._tileZoom;
11397
11398 if (!this._isValidTile(coords)) { continue; }
11399
11400 var tile = this._tiles[this._tileCoordsToKey(coords)];
11401 if (tile) {
11402 tile.current = true;
11403 } else {
11404 queue.push(coords);
11405 }
11406 }
11407 }
11408
11409 // sort tile queue to load tiles in order of their distance to center
11410 queue.sort(function (a, b) {
11411 return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
11412 });
11413
11414 if (queue.length !== 0) {
11415 // if it's the first batch of tiles to load
11416 if (!this._loading) {
11417 this._loading = true;
11418 // @event loading: Event
11419 // Fired when the grid layer starts loading tiles.
11420 this.fire('loading');
11421 }
11422
11423 // create DOM fragment to append tiles in one batch
11424 var fragment = document.createDocumentFragment();
11425
11426 for (i = 0; i < queue.length; i++) {
11427 this._addTile(queue[i], fragment);
11428 }
11429
11430 this._level.el.appendChild(fragment);
11431 }
11432 },
11433
11434 _isValidTile: function (coords) {
11435 var crs = this._map.options.crs;
11436
11437 if (!crs.infinite) {
11438 // don't load tile if it's out of bounds and not wrapped
11439 var bounds = this._globalTileRange;
11440 if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
11441 (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
11442 }
11443
11444 if (!this.options.bounds) { return true; }
11445
11446 // don't load tile if it doesn't intersect the bounds in options
11447 var tileBounds = this._tileCoordsToBounds(coords);
11448 return toLatLngBounds(this.options.bounds).overlaps(tileBounds);
11449 },
11450
11451 _keyToBounds: function (key) {
11452 return this._tileCoordsToBounds(this._keyToTileCoords(key));
11453 },
11454
11455 _tileCoordsToNwSe: function (coords) {
11456 var map = this._map,
11457 tileSize = this.getTileSize(),
11458 nwPoint = coords.scaleBy(tileSize),
11459 sePoint = nwPoint.add(tileSize),
11460 nw = map.unproject(nwPoint, coords.z),
11461 se = map.unproject(sePoint, coords.z);
11462 return [nw, se];
11463 },
11464
11465 // converts tile coordinates to its geographical bounds
11466 _tileCoordsToBounds: function (coords) {
11467 var bp = this._tileCoordsToNwSe(coords),
11468 bounds = new LatLngBounds(bp[0], bp[1]);
11469
11470 if (!this.options.noWrap) {
11471 bounds = this._map.wrapLatLngBounds(bounds);
11472 }
11473 return bounds;
11474 },
11475 // converts tile coordinates to key for the tile cache
11476 _tileCoordsToKey: function (coords) {
11477 return coords.x + ':' + coords.y + ':' + coords.z;
11478 },
11479
11480 // converts tile cache key to coordinates
11481 _keyToTileCoords: function (key) {
11482 var k = key.split(':'),
11483 coords = new Point(+k[0], +k[1]);
11484 coords.z = +k[2];
11485 return coords;
11486 },
11487
11488 _removeTile: function (key) {
11489 var tile = this._tiles[key];
11490 if (!tile) { return; }
11491
11492 remove(tile.el);
11493
11494 delete this._tiles[key];
11495
11496 // @event tileunload: TileEvent
11497 // Fired when a tile is removed (e.g. when a tile goes off the screen).
11498 this.fire('tileunload', {
11499 tile: tile.el,
11500 coords: this._keyToTileCoords(key)
11501 });
11502 },
11503
11504 _initTile: function (tile) {
11505 addClass(tile, 'leaflet-tile');
11506
11507 var tileSize = this.getTileSize();
11508 tile.style.width = tileSize.x + 'px';
11509 tile.style.height = tileSize.y + 'px';
11510
11511 tile.onselectstart = falseFn;
11512 tile.onmousemove = falseFn;
11513
11514 // update opacity on tiles in IE7-8 because of filter inheritance problems
11515 if (Browser.ielt9 && this.options.opacity < 1) {
11516 setOpacity(tile, this.options.opacity);
11517 }
11518 },
11519
11520 _addTile: function (coords, container) {
11521 var tilePos = this._getTilePos(coords),
11522 key = this._tileCoordsToKey(coords);
11523
11524 var tile = this.createTile(this._wrapCoords(coords), bind(this._tileReady, this, coords));
11525
11526 this._initTile(tile);
11527
11528 // if createTile is defined with a second argument ("done" callback),
11529 // we know that tile is async and will be ready later; otherwise
11530 if (this.createTile.length < 2) {
11531 // mark tile as ready, but delay one frame for opacity animation to happen
11532 requestAnimFrame(bind(this._tileReady, this, coords, null, tile));
11533 }
11534
11535 setPosition(tile, tilePos);
11536
11537 // save tile in cache
11538 this._tiles[key] = {
11539 el: tile,
11540 coords: coords,
11541 current: true
11542 };
11543
11544 container.appendChild(tile);
11545 // @event tileloadstart: TileEvent
11546 // Fired when a tile is requested and starts loading.
11547 this.fire('tileloadstart', {
11548 tile: tile,
11549 coords: coords
11550 });
11551 },
11552
11553 _tileReady: function (coords, err, tile) {
11554 if (err) {
11555 // @event tileerror: TileErrorEvent
11556 // Fired when there is an error loading a tile.
11557 this.fire('tileerror', {
11558 error: err,
11559 tile: tile,
11560 coords: coords
11561 });
11562 }
11563
11564 var key = this._tileCoordsToKey(coords);
11565
11566 tile = this._tiles[key];
11567 if (!tile) { return; }
11568
11569 tile.loaded = +new Date();
11570 if (this._map._fadeAnimated) {
11571 setOpacity(tile.el, 0);
11572 cancelAnimFrame(this._fadeFrame);
11573 this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
11574 } else {
11575 tile.active = true;
11576 this._pruneTiles();
11577 }
11578
11579 if (!err) {
11580 addClass(tile.el, 'leaflet-tile-loaded');
11581
11582 // @event tileload: TileEvent
11583 // Fired when a tile loads.
11584 this.fire('tileload', {
11585 tile: tile.el,
11586 coords: coords
11587 });
11588 }
11589
11590 if (this._noTilesToLoad()) {
11591 this._loading = false;
11592 // @event load: Event
11593 // Fired when the grid layer loaded all visible tiles.
11594 this.fire('load');
11595
11596 if (Browser.ielt9 || !this._map._fadeAnimated) {
11597 requestAnimFrame(this._pruneTiles, this);
11598 } else {
11599 // Wait a bit more than 0.2 secs (the duration of the tile fade-in)
11600 // to trigger a pruning.
11601 setTimeout(bind(this._pruneTiles, this), 250);
11602 }
11603 }
11604 },
11605
11606 _getTilePos: function (coords) {
11607 return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
11608 },
11609
11610 _wrapCoords: function (coords) {
11611 var newCoords = new Point(
11612 this._wrapX ? wrapNum(coords.x, this._wrapX) : coords.x,
11613 this._wrapY ? wrapNum(coords.y, this._wrapY) : coords.y);
11614 newCoords.z = coords.z;
11615 return newCoords;
11616 },
11617
11618 _pxBoundsToTileRange: function (bounds) {
11619 var tileSize = this.getTileSize();
11620 return new Bounds(
11621 bounds.min.unscaleBy(tileSize).floor(),
11622 bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
11623 },
11624
11625 _noTilesToLoad: function () {
11626 for (var key in this._tiles) {
11627 if (!this._tiles[key].loaded) { return false; }
11628 }
11629 return true;
11630 }
11631});
11632
11633// @factory L.gridLayer(options?: GridLayer options)
11634// Creates a new instance of GridLayer with the supplied options.
11635function gridLayer(options) {
11636 return new GridLayer(options);
11637}
11638
11639/*
11640 * @class TileLayer
11641 * @inherits GridLayer
11642 * @aka L.TileLayer
11643 * 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`.
11644 *
11645 * @example
11646 *
11647 * ```js
11648 * L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);
11649 * ```
11650 *
11651 * @section URL template
11652 * @example
11653 *
11654 * A string of the following form:
11655 *
11656 * ```
11657 * 'https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
11658 * ```
11659 *
11660 * `{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.
11661 *
11662 * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
11663 *
11664 * ```
11665 * L.tileLayer('https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
11666 * ```
11667 */
11668
11669
11670var TileLayer = GridLayer.extend({
11671
11672 // @section
11673 // @aka TileLayer options
11674 options: {
11675 // @option minZoom: Number = 0
11676 // The minimum zoom level down to which this layer will be displayed (inclusive).
11677 minZoom: 0,
11678
11679 // @option maxZoom: Number = 18
11680 // The maximum zoom level up to which this layer will be displayed (inclusive).
11681 maxZoom: 18,
11682
11683 // @option subdomains: String|String[] = 'abc'
11684 // 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.
11685 subdomains: 'abc',
11686
11687 // @option errorTileUrl: String = ''
11688 // URL to the tile image to show in place of the tile that failed to load.
11689 errorTileUrl: '',
11690
11691 // @option zoomOffset: Number = 0
11692 // The zoom number used in tile URLs will be offset with this value.
11693 zoomOffset: 0,
11694
11695 // @option tms: Boolean = false
11696 // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
11697 tms: false,
11698
11699 // @option zoomReverse: Boolean = false
11700 // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
11701 zoomReverse: false,
11702
11703 // @option detectRetina: Boolean = false
11704 // 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.
11705 detectRetina: false,
11706
11707 // @option crossOrigin: Boolean|String = false
11708 // Whether the crossOrigin attribute will be added to the tiles.
11709 // 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.
11710 // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
11711 crossOrigin: false,
11712
11713 // @option referrerPolicy: Boolean|String = false
11714 // Whether the referrerPolicy attribute will be added to the tiles.
11715 // If a String is provided, all tiles will have their referrerPolicy attribute set to the String provided.
11716 // This may be needed if your map's rendering context has a strict default but your tile provider expects a valid referrer
11717 // (e.g. to validate an API token).
11718 // Refer to [HTMLImageElement.referrerPolicy](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/referrerPolicy) for valid String values.
11719 referrerPolicy: false
11720 },
11721
11722 initialize: function (url, options) {
11723
11724 this._url = url;
11725
11726 options = setOptions(this, options);
11727
11728 // detecting retina displays, adjusting tileSize and zoom levels
11729 if (options.detectRetina && Browser.retina && options.maxZoom > 0) {
11730
11731 options.tileSize = Math.floor(options.tileSize / 2);
11732
11733 if (!options.zoomReverse) {
11734 options.zoomOffset++;
11735 options.maxZoom--;
11736 } else {
11737 options.zoomOffset--;
11738 options.minZoom++;
11739 }
11740
11741 options.minZoom = Math.max(0, options.minZoom);
11742 }
11743
11744 if (typeof options.subdomains === 'string') {
11745 options.subdomains = options.subdomains.split('');
11746 }
11747
11748 this.on('tileunload', this._onTileRemove);
11749 },
11750
11751 // @method setUrl(url: String, noRedraw?: Boolean): this
11752 // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
11753 // If the URL does not change, the layer will not be redrawn unless
11754 // the noRedraw parameter is set to false.
11755 setUrl: function (url, noRedraw) {
11756 if (this._url === url && noRedraw === undefined) {
11757 noRedraw = true;
11758 }
11759
11760 this._url = url;
11761
11762 if (!noRedraw) {
11763 this.redraw();
11764 }
11765 return this;
11766 },
11767
11768 // @method createTile(coords: Object, done?: Function): HTMLElement
11769 // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
11770 // to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done`
11771 // callback is called when the tile has been loaded.
11772 createTile: function (coords, done) {
11773 var tile = document.createElement('img');
11774
11775 on(tile, 'load', bind(this._tileOnLoad, this, done, tile));
11776 on(tile, 'error', bind(this._tileOnError, this, done, tile));
11777
11778 if (this.options.crossOrigin || this.options.crossOrigin === '') {
11779 tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
11780 }
11781
11782 // for this new option we follow the documented behavior
11783 // more closely by only setting the property when string
11784 if (typeof this.options.referrerPolicy === 'string') {
11785 tile.referrerPolicy = this.options.referrerPolicy;
11786 }
11787
11788 /*
11789 Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons
11790 https://www.w3.org/TR/WCAG20-TECHS/H67
11791 */
11792 tile.alt = '';
11793
11794 /*
11795 Set role="presentation" to force screen readers to ignore this
11796 https://www.w3.org/TR/wai-aria/roles#textalternativecomputation
11797 */
11798 tile.setAttribute('role', 'presentation');
11799
11800 tile.src = this.getTileUrl(coords);
11801
11802 return tile;
11803 },
11804
11805 // @section Extension methods
11806 // @uninheritable
11807 // Layers extending `TileLayer` might reimplement the following method.
11808 // @method getTileUrl(coords: Object): String
11809 // Called only internally, returns the URL for a tile given its coordinates.
11810 // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
11811 getTileUrl: function (coords) {
11812 var data = {
11813 r: Browser.retina ? '@2x' : '',
11814 s: this._getSubdomain(coords),
11815 x: coords.x,
11816 y: coords.y,
11817 z: this._getZoomForUrl()
11818 };
11819 if (this._map && !this._map.options.crs.infinite) {
11820 var invertedY = this._globalTileRange.max.y - coords.y;
11821 if (this.options.tms) {
11822 data['y'] = invertedY;
11823 }
11824 data['-y'] = invertedY;
11825 }
11826
11827 return template(this._url, extend(data, this.options));
11828 },
11829
11830 _tileOnLoad: function (done, tile) {
11831 // For https://github.com/Leaflet/Leaflet/issues/3332
11832 if (Browser.ielt9) {
11833 setTimeout(bind(done, this, null, tile), 0);
11834 } else {
11835 done(null, tile);
11836 }
11837 },
11838
11839 _tileOnError: function (done, tile, e) {
11840 var errorUrl = this.options.errorTileUrl;
11841 if (errorUrl && tile.getAttribute('src') !== errorUrl) {
11842 tile.src = errorUrl;
11843 }
11844 done(e, tile);
11845 },
11846
11847 _onTileRemove: function (e) {
11848 e.tile.onload = null;
11849 },
11850
11851 _getZoomForUrl: function () {
11852 var zoom = this._tileZoom,
11853 maxZoom = this.options.maxZoom,
11854 zoomReverse = this.options.zoomReverse,
11855 zoomOffset = this.options.zoomOffset;
11856
11857 if (zoomReverse) {
11858 zoom = maxZoom - zoom;
11859 }
11860
11861 return zoom + zoomOffset;
11862 },
11863
11864 _getSubdomain: function (tilePoint) {
11865 var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
11866 return this.options.subdomains[index];
11867 },
11868
11869 // stops loading all tiles in the background layer
11870 _abortLoading: function () {
11871 var i, tile;
11872 for (i in this._tiles) {
11873 if (this._tiles[i].coords.z !== this._tileZoom) {
11874 tile = this._tiles[i].el;
11875
11876 tile.onload = falseFn;
11877 tile.onerror = falseFn;
11878
11879 if (!tile.complete) {
11880 tile.src = emptyImageUrl;
11881 var coords = this._tiles[i].coords;
11882 remove(tile);
11883 delete this._tiles[i];
11884 // @event tileabort: TileEvent
11885 // Fired when a tile was loading but is now not wanted.
11886 this.fire('tileabort', {
11887 tile: tile,
11888 coords: coords
11889 });
11890 }
11891 }
11892 }
11893 },
11894
11895 _removeTile: function (key) {
11896 var tile = this._tiles[key];
11897 if (!tile) { return; }
11898
11899 // Cancels any pending http requests associated with the tile
11900 tile.el.setAttribute('src', emptyImageUrl);
11901
11902 return GridLayer.prototype._removeTile.call(this, key);
11903 },
11904
11905 _tileReady: function (coords, err, tile) {
11906 if (!this._map || (tile && tile.getAttribute('src') === emptyImageUrl)) {
11907 return;
11908 }
11909
11910 return GridLayer.prototype._tileReady.call(this, coords, err, tile);
11911 }
11912});
11913
11914
11915// @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
11916// Instantiates a tile layer object given a `URL template` and optionally an options object.
11917
11918function tileLayer(url, options) {
11919 return new TileLayer(url, options);
11920}
11921
11922/*
11923 * @class TileLayer.WMS
11924 * @inherits TileLayer
11925 * @aka L.TileLayer.WMS
11926 * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
11927 *
11928 * @example
11929 *
11930 * ```js
11931 * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
11932 * layers: 'nexrad-n0r-900913',
11933 * format: 'image/png',
11934 * transparent: true,
11935 * attribution: "Weather data © 2012 IEM Nexrad"
11936 * });
11937 * ```
11938 */
11939
11940var TileLayerWMS = TileLayer.extend({
11941
11942 // @section
11943 // @aka TileLayer.WMS options
11944 // If any custom options not documented here are used, they will be sent to the
11945 // WMS server as extra parameters in each request URL. This can be useful for
11946 // [non-standard vendor WMS parameters](https://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
11947 defaultWmsParams: {
11948 service: 'WMS',
11949 request: 'GetMap',
11950
11951 // @option layers: String = ''
11952 // **(required)** Comma-separated list of WMS layers to show.
11953 layers: '',
11954
11955 // @option styles: String = ''
11956 // Comma-separated list of WMS styles.
11957 styles: '',
11958
11959 // @option format: String = 'image/jpeg'
11960 // WMS image format (use `'image/png'` for layers with transparency).
11961 format: 'image/jpeg',
11962
11963 // @option transparent: Boolean = false
11964 // If `true`, the WMS service will return images with transparency.
11965 transparent: false,
11966
11967 // @option version: String = '1.1.1'
11968 // Version of the WMS service to use
11969 version: '1.1.1'
11970 },
11971
11972 options: {
11973 // @option crs: CRS = null
11974 // Coordinate Reference System to use for the WMS requests, defaults to
11975 // map CRS. Don't change this if you're not sure what it means.
11976 crs: null,
11977
11978 // @option uppercase: Boolean = false
11979 // If `true`, WMS request parameter keys will be uppercase.
11980 uppercase: false
11981 },
11982
11983 initialize: function (url, options) {
11984
11985 this._url = url;
11986
11987 var wmsParams = extend({}, this.defaultWmsParams);
11988
11989 // all keys that are not TileLayer options go to WMS params
11990 for (var i in options) {
11991 if (!(i in this.options)) {
11992 wmsParams[i] = options[i];
11993 }
11994 }
11995
11996 options = setOptions(this, options);
11997
11998 var realRetina = options.detectRetina && Browser.retina ? 2 : 1;
11999 var tileSize = this.getTileSize();
12000 wmsParams.width = tileSize.x * realRetina;
12001 wmsParams.height = tileSize.y * realRetina;
12002
12003 this.wmsParams = wmsParams;
12004 },
12005
12006 onAdd: function (map) {
12007
12008 this._crs = this.options.crs || map.options.crs;
12009 this._wmsVersion = parseFloat(this.wmsParams.version);
12010
12011 var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
12012 this.wmsParams[projectionKey] = this._crs.code;
12013
12014 TileLayer.prototype.onAdd.call(this, map);
12015 },
12016
12017 getTileUrl: function (coords) {
12018
12019 var tileBounds = this._tileCoordsToNwSe(coords),
12020 crs = this._crs,
12021 bounds = toBounds(crs.project(tileBounds[0]), crs.project(tileBounds[1])),
12022 min = bounds.min,
12023 max = bounds.max,
12024 bbox = (this._wmsVersion >= 1.3 && this._crs === EPSG4326 ?
12025 [min.y, min.x, max.y, max.x] :
12026 [min.x, min.y, max.x, max.y]).join(','),
12027 url = TileLayer.prototype.getTileUrl.call(this, coords);
12028 return url +
12029 getParamString(this.wmsParams, url, this.options.uppercase) +
12030 (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
12031 },
12032
12033 // @method setParams(params: Object, noRedraw?: Boolean): this
12034 // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
12035 setParams: function (params, noRedraw) {
12036
12037 extend(this.wmsParams, params);
12038
12039 if (!noRedraw) {
12040 this.redraw();
12041 }
12042
12043 return this;
12044 }
12045});
12046
12047
12048// @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
12049// Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
12050function tileLayerWMS(url, options) {
12051 return new TileLayerWMS(url, options);
12052}
12053
12054TileLayer.WMS = TileLayerWMS;
12055tileLayer.wms = tileLayerWMS;
12056
12057/*
12058 * @class Renderer
12059 * @inherits Layer
12060 * @aka L.Renderer
12061 *
12062 * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
12063 * DOM container of the renderer, its bounds, and its zoom animation.
12064 *
12065 * A `Renderer` works as an implicit layer group for all `Path`s - the renderer
12066 * itself can be added or removed to the map. All paths use a renderer, which can
12067 * be implicit (the map will decide the type of renderer and use it automatically)
12068 * or explicit (using the [`renderer`](#path-renderer) option of the path).
12069 *
12070 * Do not use this class directly, use `SVG` and `Canvas` instead.
12071 *
12072 * @event update: Event
12073 * Fired when the renderer updates its bounds, center and zoom, for example when
12074 * its map has moved
12075 */
12076
12077var Renderer = Layer.extend({
12078
12079 // @section
12080 // @aka Renderer options
12081 options: {
12082 // @option padding: Number = 0.1
12083 // How much to extend the clip area around the map view (relative to its size)
12084 // e.g. 0.1 would be 10% of map view in each direction
12085 padding: 0.1
12086 },
12087
12088 initialize: function (options) {
12089 setOptions(this, options);
12090 stamp(this);
12091 this._layers = this._layers || {};
12092 },
12093
12094 onAdd: function () {
12095 if (!this._container) {
12096 this._initContainer(); // defined by renderer implementations
12097
12098 if (this._zoomAnimated) {
12099 addClass(this._container, 'leaflet-zoom-animated');
12100 }
12101 }
12102
12103 this.getPane().appendChild(this._container);
12104 this._update();
12105 this.on('update', this._updatePaths, this);
12106 },
12107
12108 onRemove: function () {
12109 this.off('update', this._updatePaths, this);
12110 this._destroyContainer();
12111 },
12112
12113 getEvents: function () {
12114 var events = {
12115 viewreset: this._reset,
12116 zoom: this._onZoom,
12117 moveend: this._update,
12118 zoomend: this._onZoomEnd
12119 };
12120 if (this._zoomAnimated) {
12121 events.zoomanim = this._onAnimZoom;
12122 }
12123 return events;
12124 },
12125
12126 _onAnimZoom: function (ev) {
12127 this._updateTransform(ev.center, ev.zoom);
12128 },
12129
12130 _onZoom: function () {
12131 this._updateTransform(this._map.getCenter(), this._map.getZoom());
12132 },
12133
12134 _updateTransform: function (center, zoom) {
12135 var scale = this._map.getZoomScale(zoom, this._zoom),
12136 viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
12137 currentCenterPoint = this._map.project(this._center, zoom),
12138
12139 topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint)
12140 .subtract(this._map._getNewPixelOrigin(center, zoom));
12141
12142 if (Browser.any3d) {
12143 setTransform(this._container, topLeftOffset, scale);
12144 } else {
12145 setPosition(this._container, topLeftOffset);
12146 }
12147 },
12148
12149 _reset: function () {
12150 this._update();
12151 this._updateTransform(this._center, this._zoom);
12152
12153 for (var id in this._layers) {
12154 this._layers[id]._reset();
12155 }
12156 },
12157
12158 _onZoomEnd: function () {
12159 for (var id in this._layers) {
12160 this._layers[id]._project();
12161 }
12162 },
12163
12164 _updatePaths: function () {
12165 for (var id in this._layers) {
12166 this._layers[id]._update();
12167 }
12168 },
12169
12170 _update: function () {
12171 // Update pixel bounds of renderer container (for positioning/sizing/clipping later)
12172 // Subclasses are responsible of firing the 'update' event.
12173 var p = this.options.padding,
12174 size = this._map.getSize(),
12175 min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
12176
12177 this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
12178
12179 this._center = this._map.getCenter();
12180 this._zoom = this._map.getZoom();
12181 }
12182});
12183
12184/*
12185 * @class Canvas
12186 * @inherits Renderer
12187 * @aka L.Canvas
12188 *
12189 * Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
12190 * Inherits `Renderer`.
12191 *
12192 * Due to [technical limitations](https://caniuse.com/canvas), Canvas is not
12193 * available in all web browsers, notably IE8, and overlapping geometries might
12194 * not display properly in some edge cases.
12195 *
12196 * @example
12197 *
12198 * Use Canvas by default for all paths in the map:
12199 *
12200 * ```js
12201 * var map = L.map('map', {
12202 * renderer: L.canvas()
12203 * });
12204 * ```
12205 *
12206 * Use a Canvas renderer with extra padding for specific vector geometries:
12207 *
12208 * ```js
12209 * var map = L.map('map');
12210 * var myRenderer = L.canvas({ padding: 0.5 });
12211 * var line = L.polyline( coordinates, { renderer: myRenderer } );
12212 * var circle = L.circle( center, { renderer: myRenderer } );
12213 * ```
12214 */
12215
12216var Canvas = Renderer.extend({
12217
12218 // @section
12219 // @aka Canvas options
12220 options: {
12221 // @option tolerance: Number = 0
12222 // How much to extend the click tolerance around a path/object on the map.
12223 tolerance: 0
12224 },
12225
12226 getEvents: function () {
12227 var events = Renderer.prototype.getEvents.call(this);
12228 events.viewprereset = this._onViewPreReset;
12229 return events;
12230 },
12231
12232 _onViewPreReset: function () {
12233 // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once
12234 this._postponeUpdatePaths = true;
12235 },
12236
12237 onAdd: function () {
12238 Renderer.prototype.onAdd.call(this);
12239
12240 // Redraw vectors since canvas is cleared upon removal,
12241 // in case of removing the renderer itself from the map.
12242 this._draw();
12243 },
12244
12245 _initContainer: function () {
12246 var container = this._container = document.createElement('canvas');
12247
12248 on(container, 'mousemove', this._onMouseMove, this);
12249 on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this);
12250 on(container, 'mouseout', this._handleMouseOut, this);
12251 container['_leaflet_disable_events'] = true;
12252
12253 this._ctx = container.getContext('2d');
12254 },
12255
12256 _destroyContainer: function () {
12257 cancelAnimFrame(this._redrawRequest);
12258 delete this._ctx;
12259 remove(this._container);
12260 off(this._container);
12261 delete this._container;
12262 },
12263
12264 _updatePaths: function () {
12265 if (this._postponeUpdatePaths) { return; }
12266
12267 var layer;
12268 this._redrawBounds = null;
12269 for (var id in this._layers) {
12270 layer = this._layers[id];
12271 layer._update();
12272 }
12273 this._redraw();
12274 },
12275
12276 _update: function () {
12277 if (this._map._animatingZoom && this._bounds) { return; }
12278
12279 Renderer.prototype._update.call(this);
12280
12281 var b = this._bounds,
12282 container = this._container,
12283 size = b.getSize(),
12284 m = Browser.retina ? 2 : 1;
12285
12286 setPosition(container, b.min);
12287
12288 // set canvas size (also clearing it); use double size on retina
12289 container.width = m * size.x;
12290 container.height = m * size.y;
12291 container.style.width = size.x + 'px';
12292 container.style.height = size.y + 'px';
12293
12294 if (Browser.retina) {
12295 this._ctx.scale(2, 2);
12296 }
12297
12298 // translate so we use the same path coordinates after canvas element moves
12299 this._ctx.translate(-b.min.x, -b.min.y);
12300
12301 // Tell paths to redraw themselves
12302 this.fire('update');
12303 },
12304
12305 _reset: function () {
12306 Renderer.prototype._reset.call(this);
12307
12308 if (this._postponeUpdatePaths) {
12309 this._postponeUpdatePaths = false;
12310 this._updatePaths();
12311 }
12312 },
12313
12314 _initPath: function (layer) {
12315 this._updateDashArray(layer);
12316 this._layers[stamp(layer)] = layer;
12317
12318 var order = layer._order = {
12319 layer: layer,
12320 prev: this._drawLast,
12321 next: null
12322 };
12323 if (this._drawLast) { this._drawLast.next = order; }
12324 this._drawLast = order;
12325 this._drawFirst = this._drawFirst || this._drawLast;
12326 },
12327
12328 _addPath: function (layer) {
12329 this._requestRedraw(layer);
12330 },
12331
12332 _removePath: function (layer) {
12333 var order = layer._order;
12334 var next = order.next;
12335 var prev = order.prev;
12336
12337 if (next) {
12338 next.prev = prev;
12339 } else {
12340 this._drawLast = prev;
12341 }
12342 if (prev) {
12343 prev.next = next;
12344 } else {
12345 this._drawFirst = next;
12346 }
12347
12348 delete layer._order;
12349
12350 delete this._layers[stamp(layer)];
12351
12352 this._requestRedraw(layer);
12353 },
12354
12355 _updatePath: function (layer) {
12356 // Redraw the union of the layer's old pixel
12357 // bounds and the new pixel bounds.
12358 this._extendRedrawBounds(layer);
12359 layer._project();
12360 layer._update();
12361 // The redraw will extend the redraw bounds
12362 // with the new pixel bounds.
12363 this._requestRedraw(layer);
12364 },
12365
12366 _updateStyle: function (layer) {
12367 this._updateDashArray(layer);
12368 this._requestRedraw(layer);
12369 },
12370
12371 _updateDashArray: function (layer) {
12372 if (typeof layer.options.dashArray === 'string') {
12373 var parts = layer.options.dashArray.split(/[, ]+/),
12374 dashArray = [],
12375 dashValue,
12376 i;
12377 for (i = 0; i < parts.length; i++) {
12378 dashValue = Number(parts[i]);
12379 // Ignore dash array containing invalid lengths
12380 if (isNaN(dashValue)) { return; }
12381 dashArray.push(dashValue);
12382 }
12383 layer.options._dashArray = dashArray;
12384 } else {
12385 layer.options._dashArray = layer.options.dashArray;
12386 }
12387 },
12388
12389 _requestRedraw: function (layer) {
12390 if (!this._map) { return; }
12391
12392 this._extendRedrawBounds(layer);
12393 this._redrawRequest = this._redrawRequest || requestAnimFrame(this._redraw, this);
12394 },
12395
12396 _extendRedrawBounds: function (layer) {
12397 if (layer._pxBounds) {
12398 var padding = (layer.options.weight || 0) + 1;
12399 this._redrawBounds = this._redrawBounds || new Bounds();
12400 this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
12401 this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
12402 }
12403 },
12404
12405 _redraw: function () {
12406 this._redrawRequest = null;
12407
12408 if (this._redrawBounds) {
12409 this._redrawBounds.min._floor();
12410 this._redrawBounds.max._ceil();
12411 }
12412
12413 this._clear(); // clear layers in redraw bounds
12414 this._draw(); // draw layers
12415
12416 this._redrawBounds = null;
12417 },
12418
12419 _clear: function () {
12420 var bounds = this._redrawBounds;
12421 if (bounds) {
12422 var size = bounds.getSize();
12423 this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
12424 } else {
12425 this._ctx.save();
12426 this._ctx.setTransform(1, 0, 0, 1, 0, 0);
12427 this._ctx.clearRect(0, 0, this._container.width, this._container.height);
12428 this._ctx.restore();
12429 }
12430 },
12431
12432 _draw: function () {
12433 var layer, bounds = this._redrawBounds;
12434 this._ctx.save();
12435 if (bounds) {
12436 var size = bounds.getSize();
12437 this._ctx.beginPath();
12438 this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y);
12439 this._ctx.clip();
12440 }
12441
12442 this._drawing = true;
12443
12444 for (var order = this._drawFirst; order; order = order.next) {
12445 layer = order.layer;
12446 if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) {
12447 layer._updatePath();
12448 }
12449 }
12450
12451 this._drawing = false;
12452
12453 this._ctx.restore(); // Restore state before clipping.
12454 },
12455
12456 _updatePoly: function (layer, closed) {
12457 if (!this._drawing) { return; }
12458
12459 var i, j, len2, p,
12460 parts = layer._parts,
12461 len = parts.length,
12462 ctx = this._ctx;
12463
12464 if (!len) { return; }
12465
12466 ctx.beginPath();
12467
12468 for (i = 0; i < len; i++) {
12469 for (j = 0, len2 = parts[i].length; j < len2; j++) {
12470 p = parts[i][j];
12471 ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y);
12472 }
12473 if (closed) {
12474 ctx.closePath();
12475 }
12476 }
12477
12478 this._fillStroke(ctx, layer);
12479
12480 // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
12481 },
12482
12483 _updateCircle: function (layer) {
12484
12485 if (!this._drawing || layer._empty()) { return; }
12486
12487 var p = layer._point,
12488 ctx = this._ctx,
12489 r = Math.max(Math.round(layer._radius), 1),
12490 s = (Math.max(Math.round(layer._radiusY), 1) || r) / r;
12491
12492 if (s !== 1) {
12493 ctx.save();
12494 ctx.scale(1, s);
12495 }
12496
12497 ctx.beginPath();
12498 ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false);
12499
12500 if (s !== 1) {
12501 ctx.restore();
12502 }
12503
12504 this._fillStroke(ctx, layer);
12505 },
12506
12507 _fillStroke: function (ctx, layer) {
12508 var options = layer.options;
12509
12510 if (options.fill) {
12511 ctx.globalAlpha = options.fillOpacity;
12512 ctx.fillStyle = options.fillColor || options.color;
12513 ctx.fill(options.fillRule || 'evenodd');
12514 }
12515
12516 if (options.stroke && options.weight !== 0) {
12517 if (ctx.setLineDash) {
12518 ctx.setLineDash(layer.options && layer.options._dashArray || []);
12519 }
12520 ctx.globalAlpha = options.opacity;
12521 ctx.lineWidth = options.weight;
12522 ctx.strokeStyle = options.color;
12523 ctx.lineCap = options.lineCap;
12524 ctx.lineJoin = options.lineJoin;
12525 ctx.stroke();
12526 }
12527 },
12528
12529 // Canvas obviously doesn't have mouse events for individual drawn objects,
12530 // so we emulate that by calculating what's under the mouse on mousemove/click manually
12531
12532 _onClick: function (e) {
12533 var point = this._map.mouseEventToLayerPoint(e), layer, clickedLayer;
12534
12535 for (var order = this._drawFirst; order; order = order.next) {
12536 layer = order.layer;
12537 if (layer.options.interactive && layer._containsPoint(point)) {
12538 if (!(e.type === 'click' || e.type === 'preclick') || !this._map._draggableMoved(layer)) {
12539 clickedLayer = layer;
12540 }
12541 }
12542 }
12543 this._fireEvent(clickedLayer ? [clickedLayer] : false, e);
12544 },
12545
12546 _onMouseMove: function (e) {
12547 if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; }
12548
12549 var point = this._map.mouseEventToLayerPoint(e);
12550 this._handleMouseHover(e, point);
12551 },
12552
12553
12554 _handleMouseOut: function (e) {
12555 var layer = this._hoveredLayer;
12556 if (layer) {
12557 // if we're leaving the layer, fire mouseout
12558 removeClass(this._container, 'leaflet-interactive');
12559 this._fireEvent([layer], e, 'mouseout');
12560 this._hoveredLayer = null;
12561 this._mouseHoverThrottled = false;
12562 }
12563 },
12564
12565 _handleMouseHover: function (e, point) {
12566 if (this._mouseHoverThrottled) {
12567 return;
12568 }
12569
12570 var layer, candidateHoveredLayer;
12571
12572 for (var order = this._drawFirst; order; order = order.next) {
12573 layer = order.layer;
12574 if (layer.options.interactive && layer._containsPoint(point)) {
12575 candidateHoveredLayer = layer;
12576 }
12577 }
12578
12579 if (candidateHoveredLayer !== this._hoveredLayer) {
12580 this._handleMouseOut(e);
12581
12582 if (candidateHoveredLayer) {
12583 addClass(this._container, 'leaflet-interactive'); // change cursor
12584 this._fireEvent([candidateHoveredLayer], e, 'mouseover');
12585 this._hoveredLayer = candidateHoveredLayer;
12586 }
12587 }
12588
12589 this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e);
12590
12591 this._mouseHoverThrottled = true;
12592 setTimeout(bind(function () {
12593 this._mouseHoverThrottled = false;
12594 }, this), 32);
12595 },
12596
12597 _fireEvent: function (layers, e, type) {
12598 this._map._fireDOMEvent(e, type || e.type, layers);
12599 },
12600
12601 _bringToFront: function (layer) {
12602 var order = layer._order;
12603
12604 if (!order) { return; }
12605
12606 var next = order.next;
12607 var prev = order.prev;
12608
12609 if (next) {
12610 next.prev = prev;
12611 } else {
12612 // Already last
12613 return;
12614 }
12615 if (prev) {
12616 prev.next = next;
12617 } else if (next) {
12618 // Update first entry unless this is the
12619 // single entry
12620 this._drawFirst = next;
12621 }
12622
12623 order.prev = this._drawLast;
12624 this._drawLast.next = order;
12625
12626 order.next = null;
12627 this._drawLast = order;
12628
12629 this._requestRedraw(layer);
12630 },
12631
12632 _bringToBack: function (layer) {
12633 var order = layer._order;
12634
12635 if (!order) { return; }
12636
12637 var next = order.next;
12638 var prev = order.prev;
12639
12640 if (prev) {
12641 prev.next = next;
12642 } else {
12643 // Already first
12644 return;
12645 }
12646 if (next) {
12647 next.prev = prev;
12648 } else if (prev) {
12649 // Update last entry unless this is the
12650 // single entry
12651 this._drawLast = prev;
12652 }
12653
12654 order.prev = null;
12655
12656 order.next = this._drawFirst;
12657 this._drawFirst.prev = order;
12658 this._drawFirst = order;
12659
12660 this._requestRedraw(layer);
12661 }
12662});
12663
12664// @factory L.canvas(options?: Renderer options)
12665// Creates a Canvas renderer with the given options.
12666function canvas(options) {
12667 return Browser.canvas ? new Canvas(options) : null;
12668}
12669
12670/*
12671 * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
12672 */
12673
12674
12675var vmlCreate = (function () {
12676 try {
12677 document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
12678 return function (name) {
12679 return document.createElement('<lvml:' + name + ' class="lvml">');
12680 };
12681 } catch (e) {
12682 // Do not return fn from catch block so `e` can be garbage collected
12683 // See https://github.com/Leaflet/Leaflet/pull/7279
12684 }
12685 return function (name) {
12686 return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
12687 };
12688})();
12689
12690
12691/*
12692 * @class SVG
12693 *
12694 *
12695 * VML was deprecated in 2012, which means VML functionality exists only for backwards compatibility
12696 * with old versions of Internet Explorer.
12697 */
12698
12699// mixin to redefine some SVG methods to handle VML syntax which is similar but with some differences
12700var vmlMixin = {
12701
12702 _initContainer: function () {
12703 this._container = create$1('div', 'leaflet-vml-container');
12704 },
12705
12706 _update: function () {
12707 if (this._map._animatingZoom) { return; }
12708 Renderer.prototype._update.call(this);
12709 this.fire('update');
12710 },
12711
12712 _initPath: function (layer) {
12713 var container = layer._container = vmlCreate('shape');
12714
12715 addClass(container, 'leaflet-vml-shape ' + (this.options.className || ''));
12716
12717 container.coordsize = '1 1';
12718
12719 layer._path = vmlCreate('path');
12720 container.appendChild(layer._path);
12721
12722 this._updateStyle(layer);
12723 this._layers[stamp(layer)] = layer;
12724 },
12725
12726 _addPath: function (layer) {
12727 var container = layer._container;
12728 this._container.appendChild(container);
12729
12730 if (layer.options.interactive) {
12731 layer.addInteractiveTarget(container);
12732 }
12733 },
12734
12735 _removePath: function (layer) {
12736 var container = layer._container;
12737 remove(container);
12738 layer.removeInteractiveTarget(container);
12739 delete this._layers[stamp(layer)];
12740 },
12741
12742 _updateStyle: function (layer) {
12743 var stroke = layer._stroke,
12744 fill = layer._fill,
12745 options = layer.options,
12746 container = layer._container;
12747
12748 container.stroked = !!options.stroke;
12749 container.filled = !!options.fill;
12750
12751 if (options.stroke) {
12752 if (!stroke) {
12753 stroke = layer._stroke = vmlCreate('stroke');
12754 }
12755 container.appendChild(stroke);
12756 stroke.weight = options.weight + 'px';
12757 stroke.color = options.color;
12758 stroke.opacity = options.opacity;
12759
12760 if (options.dashArray) {
12761 stroke.dashStyle = isArray(options.dashArray) ?
12762 options.dashArray.join(' ') :
12763 options.dashArray.replace(/( *, *)/g, ' ');
12764 } else {
12765 stroke.dashStyle = '';
12766 }
12767 stroke.endcap = options.lineCap.replace('butt', 'flat');
12768 stroke.joinstyle = options.lineJoin;
12769
12770 } else if (stroke) {
12771 container.removeChild(stroke);
12772 layer._stroke = null;
12773 }
12774
12775 if (options.fill) {
12776 if (!fill) {
12777 fill = layer._fill = vmlCreate('fill');
12778 }
12779 container.appendChild(fill);
12780 fill.color = options.fillColor || options.color;
12781 fill.opacity = options.fillOpacity;
12782
12783 } else if (fill) {
12784 container.removeChild(fill);
12785 layer._fill = null;
12786 }
12787 },
12788
12789 _updateCircle: function (layer) {
12790 var p = layer._point.round(),
12791 r = Math.round(layer._radius),
12792 r2 = Math.round(layer._radiusY || r);
12793
12794 this._setPath(layer, layer._empty() ? 'M0 0' :
12795 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360));
12796 },
12797
12798 _setPath: function (layer, path) {
12799 layer._path.v = path;
12800 },
12801
12802 _bringToFront: function (layer) {
12803 toFront(layer._container);
12804 },
12805
12806 _bringToBack: function (layer) {
12807 toBack(layer._container);
12808 }
12809};
12810
12811var create = Browser.vml ? vmlCreate : svgCreate;
12812
12813/*
12814 * @class SVG
12815 * @inherits Renderer
12816 * @aka L.SVG
12817 *
12818 * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG).
12819 * Inherits `Renderer`.
12820 *
12821 * Due to [technical limitations](https://caniuse.com/svg), SVG is not
12822 * available in all web browsers, notably Android 2.x and 3.x.
12823 *
12824 * Although SVG is not available on IE7 and IE8, these browsers support
12825 * [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language)
12826 * (a now deprecated technology), and the SVG renderer will fall back to VML in
12827 * this case.
12828 *
12829 * @example
12830 *
12831 * Use SVG by default for all paths in the map:
12832 *
12833 * ```js
12834 * var map = L.map('map', {
12835 * renderer: L.svg()
12836 * });
12837 * ```
12838 *
12839 * Use a SVG renderer with extra padding for specific vector geometries:
12840 *
12841 * ```js
12842 * var map = L.map('map');
12843 * var myRenderer = L.svg({ padding: 0.5 });
12844 * var line = L.polyline( coordinates, { renderer: myRenderer } );
12845 * var circle = L.circle( center, { renderer: myRenderer } );
12846 * ```
12847 */
12848
12849var SVG = Renderer.extend({
12850
12851 _initContainer: function () {
12852 this._container = create('svg');
12853
12854 // makes it possible to click through svg root; we'll reset it back in individual paths
12855 this._container.setAttribute('pointer-events', 'none');
12856
12857 this._rootGroup = create('g');
12858 this._container.appendChild(this._rootGroup);
12859 },
12860
12861 _destroyContainer: function () {
12862 remove(this._container);
12863 off(this._container);
12864 delete this._container;
12865 delete this._rootGroup;
12866 delete this._svgSize;
12867 },
12868
12869 _update: function () {
12870 if (this._map._animatingZoom && this._bounds) { return; }
12871
12872 Renderer.prototype._update.call(this);
12873
12874 var b = this._bounds,
12875 size = b.getSize(),
12876 container = this._container;
12877
12878 // set size of svg-container if changed
12879 if (!this._svgSize || !this._svgSize.equals(size)) {
12880 this._svgSize = size;
12881 container.setAttribute('width', size.x);
12882 container.setAttribute('height', size.y);
12883 }
12884
12885 // movement: update container viewBox so that we don't have to change coordinates of individual layers
12886 setPosition(container, b.min);
12887 container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' '));
12888
12889 this.fire('update');
12890 },
12891
12892 // methods below are called by vector layers implementations
12893
12894 _initPath: function (layer) {
12895 var path = layer._path = create('path');
12896
12897 // @namespace Path
12898 // @option className: String = null
12899 // Custom class name set on an element. Only for SVG renderer.
12900 if (layer.options.className) {
12901 addClass(path, layer.options.className);
12902 }
12903
12904 if (layer.options.interactive) {
12905 addClass(path, 'leaflet-interactive');
12906 }
12907
12908 this._updateStyle(layer);
12909 this._layers[stamp(layer)] = layer;
12910 },
12911
12912 _addPath: function (layer) {
12913 if (!this._rootGroup) { this._initContainer(); }
12914 this._rootGroup.appendChild(layer._path);
12915 layer.addInteractiveTarget(layer._path);
12916 },
12917
12918 _removePath: function (layer) {
12919 remove(layer._path);
12920 layer.removeInteractiveTarget(layer._path);
12921 delete this._layers[stamp(layer)];
12922 },
12923
12924 _updatePath: function (layer) {
12925 layer._project();
12926 layer._update();
12927 },
12928
12929 _updateStyle: function (layer) {
12930 var path = layer._path,
12931 options = layer.options;
12932
12933 if (!path) { return; }
12934
12935 if (options.stroke) {
12936 path.setAttribute('stroke', options.color);
12937 path.setAttribute('stroke-opacity', options.opacity);
12938 path.setAttribute('stroke-width', options.weight);
12939 path.setAttribute('stroke-linecap', options.lineCap);
12940 path.setAttribute('stroke-linejoin', options.lineJoin);
12941
12942 if (options.dashArray) {
12943 path.setAttribute('stroke-dasharray', options.dashArray);
12944 } else {
12945 path.removeAttribute('stroke-dasharray');
12946 }
12947
12948 if (options.dashOffset) {
12949 path.setAttribute('stroke-dashoffset', options.dashOffset);
12950 } else {
12951 path.removeAttribute('stroke-dashoffset');
12952 }
12953 } else {
12954 path.setAttribute('stroke', 'none');
12955 }
12956
12957 if (options.fill) {
12958 path.setAttribute('fill', options.fillColor || options.color);
12959 path.setAttribute('fill-opacity', options.fillOpacity);
12960 path.setAttribute('fill-rule', options.fillRule || 'evenodd');
12961 } else {
12962 path.setAttribute('fill', 'none');
12963 }
12964 },
12965
12966 _updatePoly: function (layer, closed) {
12967 this._setPath(layer, pointsToPath(layer._parts, closed));
12968 },
12969
12970 _updateCircle: function (layer) {
12971 var p = layer._point,
12972 r = Math.max(Math.round(layer._radius), 1),
12973 r2 = Math.max(Math.round(layer._radiusY), 1) || r,
12974 arc = 'a' + r + ',' + r2 + ' 0 1,0 ';
12975
12976 // drawing a circle with two half-arcs
12977 var d = layer._empty() ? 'M0 0' :
12978 'M' + (p.x - r) + ',' + p.y +
12979 arc + (r * 2) + ',0 ' +
12980 arc + (-r * 2) + ',0 ';
12981
12982 this._setPath(layer, d);
12983 },
12984
12985 _setPath: function (layer, path) {
12986 layer._path.setAttribute('d', path);
12987 },
12988
12989 // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements
12990 _bringToFront: function (layer) {
12991 toFront(layer._path);
12992 },
12993
12994 _bringToBack: function (layer) {
12995 toBack(layer._path);
12996 }
12997});
12998
12999if (Browser.vml) {
13000 SVG.include(vmlMixin);
13001}
13002
13003// @namespace SVG
13004// @factory L.svg(options?: Renderer options)
13005// Creates a SVG renderer with the given options.
13006function svg(options) {
13007 return Browser.svg || Browser.vml ? new SVG(options) : null;
13008}
13009
13010Map.include({
13011 // @namespace Map; @method getRenderer(layer: Path): Renderer
13012 // Returns the instance of `Renderer` that should be used to render the given
13013 // `Path`. It will ensure that the `renderer` options of the map and paths
13014 // are respected, and that the renderers do exist on the map.
13015 getRenderer: function (layer) {
13016 // @namespace Path; @option renderer: Renderer
13017 // Use this specific instance of `Renderer` for this path. Takes
13018 // precedence over the map's [default renderer](#map-renderer).
13019 var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
13020
13021 if (!renderer) {
13022 renderer = this._renderer = this._createRenderer();
13023 }
13024
13025 if (!this.hasLayer(renderer)) {
13026 this.addLayer(renderer);
13027 }
13028 return renderer;
13029 },
13030
13031 _getPaneRenderer: function (name) {
13032 if (name === 'overlayPane' || name === undefined) {
13033 return false;
13034 }
13035
13036 var renderer = this._paneRenderers[name];
13037 if (renderer === undefined) {
13038 renderer = this._createRenderer({pane: name});
13039 this._paneRenderers[name] = renderer;
13040 }
13041 return renderer;
13042 },
13043
13044 _createRenderer: function (options) {
13045 // @namespace Map; @option preferCanvas: Boolean = false
13046 // Whether `Path`s should be rendered on a `Canvas` renderer.
13047 // By default, all `Path`s are rendered in a `SVG` renderer.
13048 return (this.options.preferCanvas && canvas(options)) || svg(options);
13049 }
13050});
13051
13052/*
13053 * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object.
13054 */
13055
13056/*
13057 * @class Rectangle
13058 * @aka L.Rectangle
13059 * @inherits Polygon
13060 *
13061 * A class for drawing rectangle overlays on a map. Extends `Polygon`.
13062 *
13063 * @example
13064 *
13065 * ```js
13066 * // define rectangle geographical bounds
13067 * var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
13068 *
13069 * // create an orange rectangle
13070 * L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map);
13071 *
13072 * // zoom the map to the rectangle bounds
13073 * map.fitBounds(bounds);
13074 * ```
13075 *
13076 */
13077
13078
13079var Rectangle = Polygon.extend({
13080 initialize: function (latLngBounds, options) {
13081 Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
13082 },
13083
13084 // @method setBounds(latLngBounds: LatLngBounds): this
13085 // Redraws the rectangle with the passed bounds.
13086 setBounds: function (latLngBounds) {
13087 return this.setLatLngs(this._boundsToLatLngs(latLngBounds));
13088 },
13089
13090 _boundsToLatLngs: function (latLngBounds) {
13091 latLngBounds = toLatLngBounds(latLngBounds);
13092 return [
13093 latLngBounds.getSouthWest(),
13094 latLngBounds.getNorthWest(),
13095 latLngBounds.getNorthEast(),
13096 latLngBounds.getSouthEast()
13097 ];
13098 }
13099});
13100
13101
13102// @factory L.rectangle(latLngBounds: LatLngBounds, options?: Polyline options)
13103function rectangle(latLngBounds, options) {
13104 return new Rectangle(latLngBounds, options);
13105}
13106
13107SVG.create = create;
13108SVG.pointsToPath = pointsToPath;
13109
13110GeoJSON.geometryToLayer = geometryToLayer;
13111GeoJSON.coordsToLatLng = coordsToLatLng;
13112GeoJSON.coordsToLatLngs = coordsToLatLngs;
13113GeoJSON.latLngToCoords = latLngToCoords;
13114GeoJSON.latLngsToCoords = latLngsToCoords;
13115GeoJSON.getFeature = getFeature;
13116GeoJSON.asFeature = asFeature;
13117
13118/*
13119 * L.Handler.BoxZoom is used to add shift-drag zoom interaction to the map
13120 * (zoom to a selected bounding box), enabled by default.
13121 */
13122
13123// @namespace Map
13124// @section Interaction Options
13125Map.mergeOptions({
13126 // @option boxZoom: Boolean = true
13127 // Whether the map can be zoomed to a rectangular area specified by
13128 // dragging the mouse while pressing the shift key.
13129 boxZoom: true
13130});
13131
13132var BoxZoom = Handler.extend({
13133 initialize: function (map) {
13134 this._map = map;
13135 this._container = map._container;
13136 this._pane = map._panes.overlayPane;
13137 this._resetStateTimeout = 0;
13138 map.on('unload', this._destroy, this);
13139 },
13140
13141 addHooks: function () {
13142 on(this._container, 'mousedown', this._onMouseDown, this);
13143 },
13144
13145 removeHooks: function () {
13146 off(this._container, 'mousedown', this._onMouseDown, this);
13147 },
13148
13149 moved: function () {
13150 return this._moved;
13151 },
13152
13153 _destroy: function () {
13154 remove(this._pane);
13155 delete this._pane;
13156 },
13157
13158 _resetState: function () {
13159 this._resetStateTimeout = 0;
13160 this._moved = false;
13161 },
13162
13163 _clearDeferredResetState: function () {
13164 if (this._resetStateTimeout !== 0) {
13165 clearTimeout(this._resetStateTimeout);
13166 this._resetStateTimeout = 0;
13167 }
13168 },
13169
13170 _onMouseDown: function (e) {
13171 if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
13172
13173 // Clear the deferred resetState if it hasn't executed yet, otherwise it
13174 // will interrupt the interaction and orphan a box element in the container.
13175 this._clearDeferredResetState();
13176 this._resetState();
13177
13178 disableTextSelection();
13179 disableImageDrag();
13180
13181 this._startPoint = this._map.mouseEventToContainerPoint(e);
13182
13183 on(document, {
13184 contextmenu: stop,
13185 mousemove: this._onMouseMove,
13186 mouseup: this._onMouseUp,
13187 keydown: this._onKeyDown
13188 }, this);
13189 },
13190
13191 _onMouseMove: function (e) {
13192 if (!this._moved) {
13193 this._moved = true;
13194
13195 this._box = create$1('div', 'leaflet-zoom-box', this._container);
13196 addClass(this._container, 'leaflet-crosshair');
13197
13198 this._map.fire('boxzoomstart');
13199 }
13200
13201 this._point = this._map.mouseEventToContainerPoint(e);
13202
13203 var bounds = new Bounds(this._point, this._startPoint),
13204 size = bounds.getSize();
13205
13206 setPosition(this._box, bounds.min);
13207
13208 this._box.style.width = size.x + 'px';
13209 this._box.style.height = size.y + 'px';
13210 },
13211
13212 _finish: function () {
13213 if (this._moved) {
13214 remove(this._box);
13215 removeClass(this._container, 'leaflet-crosshair');
13216 }
13217
13218 enableTextSelection();
13219 enableImageDrag();
13220
13221 off(document, {
13222 contextmenu: stop,
13223 mousemove: this._onMouseMove,
13224 mouseup: this._onMouseUp,
13225 keydown: this._onKeyDown
13226 }, this);
13227 },
13228
13229 _onMouseUp: function (e) {
13230 if ((e.which !== 1) && (e.button !== 1)) { return; }
13231
13232 this._finish();
13233
13234 if (!this._moved) { return; }
13235 // Postpone to next JS tick so internal click event handling
13236 // still see it as "moved".
13237 this._clearDeferredResetState();
13238 this._resetStateTimeout = setTimeout(bind(this._resetState, this), 0);
13239
13240 var bounds = new LatLngBounds(
13241 this._map.containerPointToLatLng(this._startPoint),
13242 this._map.containerPointToLatLng(this._point));
13243
13244 this._map
13245 .fitBounds(bounds)
13246 .fire('boxzoomend', {boxZoomBounds: bounds});
13247 },
13248
13249 _onKeyDown: function (e) {
13250 if (e.keyCode === 27) {
13251 this._finish();
13252 this._clearDeferredResetState();
13253 this._resetState();
13254 }
13255 }
13256});
13257
13258// @section Handlers
13259// @property boxZoom: Handler
13260// Box (shift-drag with mouse) zoom handler.
13261Map.addInitHook('addHandler', 'boxZoom', BoxZoom);
13262
13263/*
13264 * L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default.
13265 */
13266
13267// @namespace Map
13268// @section Interaction Options
13269
13270Map.mergeOptions({
13271 // @option doubleClickZoom: Boolean|String = true
13272 // Whether the map can be zoomed in by double clicking on it and
13273 // zoomed out by double clicking while holding shift. If passed
13274 // `'center'`, double-click zoom will zoom to the center of the
13275 // view regardless of where the mouse was.
13276 doubleClickZoom: true
13277});
13278
13279var DoubleClickZoom = Handler.extend({
13280 addHooks: function () {
13281 this._map.on('dblclick', this._onDoubleClick, this);
13282 },
13283
13284 removeHooks: function () {
13285 this._map.off('dblclick', this._onDoubleClick, this);
13286 },
13287
13288 _onDoubleClick: function (e) {
13289 var map = this._map,
13290 oldZoom = map.getZoom(),
13291 delta = map.options.zoomDelta,
13292 zoom = e.originalEvent.shiftKey ? oldZoom - delta : oldZoom + delta;
13293
13294 if (map.options.doubleClickZoom === 'center') {
13295 map.setZoom(zoom);
13296 } else {
13297 map.setZoomAround(e.containerPoint, zoom);
13298 }
13299 }
13300});
13301
13302// @section Handlers
13303//
13304// Map properties include interaction handlers that allow you to control
13305// interaction behavior in runtime, enabling or disabling certain features such
13306// as dragging or touch zoom (see `Handler` methods). For example:
13307//
13308// ```js
13309// map.doubleClickZoom.disable();
13310// ```
13311//
13312// @property doubleClickZoom: Handler
13313// Double click zoom handler.
13314Map.addInitHook('addHandler', 'doubleClickZoom', DoubleClickZoom);
13315
13316/*
13317 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
13318 */
13319
13320// @namespace Map
13321// @section Interaction Options
13322Map.mergeOptions({
13323 // @option dragging: Boolean = true
13324 // Whether the map is draggable with mouse/touch or not.
13325 dragging: true,
13326
13327 // @section Panning Inertia Options
13328 // @option inertia: Boolean = *
13329 // If enabled, panning of the map will have an inertia effect where
13330 // the map builds momentum while dragging and continues moving in
13331 // the same direction for some time. Feels especially nice on touch
13332 // devices. Enabled by default.
13333 inertia: true,
13334
13335 // @option inertiaDeceleration: Number = 3000
13336 // The rate with which the inertial movement slows down, in pixels/second².
13337 inertiaDeceleration: 3400, // px/s^2
13338
13339 // @option inertiaMaxSpeed: Number = Infinity
13340 // Max speed of the inertial movement, in pixels/second.
13341 inertiaMaxSpeed: Infinity, // px/s
13342
13343 // @option easeLinearity: Number = 0.2
13344 easeLinearity: 0.2,
13345
13346 // TODO refactor, move to CRS
13347 // @option worldCopyJump: Boolean = false
13348 // With this option enabled, the map tracks when you pan to another "copy"
13349 // of the world and seamlessly jumps to the original one so that all overlays
13350 // like markers and vector layers are still visible.
13351 worldCopyJump: false,
13352
13353 // @option maxBoundsViscosity: Number = 0.0
13354 // If `maxBounds` is set, this option will control how solid the bounds
13355 // are when dragging the map around. The default value of `0.0` allows the
13356 // user to drag outside the bounds at normal speed, higher values will
13357 // slow down map dragging outside bounds, and `1.0` makes the bounds fully
13358 // solid, preventing the user from dragging outside the bounds.
13359 maxBoundsViscosity: 0.0
13360});
13361
13362var Drag = Handler.extend({
13363 addHooks: function () {
13364 if (!this._draggable) {
13365 var map = this._map;
13366
13367 this._draggable = new Draggable(map._mapPane, map._container);
13368
13369 this._draggable.on({
13370 dragstart: this._onDragStart,
13371 drag: this._onDrag,
13372 dragend: this._onDragEnd
13373 }, this);
13374
13375 this._draggable.on('predrag', this._onPreDragLimit, this);
13376 if (map.options.worldCopyJump) {
13377 this._draggable.on('predrag', this._onPreDragWrap, this);
13378 map.on('zoomend', this._onZoomEnd, this);
13379
13380 map.whenReady(this._onZoomEnd, this);
13381 }
13382 }
13383 addClass(this._map._container, 'leaflet-grab leaflet-touch-drag');
13384 this._draggable.enable();
13385 this._positions = [];
13386 this._times = [];
13387 },
13388
13389 removeHooks: function () {
13390 removeClass(this._map._container, 'leaflet-grab');
13391 removeClass(this._map._container, 'leaflet-touch-drag');
13392 this._draggable.disable();
13393 },
13394
13395 moved: function () {
13396 return this._draggable && this._draggable._moved;
13397 },
13398
13399 moving: function () {
13400 return this._draggable && this._draggable._moving;
13401 },
13402
13403 _onDragStart: function () {
13404 var map = this._map;
13405
13406 map._stop();
13407 if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
13408 var bounds = toLatLngBounds(this._map.options.maxBounds);
13409
13410 this._offsetLimit = toBounds(
13411 this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
13412 this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1)
13413 .add(this._map.getSize()));
13414
13415 this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity));
13416 } else {
13417 this._offsetLimit = null;
13418 }
13419
13420 map
13421 .fire('movestart')
13422 .fire('dragstart');
13423
13424 if (map.options.inertia) {
13425 this._positions = [];
13426 this._times = [];
13427 }
13428 },
13429
13430 _onDrag: function (e) {
13431 if (this._map.options.inertia) {
13432 var time = this._lastTime = +new Date(),
13433 pos = this._lastPos = this._draggable._absPos || this._draggable._newPos;
13434
13435 this._positions.push(pos);
13436 this._times.push(time);
13437
13438 this._prunePositions(time);
13439 }
13440
13441 this._map
13442 .fire('move', e)
13443 .fire('drag', e);
13444 },
13445
13446 _prunePositions: function (time) {
13447 while (this._positions.length > 1 && time - this._times[0] > 50) {
13448 this._positions.shift();
13449 this._times.shift();
13450 }
13451 },
13452
13453 _onZoomEnd: function () {
13454 var pxCenter = this._map.getSize().divideBy(2),
13455 pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);
13456
13457 this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
13458 this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
13459 },
13460
13461 _viscousLimit: function (value, threshold) {
13462 return value - (value - threshold) * this._viscosity;
13463 },
13464
13465 _onPreDragLimit: function () {
13466 if (!this._viscosity || !this._offsetLimit) { return; }
13467
13468 var offset = this._draggable._newPos.subtract(this._draggable._startPos);
13469
13470 var limit = this._offsetLimit;
13471 if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); }
13472 if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); }
13473 if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); }
13474 if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); }
13475
13476 this._draggable._newPos = this._draggable._startPos.add(offset);
13477 },
13478
13479 _onPreDragWrap: function () {
13480 // TODO refactor to be able to adjust map pane position after zoom
13481 var worldWidth = this._worldWidth,
13482 halfWidth = Math.round(worldWidth / 2),
13483 dx = this._initialWorldOffset,
13484 x = this._draggable._newPos.x,
13485 newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
13486 newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
13487 newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
13488
13489 this._draggable._absPos = this._draggable._newPos.clone();
13490 this._draggable._newPos.x = newX;
13491 },
13492
13493 _onDragEnd: function (e) {
13494 var map = this._map,
13495 options = map.options,
13496
13497 noInertia = !options.inertia || e.noInertia || this._times.length < 2;
13498
13499 map.fire('dragend', e);
13500
13501 if (noInertia) {
13502 map.fire('moveend');
13503
13504 } else {
13505 this._prunePositions(+new Date());
13506
13507 var direction = this._lastPos.subtract(this._positions[0]),
13508 duration = (this._lastTime - this._times[0]) / 1000,
13509 ease = options.easeLinearity,
13510
13511 speedVector = direction.multiplyBy(ease / duration),
13512 speed = speedVector.distanceTo([0, 0]),
13513
13514 limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
13515 limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
13516
13517 decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
13518 offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
13519
13520 if (!offset.x && !offset.y) {
13521 map.fire('moveend');
13522
13523 } else {
13524 offset = map._limitOffset(offset, map.options.maxBounds);
13525
13526 requestAnimFrame(function () {
13527 map.panBy(offset, {
13528 duration: decelerationDuration,
13529 easeLinearity: ease,
13530 noMoveStart: true,
13531 animate: true
13532 });
13533 });
13534 }
13535 }
13536 }
13537});
13538
13539// @section Handlers
13540// @property dragging: Handler
13541// Map dragging handler (by both mouse and touch).
13542Map.addInitHook('addHandler', 'dragging', Drag);
13543
13544/*
13545 * L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
13546 */
13547
13548// @namespace Map
13549// @section Keyboard Navigation Options
13550Map.mergeOptions({
13551 // @option keyboard: Boolean = true
13552 // Makes the map focusable and allows users to navigate the map with keyboard
13553 // arrows and `+`/`-` keys.
13554 keyboard: true,
13555
13556 // @option keyboardPanDelta: Number = 80
13557 // Amount of pixels to pan when pressing an arrow key.
13558 keyboardPanDelta: 80
13559});
13560
13561var Keyboard = Handler.extend({
13562
13563 keyCodes: {
13564 left: [37],
13565 right: [39],
13566 down: [40],
13567 up: [38],
13568 zoomIn: [187, 107, 61, 171],
13569 zoomOut: [189, 109, 54, 173]
13570 },
13571
13572 initialize: function (map) {
13573 this._map = map;
13574
13575 this._setPanDelta(map.options.keyboardPanDelta);
13576 this._setZoomDelta(map.options.zoomDelta);
13577 },
13578
13579 addHooks: function () {
13580 var container = this._map._container;
13581
13582 // make the container focusable by tabbing
13583 if (container.tabIndex <= 0) {
13584 container.tabIndex = '0';
13585 }
13586
13587 on(container, {
13588 focus: this._onFocus,
13589 blur: this._onBlur,
13590 mousedown: this._onMouseDown
13591 }, this);
13592
13593 this._map.on({
13594 focus: this._addHooks,
13595 blur: this._removeHooks
13596 }, this);
13597 },
13598
13599 removeHooks: function () {
13600 this._removeHooks();
13601
13602 off(this._map._container, {
13603 focus: this._onFocus,
13604 blur: this._onBlur,
13605 mousedown: this._onMouseDown
13606 }, this);
13607
13608 this._map.off({
13609 focus: this._addHooks,
13610 blur: this._removeHooks
13611 }, this);
13612 },
13613
13614 _onMouseDown: function () {
13615 if (this._focused) { return; }
13616
13617 var body = document.body,
13618 docEl = document.documentElement,
13619 top = body.scrollTop || docEl.scrollTop,
13620 left = body.scrollLeft || docEl.scrollLeft;
13621
13622 this._map._container.focus();
13623
13624 window.scrollTo(left, top);
13625 },
13626
13627 _onFocus: function () {
13628 this._focused = true;
13629 this._map.fire('focus');
13630 },
13631
13632 _onBlur: function () {
13633 this._focused = false;
13634 this._map.fire('blur');
13635 },
13636
13637 _setPanDelta: function (panDelta) {
13638 var keys = this._panKeys = {},
13639 codes = this.keyCodes,
13640 i, len;
13641
13642 for (i = 0, len = codes.left.length; i < len; i++) {
13643 keys[codes.left[i]] = [-1 * panDelta, 0];
13644 }
13645 for (i = 0, len = codes.right.length; i < len; i++) {
13646 keys[codes.right[i]] = [panDelta, 0];
13647 }
13648 for (i = 0, len = codes.down.length; i < len; i++) {
13649 keys[codes.down[i]] = [0, panDelta];
13650 }
13651 for (i = 0, len = codes.up.length; i < len; i++) {
13652 keys[codes.up[i]] = [0, -1 * panDelta];
13653 }
13654 },
13655
13656 _setZoomDelta: function (zoomDelta) {
13657 var keys = this._zoomKeys = {},
13658 codes = this.keyCodes,
13659 i, len;
13660
13661 for (i = 0, len = codes.zoomIn.length; i < len; i++) {
13662 keys[codes.zoomIn[i]] = zoomDelta;
13663 }
13664 for (i = 0, len = codes.zoomOut.length; i < len; i++) {
13665 keys[codes.zoomOut[i]] = -zoomDelta;
13666 }
13667 },
13668
13669 _addHooks: function () {
13670 on(document, 'keydown', this._onKeyDown, this);
13671 },
13672
13673 _removeHooks: function () {
13674 off(document, 'keydown', this._onKeyDown, this);
13675 },
13676
13677 _onKeyDown: function (e) {
13678 if (e.altKey || e.ctrlKey || e.metaKey) { return; }
13679
13680 var key = e.keyCode,
13681 map = this._map,
13682 offset;
13683
13684 if (key in this._panKeys) {
13685 if (!map._panAnim || !map._panAnim._inProgress) {
13686 offset = this._panKeys[key];
13687 if (e.shiftKey) {
13688 offset = toPoint(offset).multiplyBy(3);
13689 }
13690
13691 map.panBy(offset);
13692
13693 if (map.options.maxBounds) {
13694 map.panInsideBounds(map.options.maxBounds);
13695 }
13696 }
13697 } else if (key in this._zoomKeys) {
13698 map.setZoom(map.getZoom() + (e.shiftKey ? 3 : 1) * this._zoomKeys[key]);
13699
13700 } else if (key === 27 && map._popup && map._popup.options.closeOnEscapeKey) {
13701 map.closePopup();
13702
13703 } else {
13704 return;
13705 }
13706
13707 stop(e);
13708 }
13709});
13710
13711// @section Handlers
13712// @section Handlers
13713// @property keyboard: Handler
13714// Keyboard navigation handler.
13715Map.addInitHook('addHandler', 'keyboard', Keyboard);
13716
13717/*
13718 * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
13719 */
13720
13721// @namespace Map
13722// @section Interaction Options
13723Map.mergeOptions({
13724 // @section Mouse wheel options
13725 // @option scrollWheelZoom: Boolean|String = true
13726 // Whether the map can be zoomed by using the mouse wheel. If passed `'center'`,
13727 // it will zoom to the center of the view regardless of where the mouse was.
13728 scrollWheelZoom: true,
13729
13730 // @option wheelDebounceTime: Number = 40
13731 // Limits the rate at which a wheel can fire (in milliseconds). By default
13732 // user can't zoom via wheel more often than once per 40 ms.
13733 wheelDebounceTime: 40,
13734
13735 // @option wheelPxPerZoomLevel: Number = 60
13736 // How many scroll pixels (as reported by [L.DomEvent.getWheelDelta](#domevent-getwheeldelta))
13737 // mean a change of one full zoom level. Smaller values will make wheel-zooming
13738 // faster (and vice versa).
13739 wheelPxPerZoomLevel: 60
13740});
13741
13742var ScrollWheelZoom = Handler.extend({
13743 addHooks: function () {
13744 on(this._map._container, 'wheel', this._onWheelScroll, this);
13745
13746 this._delta = 0;
13747 },
13748
13749 removeHooks: function () {
13750 off(this._map._container, 'wheel', this._onWheelScroll, this);
13751 },
13752
13753 _onWheelScroll: function (e) {
13754 var delta = getWheelDelta(e);
13755
13756 var debounce = this._map.options.wheelDebounceTime;
13757
13758 this._delta += delta;
13759 this._lastMousePos = this._map.mouseEventToContainerPoint(e);
13760
13761 if (!this._startTime) {
13762 this._startTime = +new Date();
13763 }
13764
13765 var left = Math.max(debounce - (+new Date() - this._startTime), 0);
13766
13767 clearTimeout(this._timer);
13768 this._timer = setTimeout(bind(this._performZoom, this), left);
13769
13770 stop(e);
13771 },
13772
13773 _performZoom: function () {
13774 var map = this._map,
13775 zoom = map.getZoom(),
13776 snap = this._map.options.zoomSnap || 0;
13777
13778 map._stop(); // stop panning and fly animations if any
13779
13780 // map the delta with a sigmoid function to -4..4 range leaning on -1..1
13781 var d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4),
13782 d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2,
13783 d4 = snap ? Math.ceil(d3 / snap) * snap : d3,
13784 delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom;
13785
13786 this._delta = 0;
13787 this._startTime = null;
13788
13789 if (!delta) { return; }
13790
13791 if (map.options.scrollWheelZoom === 'center') {
13792 map.setZoom(zoom + delta);
13793 } else {
13794 map.setZoomAround(this._lastMousePos, zoom + delta);
13795 }
13796 }
13797});
13798
13799// @section Handlers
13800// @property scrollWheelZoom: Handler
13801// Scroll wheel zoom handler.
13802Map.addInitHook('addHandler', 'scrollWheelZoom', ScrollWheelZoom);
13803
13804/*
13805 * L.Map.TapHold is used to simulate `contextmenu` event on long hold,
13806 * which otherwise is not fired by mobile Safari.
13807 */
13808
13809var tapHoldDelay = 600;
13810
13811// @namespace Map
13812// @section Interaction Options
13813Map.mergeOptions({
13814 // @section Touch interaction options
13815 // @option tapHold: Boolean
13816 // Enables simulation of `contextmenu` event, default is `true` for mobile Safari.
13817 tapHold: Browser.touchNative && Browser.safari && Browser.mobile,
13818
13819 // @option tapTolerance: Number = 15
13820 // The max number of pixels a user can shift his finger during touch
13821 // for it to be considered a valid tap.
13822 tapTolerance: 15
13823});
13824
13825var TapHold = Handler.extend({
13826 addHooks: function () {
13827 on(this._map._container, 'touchstart', this._onDown, this);
13828 },
13829
13830 removeHooks: function () {
13831 off(this._map._container, 'touchstart', this._onDown, this);
13832 },
13833
13834 _onDown: function (e) {
13835 clearTimeout(this._holdTimeout);
13836 if (e.touches.length !== 1) { return; }
13837
13838 var first = e.touches[0];
13839 this._startPos = this._newPos = new Point(first.clientX, first.clientY);
13840
13841 this._holdTimeout = setTimeout(bind(function () {
13842 this._cancel();
13843 if (!this._isTapValid()) { return; }
13844
13845 // prevent simulated mouse events https://w3c.github.io/touch-events/#mouse-events
13846 on(document, 'touchend', preventDefault);
13847 on(document, 'touchend touchcancel', this._cancelClickPrevent);
13848 this._simulateEvent('contextmenu', first);
13849 }, this), tapHoldDelay);
13850
13851 on(document, 'touchend touchcancel contextmenu', this._cancel, this);
13852 on(document, 'touchmove', this._onMove, this);
13853 },
13854
13855 _cancelClickPrevent: function cancelClickPrevent() {
13856 off(document, 'touchend', preventDefault);
13857 off(document, 'touchend touchcancel', cancelClickPrevent);
13858 },
13859
13860 _cancel: function () {
13861 clearTimeout(this._holdTimeout);
13862 off(document, 'touchend touchcancel contextmenu', this._cancel, this);
13863 off(document, 'touchmove', this._onMove, this);
13864 },
13865
13866 _onMove: function (e) {
13867 var first = e.touches[0];
13868 this._newPos = new Point(first.clientX, first.clientY);
13869 },
13870
13871 _isTapValid: function () {
13872 return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
13873 },
13874
13875 _simulateEvent: function (type, e) {
13876 var simulatedEvent = new MouseEvent(type, {
13877 bubbles: true,
13878 cancelable: true,
13879 view: window,
13880 // detail: 1,
13881 screenX: e.screenX,
13882 screenY: e.screenY,
13883 clientX: e.clientX,
13884 clientY: e.clientY,
13885 // button: 2,
13886 // buttons: 2
13887 });
13888
13889 simulatedEvent._simulated = true;
13890
13891 e.target.dispatchEvent(simulatedEvent);
13892 }
13893});
13894
13895// @section Handlers
13896// @property tapHold: Handler
13897// Long tap handler to simulate `contextmenu` event (useful in mobile Safari).
13898Map.addInitHook('addHandler', 'tapHold', TapHold);
13899
13900/*
13901 * L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers.
13902 */
13903
13904// @namespace Map
13905// @section Interaction Options
13906Map.mergeOptions({
13907 // @section Touch interaction options
13908 // @option touchZoom: Boolean|String = *
13909 // Whether the map can be zoomed by touch-dragging with two fingers. If
13910 // passed `'center'`, it will zoom to the center of the view regardless of
13911 // where the touch events (fingers) were. Enabled for touch-capable web
13912 // browsers.
13913 touchZoom: Browser.touch,
13914
13915 // @option bounceAtZoomLimits: Boolean = true
13916 // Set it to false if you don't want the map to zoom beyond min/max zoom
13917 // and then bounce back when pinch-zooming.
13918 bounceAtZoomLimits: true
13919});
13920
13921var TouchZoom = Handler.extend({
13922 addHooks: function () {
13923 addClass(this._map._container, 'leaflet-touch-zoom');
13924 on(this._map._container, 'touchstart', this._onTouchStart, this);
13925 },
13926
13927 removeHooks: function () {
13928 removeClass(this._map._container, 'leaflet-touch-zoom');
13929 off(this._map._container, 'touchstart', this._onTouchStart, this);
13930 },
13931
13932 _onTouchStart: function (e) {
13933 var map = this._map;
13934 if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
13935
13936 var p1 = map.mouseEventToContainerPoint(e.touches[0]),
13937 p2 = map.mouseEventToContainerPoint(e.touches[1]);
13938
13939 this._centerPoint = map.getSize()._divideBy(2);
13940 this._startLatLng = map.containerPointToLatLng(this._centerPoint);
13941 if (map.options.touchZoom !== 'center') {
13942 this._pinchStartLatLng = map.containerPointToLatLng(p1.add(p2)._divideBy(2));
13943 }
13944
13945 this._startDist = p1.distanceTo(p2);
13946 this._startZoom = map.getZoom();
13947
13948 this._moved = false;
13949 this._zooming = true;
13950
13951 map._stop();
13952
13953 on(document, 'touchmove', this._onTouchMove, this);
13954 on(document, 'touchend touchcancel', this._onTouchEnd, this);
13955
13956 preventDefault(e);
13957 },
13958
13959 _onTouchMove: function (e) {
13960 if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; }
13961
13962 var map = this._map,
13963 p1 = map.mouseEventToContainerPoint(e.touches[0]),
13964 p2 = map.mouseEventToContainerPoint(e.touches[1]),
13965 scale = p1.distanceTo(p2) / this._startDist;
13966
13967 this._zoom = map.getScaleZoom(scale, this._startZoom);
13968
13969 if (!map.options.bounceAtZoomLimits && (
13970 (this._zoom < map.getMinZoom() && scale < 1) ||
13971 (this._zoom > map.getMaxZoom() && scale > 1))) {
13972 this._zoom = map._limitZoom(this._zoom);
13973 }
13974
13975 if (map.options.touchZoom === 'center') {
13976 this._center = this._startLatLng;
13977 if (scale === 1) { return; }
13978 } else {
13979 // Get delta from pinch to center, so centerLatLng is delta applied to initial pinchLatLng
13980 var delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
13981 if (scale === 1 && delta.x === 0 && delta.y === 0) { return; }
13982 this._center = map.unproject(map.project(this._pinchStartLatLng, this._zoom).subtract(delta), this._zoom);
13983 }
13984
13985 if (!this._moved) {
13986 map._moveStart(true, false);
13987 this._moved = true;
13988 }
13989
13990 cancelAnimFrame(this._animRequest);
13991
13992 var moveFn = bind(map._move, map, this._center, this._zoom, {pinch: true, round: false});
13993 this._animRequest = requestAnimFrame(moveFn, this, true);
13994
13995 preventDefault(e);
13996 },
13997
13998 _onTouchEnd: function () {
13999 if (!this._moved || !this._zooming) {
14000 this._zooming = false;
14001 return;
14002 }
14003
14004 this._zooming = false;
14005 cancelAnimFrame(this._animRequest);
14006
14007 off(document, 'touchmove', this._onTouchMove, this);
14008 off(document, 'touchend touchcancel', this._onTouchEnd, this);
14009
14010 // Pinch updates GridLayers' levels only when zoomSnap is off, so zoomSnap becomes noUpdate.
14011 if (this._map.options.zoomAnimation) {
14012 this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.zoomSnap);
14013 } else {
14014 this._map._resetView(this._center, this._map._limitZoom(this._zoom));
14015 }
14016 }
14017});
14018
14019// @section Handlers
14020// @property touchZoom: Handler
14021// Touch zoom handler.
14022Map.addInitHook('addHandler', 'touchZoom', TouchZoom);
14023
14024Map.BoxZoom = BoxZoom;
14025Map.DoubleClickZoom = DoubleClickZoom;
14026Map.Drag = Drag;
14027Map.Keyboard = Keyboard;
14028Map.ScrollWheelZoom = ScrollWheelZoom;
14029Map.TapHold = TapHold;
14030Map.TouchZoom = TouchZoom;
14031
14032export { Bounds, Browser, CRS, Canvas, Circle, CircleMarker, Class, Control, DivIcon, DivOverlay, DomEvent, DomUtil, Draggable, Evented, FeatureGroup, GeoJSON, GridLayer, Handler, Icon, ImageOverlay, LatLng, LatLngBounds, Layer, LayerGroup, LineUtil, Map, Marker, Mixin, Path, Point, PolyUtil, Polygon, Polyline, Popup, PosAnimation, index as Projection, Rectangle, Renderer, SVG, SVGOverlay, TileLayer, Tooltip, Transformation, Util, VideoOverlay, bind, toBounds as bounds, canvas, circle, circleMarker, control, divIcon, extend, featureGroup, geoJSON, geoJson, gridLayer, icon, imageOverlay, toLatLng as latLng, toLatLngBounds as latLngBounds, layerGroup, createMap as map, marker, toPoint as point, polygon, polyline, popup, rectangle, setOptions, stamp, svg, svgOverlay, tileLayer, tooltip, toTransformation as transformation, version, videoOverlay };
14033//# sourceMappingURL=leaflet-src.esm.js.map