UNPKG

58.9 kBJavaScriptView Raw
1import * as Util from '../core/Util';
2import {Evented} from '../core/Events';
3import {EPSG3857} from '../geo/crs/CRS.EPSG3857';
4import {Point, toPoint} from '../geometry/Point';
5import {Bounds, toBounds} from '../geometry/Bounds';
6import {LatLng, toLatLng} from '../geo/LatLng';
7import {LatLngBounds, toLatLngBounds} from '../geo/LatLngBounds';
8import Browser from '../core/Browser';
9import * as DomEvent from '../dom/DomEvent';
10import * as DomUtil from '../dom/DomUtil';
11import {PosAnimation} from '../dom/PosAnimation';
12
13/*
14 * @class Map
15 * @aka L.Map
16 * @inherits Evented
17 *
18 * The central class of the API — it is used to create a map on a page and manipulate it.
19 *
20 * @example
21 *
22 * ```js
23 * // initialize the map on the "map" div with a given center and zoom
24 * var map = L.map('map', {
25 * center: [51.505, -0.09],
26 * zoom: 13
27 * });
28 * ```
29 *
30 */
31
32export var Map = Evented.extend({
33
34 options: {
35 // @section Map State Options
36 // @option crs: CRS = L.CRS.EPSG3857
37 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not
38 // sure what it means.
39 crs: EPSG3857,
40
41 // @option center: LatLng = undefined
42 // Initial geographic center of the map
43 center: undefined,
44
45 // @option zoom: Number = undefined
46 // Initial map zoom level
47 zoom: undefined,
48
49 // @option minZoom: Number = *
50 // Minimum zoom level of the map.
51 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
52 // the lowest of their `minZoom` options will be used instead.
53 minZoom: undefined,
54
55 // @option maxZoom: Number = *
56 // Maximum zoom level of the map.
57 // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
58 // the highest of their `maxZoom` options will be used instead.
59 maxZoom: undefined,
60
61 // @option layers: Layer[] = []
62 // Array of layers that will be added to the map initially
63 layers: [],
64
65 // @option maxBounds: LatLngBounds = null
66 // When this option is set, the map restricts the view to the given
67 // geographical bounds, bouncing the user back if the user tries to pan
68 // outside the view. To set the restriction dynamically, use
69 // [`setMaxBounds`](#map-setmaxbounds) method.
70 maxBounds: undefined,
71
72 // @option renderer: Renderer = *
73 // The default method for drawing vector layers on the map. `L.SVG`
74 // or `L.Canvas` by default depending on browser support.
75 renderer: undefined,
76
77
78 // @section Animation Options
79 // @option zoomAnimation: Boolean = true
80 // Whether the map zoom animation is enabled. By default it's enabled
81 // in all browsers that support CSS3 Transitions except Android.
82 zoomAnimation: true,
83
84 // @option zoomAnimationThreshold: Number = 4
85 // Won't animate zoom if the zoom difference exceeds this value.
86 zoomAnimationThreshold: 4,
87
88 // @option fadeAnimation: Boolean = true
89 // Whether the tile fade animation is enabled. By default it's enabled
90 // in all browsers that support CSS3 Transitions except Android.
91 fadeAnimation: true,
92
93 // @option markerZoomAnimation: Boolean = true
94 // Whether markers animate their zoom with the zoom animation, if disabled
95 // they will disappear for the length of the animation. By default it's
96 // enabled in all browsers that support CSS3 Transitions except Android.
97 markerZoomAnimation: true,
98
99 // @option transform3DLimit: Number = 2^23
100 // Defines the maximum size of a CSS translation transform. The default
101 // value should not be changed unless a web browser positions layers in
102 // the wrong place after doing a large `panBy`.
103 transform3DLimit: 8388608, // Precision limit of a 32-bit float
104
105 // @section Interaction Options
106 // @option zoomSnap: Number = 1
107 // Forces the map's zoom level to always be a multiple of this, particularly
108 // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom.
109 // By default, the zoom level snaps to the nearest integer; lower values
110 // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0`
111 // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom.
112 zoomSnap: 1,
113
114 // @option zoomDelta: Number = 1
115 // Controls how much the map's zoom level will change after a
116 // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+`
117 // or `-` on the keyboard, or using the [zoom controls](#control-zoom).
118 // Values smaller than `1` (e.g. `0.5`) allow for greater granularity.
119 zoomDelta: 1,
120
121 // @option trackResize: Boolean = true
122 // Whether the map automatically handles browser window resize to update itself.
123 trackResize: true
124 },
125
126 initialize: function (id, options) { // (HTMLElement or String, Object)
127 options = Util.setOptions(this, options);
128
129 // Make sure to assign internal flags at the beginning,
130 // to avoid inconsistent state in some edge cases.
131 this._handlers = [];
132 this._layers = {};
133 this._zoomBoundLayers = {};
134 this._sizeChanged = true;
135
136 this._initContainer(id);
137 this._initLayout();
138
139 // hack for https://github.com/Leaflet/Leaflet/issues/1980
140 this._onResize = Util.bind(this._onResize, this);
141
142 this._initEvents();
143
144 if (options.maxBounds) {
145 this.setMaxBounds(options.maxBounds);
146 }
147
148 if (options.zoom !== undefined) {
149 this._zoom = this._limitZoom(options.zoom);
150 }
151
152 if (options.center && options.zoom !== undefined) {
153 this.setView(toLatLng(options.center), options.zoom, {reset: true});
154 }
155
156 this.callInitHooks();
157
158 // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
159 this._zoomAnimated = DomUtil.TRANSITION && Browser.any3d && !Browser.mobileOpera &&
160 this.options.zoomAnimation;
161
162 // zoom transitions run with the same duration for all layers, so if one of transitionend events
163 // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
164 if (this._zoomAnimated) {
165 this._createAnimProxy();
166 DomEvent.on(this._proxy, DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
167 }
168
169 this._addLayers(this.options.layers);
170 },
171
172
173 // @section Methods for modifying map state
174
175 // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
176 // Sets the view of the map (geographical center and zoom) with the given
177 // animation options.
178 setView: function (center, zoom, options) {
179
180 zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
181 center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds);
182 options = options || {};
183
184 this._stop();
185
186 if (this._loaded && !options.reset && options !== true) {
187
188 if (options.animate !== undefined) {
189 options.zoom = Util.extend({animate: options.animate}, options.zoom);
190 options.pan = Util.extend({animate: options.animate, duration: options.duration}, options.pan);
191 }
192
193 // try animating pan or zoom
194 var moved = (this._zoom !== zoom) ?
195 this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
196 this._tryAnimatedPan(center, options.pan);
197
198 if (moved) {
199 // prevent resize handler call, the view will refresh after animation anyway
200 clearTimeout(this._sizeTimer);
201 return this;
202 }
203 }
204
205 // animation didn't start, just reset the map view
206 this._resetView(center, zoom);
207
208 return this;
209 },
210
211 // @method setZoom(zoom: Number, options?: Zoom/pan options): this
212 // Sets the zoom of the map.
213 setZoom: function (zoom, options) {
214 if (!this._loaded) {
215 this._zoom = zoom;
216 return this;
217 }
218 return this.setView(this.getCenter(), zoom, {zoom: options});
219 },
220
221 // @method zoomIn(delta?: Number, options?: Zoom options): this
222 // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
223 zoomIn: function (delta, options) {
224 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
225 return this.setZoom(this._zoom + delta, options);
226 },
227
228 // @method zoomOut(delta?: Number, options?: Zoom options): this
229 // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
230 zoomOut: function (delta, options) {
231 delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
232 return this.setZoom(this._zoom - delta, options);
233 },
234
235 // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this
236 // Zooms the map while keeping a specified geographical point on the map
237 // stationary (e.g. used internally for scroll zoom and double-click zoom).
238 // @alternative
239 // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this
240 // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary.
241 setZoomAround: function (latlng, zoom, options) {
242 var scale = this.getZoomScale(zoom),
243 viewHalf = this.getSize().divideBy(2),
244 containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng),
245
246 centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
247 newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
248
249 return this.setView(newCenter, zoom, {zoom: options});
250 },
251
252 _getBoundsCenterZoom: function (bounds, options) {
253
254 options = options || {};
255 bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds);
256
257 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
258 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
259
260 zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
261
262 zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom;
263
264 if (zoom === Infinity) {
265 return {
266 center: bounds.getCenter(),
267 zoom: zoom
268 };
269 }
270
271 var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
272
273 swPoint = this.project(bounds.getSouthWest(), zoom),
274 nePoint = this.project(bounds.getNorthEast(), zoom),
275 center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
276
277 return {
278 center: center,
279 zoom: zoom
280 };
281 },
282
283 // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this
284 // Sets a map view that contains the given geographical bounds with the
285 // maximum zoom level possible.
286 fitBounds: function (bounds, options) {
287
288 bounds = toLatLngBounds(bounds);
289
290 if (!bounds.isValid()) {
291 throw new Error('Bounds are not valid.');
292 }
293
294 var target = this._getBoundsCenterZoom(bounds, options);
295 return this.setView(target.center, target.zoom, options);
296 },
297
298 // @method fitWorld(options?: fitBounds options): this
299 // Sets a map view that mostly contains the whole world with the maximum
300 // zoom level possible.
301 fitWorld: function (options) {
302 return this.fitBounds([[-90, -180], [90, 180]], options);
303 },
304
305 // @method panTo(latlng: LatLng, options?: Pan options): this
306 // Pans the map to a given center.
307 panTo: function (center, options) { // (LatLng)
308 return this.setView(center, this._zoom, {pan: options});
309 },
310
311 // @method panBy(offset: Point, options?: Pan options): this
312 // Pans the map by a given number of pixels (animated).
313 panBy: function (offset, options) {
314 offset = toPoint(offset).round();
315 options = options || {};
316
317 if (!offset.x && !offset.y) {
318 return this.fire('moveend');
319 }
320 // If we pan too far, Chrome gets issues with tiles
321 // and makes them disappear or appear in the wrong place (slightly offset) #2602
322 if (options.animate !== true && !this.getSize().contains(offset)) {
323 this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
324 return this;
325 }
326
327 if (!this._panAnim) {
328 this._panAnim = new PosAnimation();
329
330 this._panAnim.on({
331 'step': this._onPanTransitionStep,
332 'end': this._onPanTransitionEnd
333 }, this);
334 }
335
336 // don't fire movestart if animating inertia
337 if (!options.noMoveStart) {
338 this.fire('movestart');
339 }
340
341 // animate pan unless animate: false specified
342 if (options.animate !== false) {
343 DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
344
345 var newPos = this._getMapPanePos().subtract(offset).round();
346 this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
347 } else {
348 this._rawPanBy(offset);
349 this.fire('move').fire('moveend');
350 }
351
352 return this;
353 },
354
355 // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
356 // Sets the view of the map (geographical center and zoom) performing a smooth
357 // pan-zoom animation.
358 flyTo: function (targetCenter, targetZoom, options) {
359
360 options = options || {};
361 if (options.animate === false || !Browser.any3d) {
362 return this.setView(targetCenter, targetZoom, options);
363 }
364
365 this._stop();
366
367 var from = this.project(this.getCenter()),
368 to = this.project(targetCenter),
369 size = this.getSize(),
370 startZoom = this._zoom;
371
372 targetCenter = toLatLng(targetCenter);
373 targetZoom = targetZoom === undefined ? startZoom : targetZoom;
374
375 var w0 = Math.max(size.x, size.y),
376 w1 = w0 * this.getZoomScale(startZoom, targetZoom),
377 u1 = (to.distanceTo(from)) || 1,
378 rho = 1.42,
379 rho2 = rho * rho;
380
381 function r(i) {
382 var s1 = i ? -1 : 1,
383 s2 = i ? w1 : w0,
384 t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
385 b1 = 2 * s2 * rho2 * u1,
386 b = t1 / b1,
387 sq = Math.sqrt(b * b + 1) - b;
388
389 // workaround for floating point precision bug when sq = 0, log = -Infinite,
390 // thus triggering an infinite loop in flyTo
391 var log = sq < 0.000000001 ? -18 : Math.log(sq);
392
393 return log;
394 }
395
396 function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
397 function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
398 function tanh(n) { return sinh(n) / cosh(n); }
399
400 var r0 = r(0);
401
402 function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
403 function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }
404
405 function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }
406
407 var start = Date.now(),
408 S = (r(1) - r0) / rho,
409 duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
410
411 function frame() {
412 var t = (Date.now() - start) / duration,
413 s = easeOut(t) * S;
414
415 if (t <= 1) {
416 this._flyToFrame = Util.requestAnimFrame(frame, this);
417
418 this._move(
419 this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
420 this.getScaleZoom(w0 / w(s), startZoom),
421 {flyTo: true});
422
423 } else {
424 this
425 ._move(targetCenter, targetZoom)
426 ._moveEnd(true);
427 }
428 }
429
430 this._moveStart(true, options.noMoveStart);
431
432 frame.call(this);
433 return this;
434 },
435
436 // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
437 // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
438 // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
439 flyToBounds: function (bounds, options) {
440 var target = this._getBoundsCenterZoom(bounds, options);
441 return this.flyTo(target.center, target.zoom, options);
442 },
443
444 // @method setMaxBounds(bounds: LatLngBounds): this
445 // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option).
446 setMaxBounds: function (bounds) {
447 bounds = toLatLngBounds(bounds);
448
449 if (!bounds.isValid()) {
450 this.options.maxBounds = null;
451 return this.off('moveend', this._panInsideMaxBounds);
452 } else if (this.options.maxBounds) {
453 this.off('moveend', this._panInsideMaxBounds);
454 }
455
456 this.options.maxBounds = bounds;
457
458 if (this._loaded) {
459 this._panInsideMaxBounds();
460 }
461
462 return this.on('moveend', this._panInsideMaxBounds);
463 },
464
465 // @method setMinZoom(zoom: Number): this
466 // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option).
467 setMinZoom: function (zoom) {
468 var oldZoom = this.options.minZoom;
469 this.options.minZoom = zoom;
470
471 if (this._loaded && oldZoom !== zoom) {
472 this.fire('zoomlevelschange');
473
474 if (this.getZoom() < this.options.minZoom) {
475 return this.setZoom(zoom);
476 }
477 }
478
479 return this;
480 },
481
482 // @method setMaxZoom(zoom: Number): this
483 // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option).
484 setMaxZoom: function (zoom) {
485 var oldZoom = this.options.maxZoom;
486 this.options.maxZoom = zoom;
487
488 if (this._loaded && oldZoom !== zoom) {
489 this.fire('zoomlevelschange');
490
491 if (this.getZoom() > this.options.maxZoom) {
492 return this.setZoom(zoom);
493 }
494 }
495
496 return this;
497 },
498
499 // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this
500 // 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.
501 panInsideBounds: function (bounds, options) {
502 this._enforcingBounds = true;
503 var center = this.getCenter(),
504 newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds));
505
506 if (!center.equals(newCenter)) {
507 this.panTo(newCenter, options);
508 }
509
510 this._enforcingBounds = false;
511 return this;
512 },
513
514 // @method panInside(latlng: LatLng, options?: padding options): this
515 // Pans the map the minimum amount to make the `latlng` visible. Use
516 // padding options to fit the display to more restricted bounds.
517 // If `latlng` is already within the (optionally padded) display bounds,
518 // the map will not be panned.
519 panInside: function (latlng, options) {
520 options = options || {};
521
522 var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
523 paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
524 pixelCenter = this.project(this.getCenter()),
525 pixelPoint = this.project(latlng),
526 pixelBounds = this.getPixelBounds(),
527 paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
528 paddedSize = paddedBounds.getSize();
529
530 if (!paddedBounds.contains(pixelPoint)) {
531 this._enforcingBounds = true;
532 var centerOffset = pixelPoint.subtract(paddedBounds.getCenter());
533 var offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize);
534 pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x;
535 pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y;
536 this.panTo(this.unproject(pixelCenter), options);
537 this._enforcingBounds = false;
538 }
539 return this;
540 },
541
542 // @method invalidateSize(options: Zoom/pan options): this
543 // Checks if the map container size changed and updates the map if so —
544 // call it after you've changed the map size dynamically, also animating
545 // pan by default. If `options.pan` is `false`, panning will not occur.
546 // If `options.debounceMoveend` is `true`, it will delay `moveend` event so
547 // that it doesn't happen often even if the method is called many
548 // times in a row.
549
550 // @alternative
551 // @method invalidateSize(animate: Boolean): this
552 // Checks if the map container size changed and updates the map if so —
553 // call it after you've changed the map size dynamically, also animating
554 // pan by default.
555 invalidateSize: function (options) {
556 if (!this._loaded) { return this; }
557
558 options = Util.extend({
559 animate: false,
560 pan: true
561 }, options === true ? {animate: true} : options);
562
563 var oldSize = this.getSize();
564 this._sizeChanged = true;
565 this._lastCenter = null;
566
567 var newSize = this.getSize(),
568 oldCenter = oldSize.divideBy(2).round(),
569 newCenter = newSize.divideBy(2).round(),
570 offset = oldCenter.subtract(newCenter);
571
572 if (!offset.x && !offset.y) { return this; }
573
574 if (options.animate && options.pan) {
575 this.panBy(offset);
576
577 } else {
578 if (options.pan) {
579 this._rawPanBy(offset);
580 }
581
582 this.fire('move');
583
584 if (options.debounceMoveend) {
585 clearTimeout(this._sizeTimer);
586 this._sizeTimer = setTimeout(Util.bind(this.fire, this, 'moveend'), 200);
587 } else {
588 this.fire('moveend');
589 }
590 }
591
592 // @section Map state change events
593 // @event resize: ResizeEvent
594 // Fired when the map is resized.
595 return this.fire('resize', {
596 oldSize: oldSize,
597 newSize: newSize
598 });
599 },
600
601 // @section Methods for modifying map state
602 // @method stop(): this
603 // Stops the currently running `panTo` or `flyTo` animation, if any.
604 stop: function () {
605 this.setZoom(this._limitZoom(this._zoom));
606 if (!this.options.zoomSnap) {
607 this.fire('viewreset');
608 }
609 return this._stop();
610 },
611
612 // @section Geolocation methods
613 // @method locate(options?: Locate options): this
614 // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
615 // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
616 // and optionally sets the map view to the user's location with respect to
617 // detection accuracy (or to the world view if geolocation failed).
618 // Note that, if your page doesn't use HTTPS, this method will fail in
619 // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
620 // See `Locate options` for more details.
621 locate: function (options) {
622
623 options = this._locateOptions = Util.extend({
624 timeout: 10000,
625 watch: false
626 // setView: false
627 // maxZoom: <Number>
628 // maximumAge: 0
629 // enableHighAccuracy: false
630 }, options);
631
632 if (!('geolocation' in navigator)) {
633 this._handleGeolocationError({
634 code: 0,
635 message: 'Geolocation not supported.'
636 });
637 return this;
638 }
639
640 var onResponse = Util.bind(this._handleGeolocationResponse, this),
641 onError = Util.bind(this._handleGeolocationError, this);
642
643 if (options.watch) {
644 this._locationWatchId =
645 navigator.geolocation.watchPosition(onResponse, onError, options);
646 } else {
647 navigator.geolocation.getCurrentPosition(onResponse, onError, options);
648 }
649 return this;
650 },
651
652 // @method stopLocate(): this
653 // Stops watching location previously initiated by `map.locate({watch: true})`
654 // and aborts resetting the map view if map.locate was called with
655 // `{setView: true}`.
656 stopLocate: function () {
657 if (navigator.geolocation && navigator.geolocation.clearWatch) {
658 navigator.geolocation.clearWatch(this._locationWatchId);
659 }
660 if (this._locateOptions) {
661 this._locateOptions.setView = false;
662 }
663 return this;
664 },
665
666 _handleGeolocationError: function (error) {
667 if (!this._container._leaflet_id) { return; }
668
669 var c = error.code,
670 message = error.message ||
671 (c === 1 ? 'permission denied' :
672 (c === 2 ? 'position unavailable' : 'timeout'));
673
674 if (this._locateOptions.setView && !this._loaded) {
675 this.fitWorld();
676 }
677
678 // @section Location events
679 // @event locationerror: ErrorEvent
680 // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
681 this.fire('locationerror', {
682 code: c,
683 message: 'Geolocation error: ' + message + '.'
684 });
685 },
686
687 _handleGeolocationResponse: function (pos) {
688 if (!this._container._leaflet_id) { return; }
689
690 var lat = pos.coords.latitude,
691 lng = pos.coords.longitude,
692 latlng = new LatLng(lat, lng),
693 bounds = latlng.toBounds(pos.coords.accuracy * 2),
694 options = this._locateOptions;
695
696 if (options.setView) {
697 var zoom = this.getBoundsZoom(bounds);
698 this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
699 }
700
701 var data = {
702 latlng: latlng,
703 bounds: bounds,
704 timestamp: pos.timestamp
705 };
706
707 for (var i in pos.coords) {
708 if (typeof pos.coords[i] === 'number') {
709 data[i] = pos.coords[i];
710 }
711 }
712
713 // @event locationfound: LocationEvent
714 // Fired when geolocation (using the [`locate`](#map-locate) method)
715 // went successfully.
716 this.fire('locationfound', data);
717 },
718
719 // TODO Appropriate docs section?
720 // @section Other Methods
721 // @method addHandler(name: String, HandlerClass: Function): this
722 // Adds a new `Handler` to the map, given its name and constructor function.
723 addHandler: function (name, HandlerClass) {
724 if (!HandlerClass) { return this; }
725
726 var handler = this[name] = new HandlerClass(this);
727
728 this._handlers.push(handler);
729
730 if (this.options[name]) {
731 handler.enable();
732 }
733
734 return this;
735 },
736
737 // @method remove(): this
738 // Destroys the map and clears all related event listeners.
739 remove: function () {
740
741 this._initEvents(true);
742 if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); }
743
744 if (this._containerId !== this._container._leaflet_id) {
745 throw new Error('Map container is being reused by another instance');
746 }
747
748 try {
749 // throws error in IE6-8
750 delete this._container._leaflet_id;
751 delete this._containerId;
752 } catch (e) {
753 /*eslint-disable */
754 this._container._leaflet_id = undefined;
755 /* eslint-enable */
756 this._containerId = undefined;
757 }
758
759 if (this._locationWatchId !== undefined) {
760 this.stopLocate();
761 }
762
763 this._stop();
764
765 DomUtil.remove(this._mapPane);
766
767 if (this._clearControlPos) {
768 this._clearControlPos();
769 }
770 if (this._resizeRequest) {
771 Util.cancelAnimFrame(this._resizeRequest);
772 this._resizeRequest = null;
773 }
774
775 this._clearHandlers();
776
777 if (this._loaded) {
778 // @section Map state change events
779 // @event unload: Event
780 // Fired when the map is destroyed with [remove](#map-remove) method.
781 this.fire('unload');
782 }
783
784 var i;
785 for (i in this._layers) {
786 this._layers[i].remove();
787 }
788 for (i in this._panes) {
789 DomUtil.remove(this._panes[i]);
790 }
791
792 this._layers = [];
793 this._panes = [];
794 delete this._mapPane;
795 delete this._renderer;
796
797 return this;
798 },
799
800 // @section Other Methods
801 // @method createPane(name: String, container?: HTMLElement): HTMLElement
802 // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already,
803 // then returns it. The pane is created as a child of `container`, or
804 // as a child of the main map pane if not set.
805 createPane: function (name, container) {
806 var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''),
807 pane = DomUtil.create('div', className, container || this._mapPane);
808
809 if (name) {
810 this._panes[name] = pane;
811 }
812 return pane;
813 },
814
815 // @section Methods for Getting Map State
816
817 // @method getCenter(): LatLng
818 // Returns the geographical center of the map view
819 getCenter: function () {
820 this._checkIfLoaded();
821
822 if (this._lastCenter && !this._moved()) {
823 return this._lastCenter;
824 }
825 return this.layerPointToLatLng(this._getCenterLayerPoint());
826 },
827
828 // @method getZoom(): Number
829 // Returns the current zoom level of the map view
830 getZoom: function () {
831 return this._zoom;
832 },
833
834 // @method getBounds(): LatLngBounds
835 // Returns the geographical bounds visible in the current map view
836 getBounds: function () {
837 var bounds = this.getPixelBounds(),
838 sw = this.unproject(bounds.getBottomLeft()),
839 ne = this.unproject(bounds.getTopRight());
840
841 return new LatLngBounds(sw, ne);
842 },
843
844 // @method getMinZoom(): Number
845 // 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.
846 getMinZoom: function () {
847 return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom;
848 },
849
850 // @method getMaxZoom(): Number
851 // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers).
852 getMaxZoom: function () {
853 return this.options.maxZoom === undefined ?
854 (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
855 this.options.maxZoom;
856 },
857
858 // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean, padding?: Point): Number
859 // Returns the maximum zoom level on which the given bounds fit to the map
860 // view in its entirety. If `inside` (optional) is set to `true`, the method
861 // instead returns the minimum zoom level on which the map view fits into
862 // the given bounds in its entirety.
863 getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
864 bounds = toLatLngBounds(bounds);
865 padding = toPoint(padding || [0, 0]);
866
867 var zoom = this.getZoom() || 0,
868 min = this.getMinZoom(),
869 max = this.getMaxZoom(),
870 nw = bounds.getNorthWest(),
871 se = bounds.getSouthEast(),
872 size = this.getSize().subtract(padding),
873 boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
874 snap = Browser.any3d ? this.options.zoomSnap : 1,
875 scalex = size.x / boundsSize.x,
876 scaley = size.y / boundsSize.y,
877 scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);
878
879 zoom = this.getScaleZoom(scale, zoom);
880
881 if (snap) {
882 zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
883 zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
884 }
885
886 return Math.max(min, Math.min(max, zoom));
887 },
888
889 // @method getSize(): Point
890 // Returns the current size of the map container (in pixels).
891 getSize: function () {
892 if (!this._size || this._sizeChanged) {
893 this._size = new Point(
894 this._container.clientWidth || 0,
895 this._container.clientHeight || 0);
896
897 this._sizeChanged = false;
898 }
899 return this._size.clone();
900 },
901
902 // @method getPixelBounds(): Bounds
903 // Returns the bounds of the current map view in projected pixel
904 // coordinates (sometimes useful in layer and overlay implementations).
905 getPixelBounds: function (center, zoom) {
906 var topLeftPoint = this._getTopLeftPoint(center, zoom);
907 return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
908 },
909
910 // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to
911 // the map pane? "left point of the map layer" can be confusing, specially
912 // since there can be negative offsets.
913 // @method getPixelOrigin(): Point
914 // Returns the projected pixel coordinates of the top left point of
915 // the map layer (useful in custom layer and overlay implementations).
916 getPixelOrigin: function () {
917 this._checkIfLoaded();
918 return this._pixelOrigin;
919 },
920
921 // @method getPixelWorldBounds(zoom?: Number): Bounds
922 // Returns the world's bounds in pixel coordinates for zoom level `zoom`.
923 // If `zoom` is omitted, the map's current zoom level is used.
924 getPixelWorldBounds: function (zoom) {
925 return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom);
926 },
927
928 // @section Other Methods
929
930 // @method getPane(pane: String|HTMLElement): HTMLElement
931 // Returns a [map pane](#map-pane), given its name or its HTML element (its identity).
932 getPane: function (pane) {
933 return typeof pane === 'string' ? this._panes[pane] : pane;
934 },
935
936 // @method getPanes(): Object
937 // Returns a plain object containing the names of all [panes](#map-pane) as keys and
938 // the panes as values.
939 getPanes: function () {
940 return this._panes;
941 },
942
943 // @method getContainer: HTMLElement
944 // Returns the HTML element that contains the map.
945 getContainer: function () {
946 return this._container;
947 },
948
949
950 // @section Conversion Methods
951
952 // @method getZoomScale(toZoom: Number, fromZoom: Number): Number
953 // Returns the scale factor to be applied to a map transition from zoom level
954 // `fromZoom` to `toZoom`. Used internally to help with zoom animations.
955 getZoomScale: function (toZoom, fromZoom) {
956 // TODO replace with universal implementation after refactoring projections
957 var crs = this.options.crs;
958 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
959 return crs.scale(toZoom) / crs.scale(fromZoom);
960 },
961
962 // @method getScaleZoom(scale: Number, fromZoom: Number): Number
963 // Returns the zoom level that the map would end up at, if it is at `fromZoom`
964 // level and everything is scaled by a factor of `scale`. Inverse of
965 // [`getZoomScale`](#map-getZoomScale).
966 getScaleZoom: function (scale, fromZoom) {
967 var crs = this.options.crs;
968 fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
969 var zoom = crs.zoom(scale * crs.scale(fromZoom));
970 return isNaN(zoom) ? Infinity : zoom;
971 },
972
973 // @method project(latlng: LatLng, zoom: Number): Point
974 // Projects a geographical coordinate `LatLng` according to the projection
975 // of the map's CRS, then scales it according to `zoom` and the CRS's
976 // `Transformation`. The result is pixel coordinate relative to
977 // the CRS origin.
978 project: function (latlng, zoom) {
979 zoom = zoom === undefined ? this._zoom : zoom;
980 return this.options.crs.latLngToPoint(toLatLng(latlng), zoom);
981 },
982
983 // @method unproject(point: Point, zoom: Number): LatLng
984 // Inverse of [`project`](#map-project).
985 unproject: function (point, zoom) {
986 zoom = zoom === undefined ? this._zoom : zoom;
987 return this.options.crs.pointToLatLng(toPoint(point), zoom);
988 },
989
990 // @method layerPointToLatLng(point: Point): LatLng
991 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
992 // returns the corresponding geographical coordinate (for the current zoom level).
993 layerPointToLatLng: function (point) {
994 var projectedPoint = toPoint(point).add(this.getPixelOrigin());
995 return this.unproject(projectedPoint);
996 },
997
998 // @method latLngToLayerPoint(latlng: LatLng): Point
999 // Given a geographical coordinate, returns the corresponding pixel coordinate
1000 // relative to the [origin pixel](#map-getpixelorigin).
1001 latLngToLayerPoint: function (latlng) {
1002 var projectedPoint = this.project(toLatLng(latlng))._round();
1003 return projectedPoint._subtract(this.getPixelOrigin());
1004 },
1005
1006 // @method wrapLatLng(latlng: LatLng): LatLng
1007 // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the
1008 // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the
1009 // CRS's bounds.
1010 // By default this means longitude is wrapped around the dateline so its
1011 // value is between -180 and +180 degrees.
1012 wrapLatLng: function (latlng) {
1013 return this.options.crs.wrapLatLng(toLatLng(latlng));
1014 },
1015
1016 // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
1017 // Returns a `LatLngBounds` with the same size as the given one, ensuring that
1018 // its center is within the CRS's bounds.
1019 // By default this means the center longitude is wrapped around the dateline so its
1020 // value is between -180 and +180 degrees, and the majority of the bounds
1021 // overlaps the CRS's bounds.
1022 wrapLatLngBounds: function (latlng) {
1023 return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng));
1024 },
1025
1026 // @method distance(latlng1: LatLng, latlng2: LatLng): Number
1027 // Returns the distance between two geographical coordinates according to
1028 // the map's CRS. By default this measures distance in meters.
1029 distance: function (latlng1, latlng2) {
1030 return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2));
1031 },
1032
1033 // @method containerPointToLayerPoint(point: Point): Point
1034 // Given a pixel coordinate relative to the map container, returns the corresponding
1035 // pixel coordinate relative to the [origin pixel](#map-getpixelorigin).
1036 containerPointToLayerPoint: function (point) { // (Point)
1037 return toPoint(point).subtract(this._getMapPanePos());
1038 },
1039
1040 // @method layerPointToContainerPoint(point: Point): Point
1041 // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
1042 // returns the corresponding pixel coordinate relative to the map container.
1043 layerPointToContainerPoint: function (point) { // (Point)
1044 return toPoint(point).add(this._getMapPanePos());
1045 },
1046
1047 // @method containerPointToLatLng(point: Point): LatLng
1048 // Given a pixel coordinate relative to the map container, returns
1049 // the corresponding geographical coordinate (for the current zoom level).
1050 containerPointToLatLng: function (point) {
1051 var layerPoint = this.containerPointToLayerPoint(toPoint(point));
1052 return this.layerPointToLatLng(layerPoint);
1053 },
1054
1055 // @method latLngToContainerPoint(latlng: LatLng): Point
1056 // Given a geographical coordinate, returns the corresponding pixel coordinate
1057 // relative to the map container.
1058 latLngToContainerPoint: function (latlng) {
1059 return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
1060 },
1061
1062 // @method mouseEventToContainerPoint(ev: MouseEvent): Point
1063 // Given a MouseEvent object, returns the pixel coordinate relative to the
1064 // map container where the event took place.
1065 mouseEventToContainerPoint: function (e) {
1066 return DomEvent.getMousePosition(e, this._container);
1067 },
1068
1069 // @method mouseEventToLayerPoint(ev: MouseEvent): Point
1070 // Given a MouseEvent object, returns the pixel coordinate relative to
1071 // the [origin pixel](#map-getpixelorigin) where the event took place.
1072 mouseEventToLayerPoint: function (e) {
1073 return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
1074 },
1075
1076 // @method mouseEventToLatLng(ev: MouseEvent): LatLng
1077 // Given a MouseEvent object, returns geographical coordinate where the
1078 // event took place.
1079 mouseEventToLatLng: function (e) { // (MouseEvent)
1080 return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
1081 },
1082
1083
1084 // map initialization methods
1085
1086 _initContainer: function (id) {
1087 var container = this._container = DomUtil.get(id);
1088
1089 if (!container) {
1090 throw new Error('Map container not found.');
1091 } else if (container._leaflet_id) {
1092 throw new Error('Map container is already initialized.');
1093 }
1094
1095 DomEvent.on(container, 'scroll', this._onScroll, this);
1096 this._containerId = Util.stamp(container);
1097 },
1098
1099 _initLayout: function () {
1100 var container = this._container;
1101
1102 this._fadeAnimated = this.options.fadeAnimation && Browser.any3d;
1103
1104 DomUtil.addClass(container, 'leaflet-container' +
1105 (Browser.touch ? ' leaflet-touch' : '') +
1106 (Browser.retina ? ' leaflet-retina' : '') +
1107 (Browser.ielt9 ? ' leaflet-oldie' : '') +
1108 (Browser.safari ? ' leaflet-safari' : '') +
1109 (this._fadeAnimated ? ' leaflet-fade-anim' : ''));
1110
1111 var position = DomUtil.getStyle(container, 'position');
1112
1113 if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
1114 container.style.position = 'relative';
1115 }
1116
1117 this._initPanes();
1118
1119 if (this._initControlPos) {
1120 this._initControlPos();
1121 }
1122 },
1123
1124 _initPanes: function () {
1125 var panes = this._panes = {};
1126 this._paneRenderers = {};
1127
1128 // @section
1129 //
1130 // Panes are DOM elements used to control the ordering of layers on the map. You
1131 // can access panes with [`map.getPane`](#map-getpane) or
1132 // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the
1133 // [`map.createPane`](#map-createpane) method.
1134 //
1135 // Every map has the following default panes that differ only in zIndex.
1136 //
1137 // @pane mapPane: HTMLElement = 'auto'
1138 // Pane that contains all other map panes
1139
1140 this._mapPane = this.createPane('mapPane', this._container);
1141 DomUtil.setPosition(this._mapPane, new Point(0, 0));
1142
1143 // @pane tilePane: HTMLElement = 200
1144 // Pane for `GridLayer`s and `TileLayer`s
1145 this.createPane('tilePane');
1146 // @pane overlayPane: HTMLElement = 400
1147 // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s
1148 this.createPane('overlayPane');
1149 // @pane shadowPane: HTMLElement = 500
1150 // Pane for overlay shadows (e.g. `Marker` shadows)
1151 this.createPane('shadowPane');
1152 // @pane markerPane: HTMLElement = 600
1153 // Pane for `Icon`s of `Marker`s
1154 this.createPane('markerPane');
1155 // @pane tooltipPane: HTMLElement = 650
1156 // Pane for `Tooltip`s.
1157 this.createPane('tooltipPane');
1158 // @pane popupPane: HTMLElement = 700
1159 // Pane for `Popup`s.
1160 this.createPane('popupPane');
1161
1162 if (!this.options.markerZoomAnimation) {
1163 DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide');
1164 DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide');
1165 }
1166 },
1167
1168
1169 // private methods that modify map state
1170
1171 // @section Map state change events
1172 _resetView: function (center, zoom) {
1173 DomUtil.setPosition(this._mapPane, new Point(0, 0));
1174
1175 var loading = !this._loaded;
1176 this._loaded = true;
1177 zoom = this._limitZoom(zoom);
1178
1179 this.fire('viewprereset');
1180
1181 var zoomChanged = this._zoom !== zoom;
1182 this
1183 ._moveStart(zoomChanged, false)
1184 ._move(center, zoom)
1185 ._moveEnd(zoomChanged);
1186
1187 // @event viewreset: Event
1188 // Fired when the map needs to redraw its content (this usually happens
1189 // on map zoom or load). Very useful for creating custom overlays.
1190 this.fire('viewreset');
1191
1192 // @event load: Event
1193 // Fired when the map is initialized (when its center and zoom are set
1194 // for the first time).
1195 if (loading) {
1196 this.fire('load');
1197 }
1198 },
1199
1200 _moveStart: function (zoomChanged, noMoveStart) {
1201 // @event zoomstart: Event
1202 // Fired when the map zoom is about to change (e.g. before zoom animation).
1203 // @event movestart: Event
1204 // Fired when the view of the map starts changing (e.g. user starts dragging the map).
1205 if (zoomChanged) {
1206 this.fire('zoomstart');
1207 }
1208 if (!noMoveStart) {
1209 this.fire('movestart');
1210 }
1211 return this;
1212 },
1213
1214 _move: function (center, zoom, data, supressEvent) {
1215 if (zoom === undefined) {
1216 zoom = this._zoom;
1217 }
1218 var zoomChanged = this._zoom !== zoom;
1219
1220 this._zoom = zoom;
1221 this._lastCenter = center;
1222 this._pixelOrigin = this._getNewPixelOrigin(center);
1223
1224 if (!supressEvent) {
1225 // @event zoom: Event
1226 // Fired repeatedly during any change in zoom level,
1227 // including zoom and fly animations.
1228 if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530
1229 this.fire('zoom', data);
1230 }
1231
1232 // @event move: Event
1233 // Fired repeatedly during any movement of the map,
1234 // including pan and fly animations.
1235 this.fire('move', data);
1236 } else if (data && data.pinch) { // Always fire 'zoom' if pinching because #3530
1237 this.fire('zoom', data);
1238 }
1239 return this;
1240 },
1241
1242 _moveEnd: function (zoomChanged) {
1243 // @event zoomend: Event
1244 // Fired when the map zoom changed, after any animations.
1245 if (zoomChanged) {
1246 this.fire('zoomend');
1247 }
1248
1249 // @event moveend: Event
1250 // Fired when the center of the map stops changing
1251 // (e.g. user stopped dragging the map or after non-centered zoom).
1252 return this.fire('moveend');
1253 },
1254
1255 _stop: function () {
1256 Util.cancelAnimFrame(this._flyToFrame);
1257 if (this._panAnim) {
1258 this._panAnim.stop();
1259 }
1260 return this;
1261 },
1262
1263 _rawPanBy: function (offset) {
1264 DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
1265 },
1266
1267 _getZoomSpan: function () {
1268 return this.getMaxZoom() - this.getMinZoom();
1269 },
1270
1271 _panInsideMaxBounds: function () {
1272 if (!this._enforcingBounds) {
1273 this.panInsideBounds(this.options.maxBounds);
1274 }
1275 },
1276
1277 _checkIfLoaded: function () {
1278 if (!this._loaded) {
1279 throw new Error('Set map center and zoom first.');
1280 }
1281 },
1282
1283 // DOM event handling
1284
1285 // @section Interaction events
1286 _initEvents: function (remove) {
1287 this._targets = {};
1288 this._targets[Util.stamp(this._container)] = this;
1289
1290 var onOff = remove ? DomEvent.off : DomEvent.on;
1291
1292 // @event click: MouseEvent
1293 // Fired when the user clicks (or taps) the map.
1294 // @event dblclick: MouseEvent
1295 // Fired when the user double-clicks (or double-taps) the map.
1296 // @event mousedown: MouseEvent
1297 // Fired when the user pushes the mouse button on the map.
1298 // @event mouseup: MouseEvent
1299 // Fired when the user releases the mouse button on the map.
1300 // @event mouseover: MouseEvent
1301 // Fired when the mouse enters the map.
1302 // @event mouseout: MouseEvent
1303 // Fired when the mouse leaves the map.
1304 // @event mousemove: MouseEvent
1305 // Fired while the mouse moves over the map.
1306 // @event contextmenu: MouseEvent
1307 // Fired when the user pushes the right mouse button on the map, prevents
1308 // default browser context menu from showing if there are listeners on
1309 // this event. Also fired on mobile when the user holds a single touch
1310 // for a second (also called long press).
1311 // @event keypress: KeyboardEvent
1312 // Fired when the user presses a key from the keyboard that produces a character value while the map is focused.
1313 // @event keydown: KeyboardEvent
1314 // Fired when the user presses a key from the keyboard while the map is focused. Unlike the `keypress` event,
1315 // the `keydown` event is fired for keys that produce a character value and for keys
1316 // that do not produce a character value.
1317 // @event keyup: KeyboardEvent
1318 // Fired when the user releases a key from the keyboard while the map is focused.
1319 onOff(this._container, 'click dblclick mousedown mouseup ' +
1320 'mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this);
1321
1322 if (this.options.trackResize) {
1323 onOff(window, 'resize', this._onResize, this);
1324 }
1325
1326 if (Browser.any3d && this.options.transform3DLimit) {
1327 (remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
1328 }
1329 },
1330
1331 _onResize: function () {
1332 Util.cancelAnimFrame(this._resizeRequest);
1333 this._resizeRequest = Util.requestAnimFrame(
1334 function () { this.invalidateSize({debounceMoveend: true}); }, this);
1335 },
1336
1337 _onScroll: function () {
1338 this._container.scrollTop = 0;
1339 this._container.scrollLeft = 0;
1340 },
1341
1342 _onMoveEnd: function () {
1343 var pos = this._getMapPanePos();
1344 if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
1345 // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
1346 // a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/
1347 this._resetView(this.getCenter(), this.getZoom());
1348 }
1349 },
1350
1351 _findEventTargets: function (e, type) {
1352 var targets = [],
1353 target,
1354 isHover = type === 'mouseout' || type === 'mouseover',
1355 src = e.target || e.srcElement,
1356 dragging = false;
1357
1358 while (src) {
1359 target = this._targets[Util.stamp(src)];
1360 if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) {
1361 // Prevent firing click after you just dragged an object.
1362 dragging = true;
1363 break;
1364 }
1365 if (target && target.listens(type, true)) {
1366 if (isHover && !DomEvent.isExternalTarget(src, e)) { break; }
1367 targets.push(target);
1368 if (isHover) { break; }
1369 }
1370 if (src === this._container) { break; }
1371 src = src.parentNode;
1372 }
1373 if (!targets.length && !dragging && !isHover && this.listens(type, true)) {
1374 targets = [this];
1375 }
1376 return targets;
1377 },
1378
1379 _isClickDisabled: function (el) {
1380 while (el !== this._container) {
1381 if (el['_leaflet_disable_click']) { return true; }
1382 el = el.parentNode;
1383 }
1384 },
1385
1386 _handleDOMEvent: function (e) {
1387 var el = (e.target || e.srcElement);
1388 if (!this._loaded || el['_leaflet_disable_events'] || e.type === 'click' && this._isClickDisabled(el)) {
1389 return;
1390 }
1391
1392 var type = e.type;
1393
1394 if (type === 'mousedown') {
1395 // prevents outline when clicking on keyboard-focusable element
1396 DomUtil.preventOutline(el);
1397 }
1398
1399 this._fireDOMEvent(e, type);
1400 },
1401
1402 _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],
1403
1404 _fireDOMEvent: function (e, type, canvasTargets) {
1405
1406 if (e.type === 'click') {
1407 // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
1408 // @event preclick: MouseEvent
1409 // Fired before mouse click on the map (sometimes useful when you
1410 // want something to happen on click before any existing click
1411 // handlers start running).
1412 var synth = Util.extend({}, e);
1413 synth.type = 'preclick';
1414 this._fireDOMEvent(synth, synth.type, canvasTargets);
1415 }
1416
1417 // Find the layer the event is propagating from and its parents.
1418 var targets = this._findEventTargets(e, type);
1419
1420 if (canvasTargets) {
1421 var filtered = []; // pick only targets with listeners
1422 for (var i = 0; i < canvasTargets.length; i++) {
1423 if (canvasTargets[i].listens(type, true)) {
1424 filtered.push(canvasTargets[i]);
1425 }
1426 }
1427 targets = filtered.concat(targets);
1428 }
1429
1430 if (!targets.length) { return; }
1431
1432 if (type === 'contextmenu') {
1433 DomEvent.preventDefault(e);
1434 }
1435
1436 var target = targets[0];
1437 var data = {
1438 originalEvent: e
1439 };
1440
1441 if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') {
1442 var isMarker = target.getLatLng && (!target._radius || target._radius <= 10);
1443 data.containerPoint = isMarker ?
1444 this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
1445 data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
1446 data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
1447 }
1448
1449 for (i = 0; i < targets.length; i++) {
1450 targets[i].fire(type, data, true);
1451 if (data.originalEvent._stopped ||
1452 (targets[i].options.bubblingMouseEvents === false && Util.indexOf(this._mouseEvents, type) !== -1)) { return; }
1453 }
1454 },
1455
1456 _draggableMoved: function (obj) {
1457 obj = obj.dragging && obj.dragging.enabled() ? obj : this;
1458 return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
1459 },
1460
1461 _clearHandlers: function () {
1462 for (var i = 0, len = this._handlers.length; i < len; i++) {
1463 this._handlers[i].disable();
1464 }
1465 },
1466
1467 // @section Other Methods
1468
1469 // @method whenReady(fn: Function, context?: Object): this
1470 // Runs the given function `fn` when the map gets initialized with
1471 // a view (center and zoom) and at least one layer, or immediately
1472 // if it's already initialized, optionally passing a function context.
1473 whenReady: function (callback, context) {
1474 if (this._loaded) {
1475 callback.call(context || this, {target: this});
1476 } else {
1477 this.on('load', callback, context);
1478 }
1479 return this;
1480 },
1481
1482
1483 // private methods for getting map state
1484
1485 _getMapPanePos: function () {
1486 return DomUtil.getPosition(this._mapPane) || new Point(0, 0);
1487 },
1488
1489 _moved: function () {
1490 var pos = this._getMapPanePos();
1491 return pos && !pos.equals([0, 0]);
1492 },
1493
1494 _getTopLeftPoint: function (center, zoom) {
1495 var pixelOrigin = center && zoom !== undefined ?
1496 this._getNewPixelOrigin(center, zoom) :
1497 this.getPixelOrigin();
1498 return pixelOrigin.subtract(this._getMapPanePos());
1499 },
1500
1501 _getNewPixelOrigin: function (center, zoom) {
1502 var viewHalf = this.getSize()._divideBy(2);
1503 return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round();
1504 },
1505
1506 _latLngToNewLayerPoint: function (latlng, zoom, center) {
1507 var topLeft = this._getNewPixelOrigin(center, zoom);
1508 return this.project(latlng, zoom)._subtract(topLeft);
1509 },
1510
1511 _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
1512 var topLeft = this._getNewPixelOrigin(center, zoom);
1513 return toBounds([
1514 this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
1515 this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
1516 this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
1517 this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)
1518 ]);
1519 },
1520
1521 // layer point of the current center
1522 _getCenterLayerPoint: function () {
1523 return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
1524 },
1525
1526 // offset of the specified place to the current center in pixels
1527 _getCenterOffset: function (latlng) {
1528 return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
1529 },
1530
1531 // adjust center for view to get inside bounds
1532 _limitCenter: function (center, zoom, bounds) {
1533
1534 if (!bounds) { return center; }
1535
1536 var centerPoint = this.project(center, zoom),
1537 viewHalf = this.getSize().divideBy(2),
1538 viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
1539 offset = this._getBoundsOffset(viewBounds, bounds, zoom);
1540
1541 // If offset is less than a pixel, ignore.
1542 // This prevents unstable projections from getting into
1543 // an infinite loop of tiny offsets.
1544 if (offset.round().equals([0, 0])) {
1545 return center;
1546 }
1547
1548 return this.unproject(centerPoint.add(offset), zoom);
1549 },
1550
1551 // adjust offset for view to get inside bounds
1552 _limitOffset: function (offset, bounds) {
1553 if (!bounds) { return offset; }
1554
1555 var viewBounds = this.getPixelBounds(),
1556 newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
1557
1558 return offset.add(this._getBoundsOffset(newBounds, bounds));
1559 },
1560
1561 // returns offset needed for pxBounds to get inside maxBounds at a specified zoom
1562 _getBoundsOffset: function (pxBounds, maxBounds, zoom) {
1563 var projectedMaxBounds = toBounds(
1564 this.project(maxBounds.getNorthEast(), zoom),
1565 this.project(maxBounds.getSouthWest(), zoom)
1566 ),
1567 minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
1568 maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),
1569
1570 dx = this._rebound(minOffset.x, -maxOffset.x),
1571 dy = this._rebound(minOffset.y, -maxOffset.y);
1572
1573 return new Point(dx, dy);
1574 },
1575
1576 _rebound: function (left, right) {
1577 return left + right > 0 ?
1578 Math.round(left - right) / 2 :
1579 Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
1580 },
1581
1582 _limitZoom: function (zoom) {
1583 var min = this.getMinZoom(),
1584 max = this.getMaxZoom(),
1585 snap = Browser.any3d ? this.options.zoomSnap : 1;
1586 if (snap) {
1587 zoom = Math.round(zoom / snap) * snap;
1588 }
1589 return Math.max(min, Math.min(max, zoom));
1590 },
1591
1592 _onPanTransitionStep: function () {
1593 this.fire('move');
1594 },
1595
1596 _onPanTransitionEnd: function () {
1597 DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
1598 this.fire('moveend');
1599 },
1600
1601 _tryAnimatedPan: function (center, options) {
1602 // difference between the new and current centers in pixels
1603 var offset = this._getCenterOffset(center)._trunc();
1604
1605 // don't animate too far unless animate: true specified in options
1606 if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
1607
1608 this.panBy(offset, options);
1609
1610 return true;
1611 },
1612
1613 _createAnimProxy: function () {
1614
1615 var proxy = this._proxy = DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated');
1616 this._panes.mapPane.appendChild(proxy);
1617
1618 this.on('zoomanim', function (e) {
1619 var prop = DomUtil.TRANSFORM,
1620 transform = this._proxy.style[prop];
1621
1622 DomUtil.setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));
1623
1624 // workaround for case when transform is the same and so transitionend event is not fired
1625 if (transform === this._proxy.style[prop] && this._animatingZoom) {
1626 this._onZoomTransitionEnd();
1627 }
1628 }, this);
1629
1630 this.on('load moveend', this._animMoveEnd, this);
1631
1632 this._on('unload', this._destroyAnimProxy, this);
1633 },
1634
1635 _destroyAnimProxy: function () {
1636 DomUtil.remove(this._proxy);
1637 this.off('load moveend', this._animMoveEnd, this);
1638 delete this._proxy;
1639 },
1640
1641 _animMoveEnd: function () {
1642 var c = this.getCenter(),
1643 z = this.getZoom();
1644 DomUtil.setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1));
1645 },
1646
1647 _catchTransitionEnd: function (e) {
1648 if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
1649 this._onZoomTransitionEnd();
1650 }
1651 },
1652
1653 _nothingToAnimate: function () {
1654 return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
1655 },
1656
1657 _tryAnimatedZoom: function (center, zoom, options) {
1658
1659 if (this._animatingZoom) { return true; }
1660
1661 options = options || {};
1662
1663 // don't animate if disabled, not supported or zoom difference is too large
1664 if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
1665 Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
1666
1667 // offset is the pixel coords of the zoom origin relative to the current center
1668 var scale = this.getZoomScale(zoom),
1669 offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);
1670
1671 // don't animate if the zoom origin isn't within one screen from the current center, unless forced
1672 if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
1673
1674 Util.requestAnimFrame(function () {
1675 this
1676 ._moveStart(true, false)
1677 ._animateZoom(center, zoom, true);
1678 }, this);
1679
1680 return true;
1681 },
1682
1683 _animateZoom: function (center, zoom, startAnim, noUpdate) {
1684 if (!this._mapPane) { return; }
1685
1686 if (startAnim) {
1687 this._animatingZoom = true;
1688
1689 // remember what center/zoom to set after animation
1690 this._animateToCenter = center;
1691 this._animateToZoom = zoom;
1692
1693 DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
1694 }
1695
1696 // @section Other Events
1697 // @event zoomanim: ZoomAnimEvent
1698 // Fired at least once per zoom animation. For continuous zoom, like pinch zooming, fired once per frame during zoom.
1699 this.fire('zoomanim', {
1700 center: center,
1701 zoom: zoom,
1702 noUpdate: noUpdate
1703 });
1704
1705 if (!this._tempFireZoomEvent) {
1706 this._tempFireZoomEvent = this._zoom !== this._animateToZoom;
1707 }
1708
1709 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
1710
1711 // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
1712 setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250);
1713 },
1714
1715 _onZoomTransitionEnd: function () {
1716 if (!this._animatingZoom) { return; }
1717
1718 if (this._mapPane) {
1719 DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
1720 }
1721
1722 this._animatingZoom = false;
1723
1724 this._move(this._animateToCenter, this._animateToZoom, undefined, true);
1725
1726 if (this._tempFireZoomEvent) {
1727 this.fire('zoom');
1728 }
1729 delete this._tempFireZoomEvent;
1730
1731 this.fire('move');
1732
1733 this._moveEnd(true);
1734 }
1735});
1736
1737// @section
1738
1739// @factory L.map(id: String, options?: Map options)
1740// Instantiates a map object given the DOM ID of a `<div>` element
1741// and optionally an object literal with `Map options`.
1742//
1743// @alternative
1744// @factory L.map(el: HTMLElement, options?: Map options)
1745// Instantiates a map object given an instance of a `<div>` HTML element
1746// and optionally an object literal with `Map options`.
1747export function createMap(id, options) {
1748 return new Map(id, options);
1749}