OpenSourcePolitics / decidim-ditp

Decidim instance for DITP OGP
1 stars 2 forks source link

replace with universal implementation after refactoring projections #36

Open github-actions[bot] opened 12 months ago

github-actions[bot] commented 12 months ago

Returns the zoom level that the map would end up at, if it is at fromZoom

level and everything is scaled by a factor of scale. Inverse of

getZoomScale.

Projects a geographical coordinate LatLng according to the projection

of the map's CRS, then scales it according to zoom and the CRS's

Transformation. The result is pixel coordinate relative to

the CRS origin.

Inverse of project.

Given a pixel coordinate relative to the origin pixel,

returns the corresponding geographical coordinate (for the current zoom level).

Given a geographical coordinate, returns the corresponding pixel coordinate

relative to the origin pixel.

Returns a LatLng where lat and lng has been wrapped according to the

map's CRS's wrapLat and wrapLng properties, if they are outside the

CRS's bounds.

By default this means longitude is wrapped around the dateline so its

value is between -180 and +180 degrees.

Returns a LatLngBounds with the same size as the given one, ensuring that

its center is within the CRS's bounds.

By default this means the center longitude is wrapped around the dateline so its

value is between -180 and +180 degrees, and the majority of the bounds

overlaps the CRS's bounds.

Returns the distance between two geographical coordinates according to

the map's CRS. By default this measures distance in meters.

Given a pixel coordinate relative to the map container, returns the corresponding

pixel coordinate relative to the origin pixel.

containerPointToLayerPoint: function (point) { // (Point)

Given a pixel coordinate relative to the origin pixel,

returns the corresponding pixel coordinate relative to the map container.

layerPointToContainerPoint: function (point) { // (Point)

Given a pixel coordinate relative to the map container, returns

the corresponding geographical coordinate (for the current zoom level).

Given a geographical coordinate, returns the corresponding pixel coordinate

relative to the map container.

Given a MouseEvent object, returns the pixel coordinate relative to the

map container where the event took place.

Given a MouseEvent object, returns the pixel coordinate relative to

the origin pixel where the event took place.

Given a MouseEvent object, returns geographical coordinate where the

event took place.

mouseEventToLatLng: function (e) { // (MouseEvent)

Panes are DOM elements used to control the ordering of layers on the map. You

can access panes with map.getPane or

map.getPanes methods. New panes can be created with the

map.createPane method.

Every map has the following default panes that differ only in zIndex.

@pane mapPane: HTMLElement = 'auto'

Pane that contains all other map panes

Pane for GridLayers and TileLayers

Pane for vectors (Paths, like Polylines and Polygons), ImageOverlays and VideoOverlays

Pane for overlay shadows (e.g. Marker shadows)

Pane for Icons of Markers

Pane for Tooltips.

Pane for Popups.

Fired when the map needs to redraw its content (this usually happens

on map zoom or load). Very useful for creating custom overlays.

Fired when the map is initialized (when its center and zoom are set

for the first time).

Fired when the map zoom is about to change (e.g. before zoom animation).

@event movestart: Event

Fired when the view of the map starts changing (e.g. user starts dragging the map).

Fired repeatedly during any change in zoom level, including zoom

and fly animations.

if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530

Fired repeatedly during any movement of the map, including pan and

fly animations.

Fired when the map has changed, after any animations.

Fired when the center of the map stops changing (e.g. user stopped

dragging the map).

Fired when the user clicks (or taps) the map.

@event dblclick: MouseEvent

Fired when the user double-clicks (or double-taps) the map.

@event mousedown: MouseEvent

Fired when the user pushes the mouse button on the map.

@event mouseup: MouseEvent

Fired when the user releases the mouse button on the map.

@event mouseover: MouseEvent

Fired when the mouse enters the map.

@event mouseout: MouseEvent

Fired when the mouse leaves the map.

@event mousemove: MouseEvent

Fired while the mouse moves over the map.

@event contextmenu: MouseEvent

Fired when the user pushes the right mouse button on the map, prevents

default browser context menu from showing if there are listeners on

this event. Also fired on mobile when the user holds a single touch

for a second (also called long press).

@event keypress: KeyboardEvent

Fired when the user presses a key from the keyboard while the map is focused.

a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/

@event preclick: MouseEvent

Fired before mouse click on the map (sometimes useful when you

want something to happen on click before any existing click

handlers start running).

Runs the given function fn when the map gets initialized with

a view (center and zoom) and at least one layer, or immediately

if it's already initialized, optionally passing a function context.

This prevents unstable projections from getting into

an infinite loop of tiny offsets.

Fired on every frame of a zoom animation

Instantiates a map object given the DOM ID of a <div> element

and optionally an object literal with Map options.

@alternative

@factory L.map(el: HTMLElement, options?: Map options)

Instantiates a map object given an instance of a <div> HTML element

and optionally an object literal with Map options.

https://api.github.com/OpenSourcePolitics/decidim-ditp/blob/cf68cc605e2b45b5d16865ebf4eadd87b89a898f/packages/core/node_modules/leaflet/src/map/Map.js#L917


import * as Util from '../core/Util';
import {Evented} from '../core/Events';
import {EPSG3857} from '../geo/crs/CRS.EPSG3857';
import {Point, toPoint} from '../geometry/Point';
import {Bounds, toBounds} from '../geometry/Bounds';
import {LatLng, toLatLng} from '../geo/LatLng';
import {LatLngBounds, toLatLngBounds} from '../geo/LatLngBounds';
import * as Browser from '../core/Browser';
import * as DomEvent from '../dom/DomEvent';
import * as DomUtil from '../dom/DomUtil';
import {PosAnimation} from '../dom/PosAnimation';

/*
 * @class Map
 * @aka L.Map
 * @inherits Evented
 *
 * The central class of the API — it is used to create a map on a page and manipulate it.
 *
 * @example
 *
 * ```js
 * // initialize the map on the "map" div with a given center and zoom
 * var map = L.map('map', {
 *  center: [51.505, -0.09],
 *  zoom: 13
 * });
 * ```
 *
 */

export var Map = Evented.extend({

    options: {
        // @section Map State Options
        // @option crs: CRS = L.CRS.EPSG3857
        // The [Coordinate Reference System](#crs) to use. Don't change this if you're not
        // sure what it means.
        crs: EPSG3857,

        // @option center: LatLng = undefined
        // Initial geographic center of the map
        center: undefined,

        // @option zoom: Number = undefined
        // Initial map zoom level
        zoom: undefined,

        // @option minZoom: Number = *
        // Minimum zoom level of the map.
        // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
        // the lowest of their `minZoom` options will be used instead.
        minZoom: undefined,

        // @option maxZoom: Number = *
        // Maximum zoom level of the map.
        // If not specified and at least one `GridLayer` or `TileLayer` is in the map,
        // the highest of their `maxZoom` options will be used instead.
        maxZoom: undefined,

        // @option layers: Layer[] = []
        // Array of layers that will be added to the map initially
        layers: [],

        // @option maxBounds: LatLngBounds = null
        // When this option is set, the map restricts the view to the given
        // geographical bounds, bouncing the user back if the user tries to pan
        // outside the view. To set the restriction dynamically, use
        // [`setMaxBounds`](#map-setmaxbounds) method.
        maxBounds: undefined,

        // @option renderer: Renderer = *
        // The default method for drawing vector layers on the map. `L.SVG`
        // or `L.Canvas` by default depending on browser support.
        renderer: undefined,

        // @section Animation Options
        // @option zoomAnimation: Boolean = true
        // Whether the map zoom animation is enabled. By default it's enabled
        // in all browsers that support CSS3 Transitions except Android.
        zoomAnimation: true,

        // @option zoomAnimationThreshold: Number = 4
        // Won't animate zoom if the zoom difference exceeds this value.
        zoomAnimationThreshold: 4,

        // @option fadeAnimation: Boolean = true
        // Whether the tile fade animation is enabled. By default it's enabled
        // in all browsers that support CSS3 Transitions except Android.
        fadeAnimation: true,

        // @option markerZoomAnimation: Boolean = true
        // Whether markers animate their zoom with the zoom animation, if disabled
        // they will disappear for the length of the animation. By default it's
        // enabled in all browsers that support CSS3 Transitions except Android.
        markerZoomAnimation: true,

        // @option transform3DLimit: Number = 2^23
        // Defines the maximum size of a CSS translation transform. The default
        // value should not be changed unless a web browser positions layers in
        // the wrong place after doing a large `panBy`.
        transform3DLimit: 8388608, // Precision limit of a 32-bit float

        // @section Interaction Options
        // @option zoomSnap: Number = 1
        // Forces the map's zoom level to always be a multiple of this, particularly
        // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom.
        // By default, the zoom level snaps to the nearest integer; lower values
        // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0`
        // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom.
        zoomSnap: 1,

        // @option zoomDelta: Number = 1
        // Controls how much the map's zoom level will change after a
        // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+`
        // or `-` on the keyboard, or using the [zoom controls](#control-zoom).
        // Values smaller than `1` (e.g. `0.5`) allow for greater granularity.
        zoomDelta: 1,

        // @option trackResize: Boolean = true
        // Whether the map automatically handles browser window resize to update itself.
        trackResize: true
    },

    initialize: function (id, options) { // (HTMLElement or String, Object)
        options = Util.setOptions(this, options);

        this._initContainer(id);
        this._initLayout();

        // hack for https://github.com/Leaflet/Leaflet/issues/1980
        this._onResize = Util.bind(this._onResize, this);

        this._initEvents();

        if (options.maxBounds) {
            this.setMaxBounds(options.maxBounds);
        }

        if (options.zoom !== undefined) {
            this._zoom = this._limitZoom(options.zoom);
        }

        if (options.center && options.zoom !== undefined) {
            this.setView(toLatLng(options.center), options.zoom, {reset: true});
        }

        this._handlers = [];
        this._layers = {};
        this._zoomBoundLayers = {};
        this._sizeChanged = true;

        this.callInitHooks();

        // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
        this._zoomAnimated = DomUtil.TRANSITION && Browser.any3d && !Browser.mobileOpera &&
                this.options.zoomAnimation;

        // zoom transitions run with the same duration for all layers, so if one of transitionend events
        // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
        if (this._zoomAnimated) {
            this._createAnimProxy();
            DomEvent.on(this._proxy, DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
        }

        this._addLayers(this.options.layers);
    },

    // @section Methods for modifying map state

    // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
    // Sets the view of the map (geographical center and zoom) with the given
    // animation options.
    setView: function (center, zoom, options) {

        zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
        center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds);
        options = options || {};

        this._stop();

        if (this._loaded && !options.reset && options !== true) {

            if (options.animate !== undefined) {
                options.zoom = Util.extend({animate: options.animate}, options.zoom);
                options.pan = Util.extend({animate: options.animate, duration: options.duration}, options.pan);
            }

            // try animating pan or zoom
            var moved = (this._zoom !== zoom) ?
                this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
                this._tryAnimatedPan(center, options.pan);

            if (moved) {
                // prevent resize handler call, the view will refresh after animation anyway
                clearTimeout(this._sizeTimer);
                return this;
            }
        }

        // animation didn't start, just reset the map view
        this._resetView(center, zoom);

        return this;
    },

    // @method setZoom(zoom: Number, options?: Zoom/pan options): this
    // Sets the zoom of the map.
    setZoom: function (zoom, options) {
        if (!this._loaded) {
            this._zoom = zoom;
            return this;
        }
        return this.setView(this.getCenter(), zoom, {zoom: options});
    },

    // @method zoomIn(delta?: Number, options?: Zoom options): this
    // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
    zoomIn: function (delta, options) {
        delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
        return this.setZoom(this._zoom + delta, options);
    },

    // @method zoomOut(delta?: Number, options?: Zoom options): this
    // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
    zoomOut: function (delta, options) {
        delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
        return this.setZoom(this._zoom - delta, options);
    },

    // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this
    // Zooms the map while keeping a specified geographical point on the map
    // stationary (e.g. used internally for scroll zoom and double-click zoom).
    // @alternative
    // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this
    // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary.
    setZoomAround: function (latlng, zoom, options) {
        var scale = this.getZoomScale(zoom),
            viewHalf = this.getSize().divideBy(2),
            containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng),

            centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
            newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));

        return this.setView(newCenter, zoom, {zoom: options});
    },

    _getBoundsCenterZoom: function (bounds, options) {

        options = options || {};
        bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds);

        var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
            paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),

            zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));

        zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom;

        if (zoom === Infinity) {
            return {
                center: bounds.getCenter(),
                zoom: zoom
            };
        }

        var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),

            swPoint = this.project(bounds.getSouthWest(), zoom),
            nePoint = this.project(bounds.getNorthEast(), zoom),
            center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);

        return {
            center: center,
            zoom: zoom
        };
    },

    // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this
    // Sets a map view that contains the given geographical bounds with the
    // maximum zoom level possible.
    fitBounds: function (bounds, options) {

        bounds = toLatLngBounds(bounds);

        if (!bounds.isValid()) {
            throw new Error('Bounds are not valid.');
        }

        var target = this._getBoundsCenterZoom(bounds, options);
        return this.setView(target.center, target.zoom, options);
    },

    // @method fitWorld(options?: fitBounds options): this
    // Sets a map view that mostly contains the whole world with the maximum
    // zoom level possible.
    fitWorld: function (options) {
        return this.fitBounds([[-90, -180], [90, 180]], options);
    },

    // @method panTo(latlng: LatLng, options?: Pan options): this
    // Pans the map to a given center.
    panTo: function (center, options) { // (LatLng)
        return this.setView(center, this._zoom, {pan: options});
    },

    // @method panBy(offset: Point, options?: Pan options): this
    // Pans the map by a given number of pixels (animated).
    panBy: function (offset, options) {
        offset = toPoint(offset).round();
        options = options || {};

        if (!offset.x && !offset.y) {
            return this.fire('moveend');
        }
        // If we pan too far, Chrome gets issues with tiles
        // and makes them disappear or appear in the wrong place (slightly offset) #2602
        if (options.animate !== true && !this.getSize().contains(offset)) {
            this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
            return this;
        }

        if (!this._panAnim) {
            this._panAnim = new PosAnimation();

            this._panAnim.on({
                'step': this._onPanTransitionStep,
                'end': this._onPanTransitionEnd
            }, this);
        }

        // don't fire movestart if animating inertia
        if (!options.noMoveStart) {
            this.fire('movestart');
        }

        // animate pan unless animate: false specified
        if (options.animate !== false) {
            DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');

            var newPos = this._getMapPanePos().subtract(offset).round();
            this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
        } else {
            this._rawPanBy(offset);
            this.fire('move').fire('moveend');
        }

        return this;
    },

    // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
    // Sets the view of the map (geographical center and zoom) performing a smooth
    // pan-zoom animation.
    flyTo: function (targetCenter, targetZoom, options) {

        options = options || {};
        if (options.animate === false || !Browser.any3d) {
            return this.setView(targetCenter, targetZoom, options);
        }

        this._stop();

        var from = this.project(this.getCenter()),
            to = this.project(targetCenter),
            size = this.getSize(),
            startZoom = this._zoom;

        targetCenter = toLatLng(targetCenter);
        targetZoom = targetZoom === undefined ? startZoom : targetZoom;

        var w0 = Math.max(size.x, size.y),
            w1 = w0 * this.getZoomScale(startZoom, targetZoom),
            u1 = (to.distanceTo(from)) || 1,
            rho = 1.42,
            rho2 = rho * rho;

        function r(i) {
            var s1 = i ? -1 : 1,
                s2 = i ? w1 : w0,
                t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
                b1 = 2 * s2 * rho2 * u1,
                b = t1 / b1,
                sq = Math.sqrt(b * b + 1) - b;

                // workaround for floating point precision bug when sq = 0, log = -Infinite,
                // thus triggering an infinite loop in flyTo
                var log = sq < 0.000000001 ? -18 : Math.log(sq);

            return log;
        }

        function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
        function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
        function tanh(n) { return sinh(n) / cosh(n); }

        var r0 = r(0);

        function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
        function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }

        function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }

        var start = Date.now(),
            S = (r(1) - r0) / rho,
            duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;

        function frame() {
            var t = (Date.now() - start) / duration,
                s = easeOut(t) * S;

            if (t <= 1) {
                this._flyToFrame = Util.requestAnimFrame(frame, this);

                this._move(
                    this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
                    this.getScaleZoom(w0 / w(s), startZoom),
                    {flyTo: true});

            } else {
                this
                    ._move(targetCenter, targetZoom)
                    ._moveEnd(true);
            }
        }

        this._moveStart(true, options.noMoveStart);

        frame.call(this);
        return this;
    },

    // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
    // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
    // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
    flyToBounds: function (bounds, options) {
        var target = this._getBoundsCenterZoom(bounds, options);
        return this.flyTo(target.center, target.zoom, options);
    },

    // @method setMaxBounds(bounds: Bounds): this
    // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option).
    setMaxBounds: function (bounds) {
        bounds = toLatLngBounds(bounds);

        if (!bounds.isValid()) {
            this.options.maxBounds = null;
            return this.off('moveend', this._panInsideMaxBounds);
        } else if (this.options.maxBounds) {
            this.off('moveend', this._panInsideMaxBounds);
        }

        this.options.maxBounds = bounds;

        if (this._loaded) {
            this._panInsideMaxBounds();
        }

        return this.on('moveend', this._panInsideMaxBounds);
    },

    // @method setMinZoom(zoom: Number): this
    // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option).
    setMinZoom: function (zoom) {
        var oldZoom = this.options.minZoom;
        this.options.minZoom = zoom;

        if (this._loaded && oldZoom !== zoom) {
            this.fire('zoomlevelschange');

            if (this.getZoom() < this.options.minZoom) {
                return this.setZoom(zoom);
            }
        }

        return this;
    },

    // @method setMaxZoom(zoom: Number): this
    // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option).
    setMaxZoom: function (zoom) {
        var oldZoom = this.options.maxZoom;
        this.options.maxZoom = zoom;

        if (this._loaded && oldZoom !== zoom) {
            this.fire('zoomlevelschange');

            if (this.getZoom() > this.options.maxZoom) {
                return this.setZoom(zoom);
            }
        }

        return this;
    },

    // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this
    // 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.
    panInsideBounds: function (bounds, options) {
        this._enforcingBounds = true;
        var center = this.getCenter(),
            newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds));

        if (!center.equals(newCenter)) {
            this.panTo(newCenter, options);
        }

        this._enforcingBounds = false;
        return this;
    },

    // @method invalidateSize(options: Zoom/pan options): this
    // Checks if the map container size changed and updates the map if so —
    // call it after you've changed the map size dynamically, also animating
    // pan by default. If `options.pan` is `false`, panning will not occur.
    // If `options.debounceMoveend` is `true`, it will delay `moveend` event so
    // that it doesn't happen often even if the method is called many
    // times in a row.

    // @alternative
    // @method invalidateSize(animate: Boolean): this
    // Checks if the map container size changed and updates the map if so —
    // call it after you've changed the map size dynamically, also animating
    // pan by default.
    invalidateSize: function (options) {
        if (!this._loaded) { return this; }

        options = Util.extend({
            animate: false,
            pan: true
        }, options === true ? {animate: true} : options);

        var oldSize = this.getSize();
        this._sizeChanged = true;
        this._lastCenter = null;

        var newSize = this.getSize(),
            oldCenter = oldSize.divideBy(2).round(),
            newCenter = newSize.divideBy(2).round(),
            offset = oldCenter.subtract(newCenter);

        if (!offset.x && !offset.y) { return this; }

        if (options.animate && options.pan) {
            this.panBy(offset);

        } else {
            if (options.pan) {
                this._rawPanBy(offset);
            }

            this.fire('move');

            if (options.debounceMoveend) {
                clearTimeout(this._sizeTimer);
                this._sizeTimer = setTimeout(Util.bind(this.fire, this, 'moveend'), 200);
            } else {
                this.fire('moveend');
            }
        }

        // @section Map state change events
        // @event resize: ResizeEvent
        // Fired when the map is resized.
        return this.fire('resize', {
            oldSize: oldSize,
            newSize: newSize
        });
    },

    // @section Methods for modifying map state
    // @method stop(): this
    // Stops the currently running `panTo` or `flyTo` animation, if any.
    stop: function () {
        this.setZoom(this._limitZoom(this._zoom));
        if (!this.options.zoomSnap) {
            this.fire('viewreset');
        }
        return this._stop();
    },

    // @section Geolocation methods
    // @method locate(options?: Locate options): this
    // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
    // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
    // and optionally sets the map view to the user's location with respect to
    // detection accuracy (or to the world view if geolocation failed).
    // Note that, if your page doesn't use HTTPS, this method will fail in
    // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
    // See `Locate options` for more details.
    locate: function (options) {

        options = this._locateOptions = Util.extend({
            timeout: 10000,
            watch: false
            // setView: false
            // maxZoom: <Number>
            // maximumAge: 0
            // enableHighAccuracy: false
        }, options);

        if (!('geolocation' in navigator)) {
            this._handleGeolocationError({
                code: 0,
                message: 'Geolocation not supported.'
            });
            return this;
        }

        var onResponse = Util.bind(this._handleGeolocationResponse, this),
            onError = Util.bind(this._handleGeolocationError, this);

        if (options.watch) {
            this._locationWatchId =
                    navigator.geolocation.watchPosition(onResponse, onError, options);
        } else {
            navigator.geolocation.getCurrentPosition(onResponse, onError, options);
        }
        return this;
    },

    // @method stopLocate(): this
    // Stops watching location previously initiated by `map.locate({watch: true})`
    // and aborts resetting the map view if map.locate was called with
    // `{setView: true}`.
    stopLocate: function () {
        if (navigator.geolocation && navigator.geolocation.clearWatch) {
            navigator.geolocation.clearWatch(this._locationWatchId);
        }
        if (this._locateOptions) {
            this._locateOptions.setView = false;
        }
        return this;
    },

    _handleGeolocationError: function (error) {
        var c = error.code,
            message = error.message ||
                    (c === 1 ? 'permission denied' :
                    (c === 2 ? 'position unavailable' : 'timeout'));

        if (this._locateOptions.setView && !this._loaded) {
            this.fitWorld();
        }

        // @section Location events
        // @event locationerror: ErrorEvent
        // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
        this.fire('locationerror', {
            code: c,
            message: 'Geolocation error: ' + message + '.'
        });
    },

    _handleGeolocationResponse: function (pos) {
        var lat = pos.coords.latitude,
            lng = pos.coords.longitude,
            latlng = new LatLng(lat, lng),
            bounds = latlng.toBounds(pos.coords.accuracy),
            options = this._locateOptions;

        if (options.setView) {
            var zoom = this.getBoundsZoom(bounds);
            this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
        }

        var data = {
            latlng: latlng,
            bounds: bounds,
            timestamp: pos.timestamp
        };

        for (var i in pos.coords) {
            if (typeof pos.coords[i] === 'number') {
                data[i] = pos.coords[i];
            }
        }

        // @event locationfound: LocationEvent
        // Fired when geolocation (using the [`locate`](#map-locate) method)
        // went successfully.
        this.fire('locationfound', data);
    },

    // TODO Appropriate docs section?
    // @section Other Methods
    // @method addHandler(name: String, HandlerClass: Function): this
    // Adds a new `Handler` to the map, given its name and constructor function.
    addHandler: function (name, HandlerClass) {
        if (!HandlerClass) { return this; }

        var handler = this[name] = new HandlerClass(this);

        this._handlers.push(handler);

        if (this.options[name]) {
            handler.enable();
        }

        return this;
    },

    // @method remove(): this
    // Destroys the map and clears all related event listeners.
    remove: function () {

        this._initEvents(true);

        if (this._containerId !== this._container._leaflet_id) {
            throw new Error('Map container is being reused by another instance');
        }

        try {
            // throws error in IE6-8
            delete this._container._leaflet_id;
            delete this._containerId;
        } catch (e) {
            /*eslint-disable */
            this._container._leaflet_id = undefined;
            /* eslint-enable */
            this._containerId = undefined;
        }

        if (this._locationWatchId !== undefined) {
            this.stopLocate();
        }

        this._stop();

        DomUtil.remove(this._mapPane);

        if (this._clearControlPos) {
            this._clearControlPos();
        }

        this._clearHandlers();

        if (this._loaded) {
            // @section Map state change events
            // @event unload: Event
            // Fired when the map is destroyed with [remove](#map-remove) method.
            this.fire('unload');
        }

        var i;
        for (i in this._layers) {
            this._layers[i].remove();
        }
        for (i in this._panes) {
            DomUtil.remove(this._panes[i]);
        }

        this._layers = [];
        this._panes = [];
        delete this._mapPane;
        delete this._renderer;

        return this;
    },

    // @section Other Methods
    // @method createPane(name: String, container?: HTMLElement): HTMLElement
    // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already,
    // then returns it. The pane is created as a child of `container`, or
    // as a child of the main map pane if not set.
    createPane: function (name, container) {
        var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''),
            pane = DomUtil.create('div', className, container || this._mapPane);

        if (name) {
            this._panes[name] = pane;
        }
        return pane;
    },

    // @section Methods for Getting Map State

    // @method getCenter(): LatLng
    // Returns the geographical center of the map view
    getCenter: function () {
        this._checkIfLoaded();

        if (this._lastCenter && !this._moved()) {
            return this._lastCenter;
        }
        return this.layerPointToLatLng(this._getCenterLayerPoint());
    },

    // @method getZoom(): Number
    // Returns the current zoom level of the map view
    getZoom: function () {
        return this._zoom;
    },

    // @method getBounds(): LatLngBounds
    // Returns the geographical bounds visible in the current map view
    getBounds: function () {
        var bounds = this.getPixelBounds(),
            sw = this.unproject(bounds.getBottomLeft()),
            ne = this.unproject(bounds.getTopRight());

        return new LatLngBounds(sw, ne);
    },

    // @method getMinZoom(): Number
    // 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.
    getMinZoom: function () {
        return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom;
    },

    // @method getMaxZoom(): Number
    // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers).
    getMaxZoom: function () {
        return this.options.maxZoom === undefined ?
            (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
            this.options.maxZoom;
    },

    // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean): Number
    // Returns the maximum zoom level on which the given bounds fit to the map
    // view in its entirety. If `inside` (optional) is set to `true`, the method
    // instead returns the minimum zoom level on which the map view fits into
    // the given bounds in its entirety.
    getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
        bounds = toLatLngBounds(bounds);
        padding = toPoint(padding || [0, 0]);

        var zoom = this.getZoom() || 0,
            min = this.getMinZoom(),
            max = this.getMaxZoom(),
            nw = bounds.getNorthWest(),
            se = bounds.getSouthEast(),
            size = this.getSize().subtract(padding),
            boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
            snap = Browser.any3d ? this.options.zoomSnap : 1,
            scalex = size.x / boundsSize.x,
            scaley = size.y / boundsSize.y,
            scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);

        zoom = this.getScaleZoom(scale, zoom);

        if (snap) {
            zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
            zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
        }

        return Math.max(min, Math.min(max, zoom));
    },

    // @method getSize(): Point
    // Returns the current size of the map container (in pixels).
    getSize: function () {
        if (!this._size || this._sizeChanged) {
            this._size = new Point(
                this._container.clientWidth || 0,
                this._container.clientHeight || 0);

            this._sizeChanged = false;
        }
        return this._size.clone();
    },

    // @method getPixelBounds(): Bounds
    // Returns the bounds of the current map view in projected pixel
    // coordinates (sometimes useful in layer and overlay implementations).
    getPixelBounds: function (center, zoom) {
        var topLeftPoint = this._getTopLeftPoint(center, zoom);
        return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
    },

    // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to
    // the map pane? "left point of the map layer" can be confusing, specially
    // since there can be negative offsets.
    // @method getPixelOrigin(): Point
    // Returns the projected pixel coordinates of the top left point of
    // the map layer (useful in custom layer and overlay implementations).
    getPixelOrigin: function () {
        this._checkIfLoaded();
        return this._pixelOrigin;
    },

    // @method getPixelWorldBounds(zoom?: Number): Bounds
    // Returns the world's bounds in pixel coordinates for zoom level `zoom`.
    // If `zoom` is omitted, the map's current zoom level is used.
    getPixelWorldBounds: function (zoom) {
        return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom);
    },

    // @section Other Methods

    // @method getPane(pane: String|HTMLElement): HTMLElement
    // Returns a [map pane](#map-pane), given its name or its HTML element (its identity).
    getPane: function (pane) {
        return typeof pane === 'string' ? this._panes[pane] : pane;
    },

    // @method getPanes(): Object
    // Returns a plain object containing the names of all [panes](#map-pane) as keys and
    // the panes as values.
    getPanes: function () {
        return this._panes;
    },

    // @method getContainer: HTMLElement
    // Returns the HTML element that contains the map.
    getContainer: function () {
        return this._container;
    },

    // @section Conversion Methods

    // @method getZoomScale(toZoom: Number, fromZoom: Number): Number
    // Returns the scale factor to be applied to a map transition from zoom level
    // `fromZoom` to `toZoom`. Used internally to help with zoom animations.
    getZoomScale: function (toZoom, fromZoom) {
        // TODO replace with universal implementation after refactoring projections
        var crs = this.options.crs;
        fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
        return crs.scale(toZoom) / crs.scale(fromZoom);
    },

    // @method getScaleZoom(scale: Number, fromZoom: Number): Number
    // Returns the zoom level that the map would end up at, if it is at `fromZoom`
    // level and everything is scaled by a factor of `scale`. Inverse of
    // [`getZoomScale`](#map-getZoomScale).
    getScaleZoom: function (scale, fromZoom) {
        var crs = this.options.crs;
        fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
        var zoom = crs.zoom(scale * crs.scale(fromZoom));
        return isNaN(zoom) ? Infinity : zoom;
    },

    // @method project(latlng: LatLng, zoom: Number): Point
    // Projects a geographical coordinate `LatLng` according to the projection
    // of the map's CRS, then scales it according to `zoom` and the CRS's
    // `Transformation`. The result is pixel coordinate relative to
    // the CRS origin.
    project: function (latlng, zoom) {
        zoom = zoom === undefined ? this._zoom : zoom;
        return this.options.crs.latLngToPoint(toLatLng(latlng), zoom);
    },

    // @method unproject(point: Point, zoom: Number): LatLng
    // Inverse of [`project`](#map-project).
    unproject: function (point, zoom) {
        zoom = zoom === undefined ? this._zoom : zoom;
        return this.options.crs.pointToLatLng(toPoint(point), zoom);
    },

    // @method layerPointToLatLng(point: Point): LatLng
    // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
    // returns the corresponding geographical coordinate (for the current zoom level).
    layerPointToLatLng: function (point) {
        var projectedPoint = toPoint(point).add(this.getPixelOrigin());
        return this.unproject(projectedPoint);
    },

    // @method latLngToLayerPoint(latlng: LatLng): Point
    // Given a geographical coordinate, returns the corresponding pixel coordinate
    // relative to the [origin pixel](#map-getpixelorigin).
    latLngToLayerPoint: function (latlng) {
        var projectedPoint = this.project(toLatLng(latlng))._round();
        return projectedPoint._subtract(this.getPixelOrigin());
    },

    // @method wrapLatLng(latlng: LatLng): LatLng
    // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the
    // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the
    // CRS's bounds.
    // By default this means longitude is wrapped around the dateline so its
    // value is between -180 and +180 degrees.
    wrapLatLng: function (latlng) {
        return this.options.crs.wrapLatLng(toLatLng(latlng));
    },

    // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
    // Returns a `LatLngBounds` with the same size as the given one, ensuring that
    // its center is within the CRS's bounds.
    // By default this means the center longitude is wrapped around the dateline so its
    // value is between -180 and +180 degrees, and the majority of the bounds
    // overlaps the CRS's bounds.
    wrapLatLngBounds: function (latlng) {
        return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng));
    },

    // @method distance(latlng1: LatLng, latlng2: LatLng): Number
    // Returns the distance between two geographical coordinates according to
    // the map's CRS. By default this measures distance in meters.
    distance: function (latlng1, latlng2) {
        return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2));
    },

    // @method containerPointToLayerPoint(point: Point): Point
    // Given a pixel coordinate relative to the map container, returns the corresponding
    // pixel coordinate relative to the [origin pixel](#map-getpixelorigin).
    containerPointToLayerPoint: function (point) { // (Point)
        return toPoint(point).subtract(this._getMapPanePos());
    },

    // @method layerPointToContainerPoint(point: Point): Point
    // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
    // returns the corresponding pixel coordinate relative to the map container.
    layerPointToContainerPoint: function (point) { // (Point)
        return toPoint(point).add(this._getMapPanePos());
    },

    // @method containerPointToLatLng(point: Point): LatLng
    // Given a pixel coordinate relative to the map container, returns
    // the corresponding geographical coordinate (for the current zoom level).
    containerPointToLatLng: function (point) {
        var layerPoint = this.containerPointToLayerPoint(toPoint(point));
        return this.layerPointToLatLng(layerPoint);
    },

    // @method latLngToContainerPoint(latlng: LatLng): Point
    // Given a geographical coordinate, returns the corresponding pixel coordinate
    // relative to the map container.
    latLngToContainerPoint: function (latlng) {
        return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
    },

    // @method mouseEventToContainerPoint(ev: MouseEvent): Point
    // Given a MouseEvent object, returns the pixel coordinate relative to the
    // map container where the event took place.
    mouseEventToContainerPoint: function (e) {
        return DomEvent.getMousePosition(e, this._container);
    },

    // @method mouseEventToLayerPoint(ev: MouseEvent): Point
    // Given a MouseEvent object, returns the pixel coordinate relative to
    // the [origin pixel](#map-getpixelorigin) where the event took place.
    mouseEventToLayerPoint: function (e) {
        return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
    },

    // @method mouseEventToLatLng(ev: MouseEvent): LatLng
    // Given a MouseEvent object, returns geographical coordinate where the
    // event took place.
    mouseEventToLatLng: function (e) { // (MouseEvent)
        return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
    },

    // map initialization methods

    _initContainer: function (id) {
        var container = this._container = DomUtil.get(id);

        if (!container) {
            throw new Error('Map container not found.');
        } else if (container._leaflet_id) {
            throw new Error('Map container is already initialized.');
        }

        DomEvent.on(container, 'scroll', this._onScroll, this);
        this._containerId = Util.stamp(container);
    },

    _initLayout: function () {
        var container = this._container;

        this._fadeAnimated = this.options.fadeAnimation && Browser.any3d;

        DomUtil.addClass(container, 'leaflet-container' +
            (Browser.touch ? ' leaflet-touch' : '') +
            (Browser.retina ? ' leaflet-retina' : '') +
            (Browser.ielt9 ? ' leaflet-oldie' : '') +
            (Browser.safari ? ' leaflet-safari' : '') +
            (this._fadeAnimated ? ' leaflet-fade-anim' : ''));

        var position = DomUtil.getStyle(container, 'position');

        if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
            container.style.position = 'relative';
        }

        this._initPanes();

        if (this._initControlPos) {
            this._initControlPos();
        }
    },

    _initPanes: function () {
        var panes = this._panes = {};
        this._paneRenderers = {};

        // @section
        //
        // Panes are DOM elements used to control the ordering of layers on the map. You
        // can access panes with [`map.getPane`](#map-getpane) or
        // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the
        // [`map.createPane`](#map-createpane) method.
        //
        // Every map has the following default panes that differ only in zIndex.
        //
        // @pane mapPane: HTMLElement = 'auto'
        // Pane that contains all other map panes

        this._mapPane = this.createPane('mapPane', this._container);
        DomUtil.setPosition(this._mapPane, new Point(0, 0));

        // @pane tilePane: HTMLElement = 200
        // Pane for `GridLayer`s and `TileLayer`s
        this.createPane('tilePane');
        // @pane overlayPane: HTMLElement = 400
        // Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s
        this.createPane('shadowPane');
        // @pane shadowPane: HTMLElement = 500
        // Pane for overlay shadows (e.g. `Marker` shadows)
        this.createPane('overlayPane');
        // @pane markerPane: HTMLElement = 600
        // Pane for `Icon`s of `Marker`s
        this.createPane('markerPane');
        // @pane tooltipPane: HTMLElement = 650
        // Pane for `Tooltip`s.
        this.createPane('tooltipPane');
        // @pane popupPane: HTMLElement = 700
        // Pane for `Popup`s.
        this.createPane('popupPane');

        if (!this.options.markerZoomAnimation) {
            DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide');
            DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide');
        }
    },

    // private methods that modify map state

    // @section Map state change events
    _resetView: function (center, zoom) {
        DomUtil.setPosition(this._mapPane, new Point(0, 0));

        var loading = !this._loaded;
        this._loaded = true;
        zoom = this._limitZoom(zoom);

        this.fire('viewprereset');

        var zoomChanged = this._zoom !== zoom;
        this
            ._moveStart(zoomChanged, false)
            ._move(center, zoom)
            ._moveEnd(zoomChanged);

        // @event viewreset: Event
        // Fired when the map needs to redraw its content (this usually happens
        // on map zoom or load). Very useful for creating custom overlays.
        this.fire('viewreset');

        // @event load: Event
        // Fired when the map is initialized (when its center and zoom are set
        // for the first time).
        if (loading) {
            this.fire('load');
        }
    },

    _moveStart: function (zoomChanged, noMoveStart) {
        // @event zoomstart: Event
        // Fired when the map zoom is about to change (e.g. before zoom animation).
        // @event movestart: Event
        // Fired when the view of the map starts changing (e.g. user starts dragging the map).
        if (zoomChanged) {
            this.fire('zoomstart');
        }
        if (!noMoveStart) {
            this.fire('movestart');
        }
        return this;
    },

    _move: function (center, zoom, data) {
        if (zoom === undefined) {
            zoom = this._zoom;
        }
        var zoomChanged = this._zoom !== zoom;

        this._zoom = zoom;
        this._lastCenter = center;
        this._pixelOrigin = this._getNewPixelOrigin(center);

        // @event zoom: Event
        // Fired repeatedly during any change in zoom level, including zoom
        // and fly animations.
        if (zoomChanged || (data && data.pinch)) {  // Always fire 'zoom' if pinching because #3530
            this.fire('zoom', data);
        }

        // @event move: Event
        // Fired repeatedly during any movement of the map, including pan and
        // fly animations.
        return this.fire('move', data);
    },

    _moveEnd: function (zoomChanged) {
        // @event zoomend: Event
        // Fired when the map has changed, after any animations.
        if (zoomChanged) {
            this.fire('zoomend');
        }

        // @event moveend: Event
        // Fired when the center of the map stops changing (e.g. user stopped
        // dragging the map).
        return this.fire('moveend');
    },

    _stop: function () {
        Util.cancelAnimFrame(this._flyToFrame);
        if (this._panAnim) {
            this._panAnim.stop();
        }
        return this;
    },

    _rawPanBy: function (offset) {
        DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
    },

    _getZoomSpan: function () {
        return this.getMaxZoom() - this.getMinZoom();
    },

    _panInsideMaxBounds: function () {
        if (!this._enforcingBounds) {
            this.panInsideBounds(this.options.maxBounds);
        }
    },

    _checkIfLoaded: function () {
        if (!this._loaded) {
            throw new Error('Set map center and zoom first.');
        }
    },

    // DOM event handling

    // @section Interaction events
    _initEvents: function (remove) {
        this._targets = {};
        this._targets[Util.stamp(this._container)] = this;

        var onOff = remove ? DomEvent.off : DomEvent.on;

        // @event click: MouseEvent
        // Fired when the user clicks (or taps) the map.
        // @event dblclick: MouseEvent
        // Fired when the user double-clicks (or double-taps) the map.
        // @event mousedown: MouseEvent
        // Fired when the user pushes the mouse button on the map.
        // @event mouseup: MouseEvent
        // Fired when the user releases the mouse button on the map.
        // @event mouseover: MouseEvent
        // Fired when the mouse enters the map.
        // @event mouseout: MouseEvent
        // Fired when the mouse leaves the map.
        // @event mousemove: MouseEvent
        // Fired while the mouse moves over the map.
        // @event contextmenu: MouseEvent
        // Fired when the user pushes the right mouse button on the map, prevents
        // default browser context menu from showing if there are listeners on
        // this event. Also fired on mobile when the user holds a single touch
        // for a second (also called long press).
        // @event keypress: KeyboardEvent
        // Fired when the user presses a key from the keyboard while the map is focused.
        onOff(this._container, 'click dblclick mousedown mouseup ' +
            'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this);

        if (this.options.trackResize) {
            onOff(window, 'resize', this._onResize, this);
        }

        if (Browser.any3d && this.options.transform3DLimit) {
            (remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
        }
    },

    _onResize: function () {
        Util.cancelAnimFrame(this._resizeRequest);
        this._resizeRequest = Util.requestAnimFrame(
                function () { this.invalidateSize({debounceMoveend: true}); }, this);
    },

    _onScroll: function () {
        this._container.scrollTop  = 0;
        this._container.scrollLeft = 0;
    },

    _onMoveEnd: function () {
        var pos = this._getMapPanePos();
        if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
            // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
            // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/
            this._resetView(this.getCenter(), this.getZoom());
        }
    },

    _findEventTargets: function (e, type) {
        var targets = [],
            target,
            isHover = type === 'mouseout' || type === 'mouseover',
            src = e.target || e.srcElement,
            dragging = false;

        while (src) {
            target = this._targets[Util.stamp(src)];
            if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) {
                // Prevent firing click after you just dragged an object.
                dragging = true;
                break;
            }
            if (target && target.listens(type, true)) {
                if (isHover && !DomEvent.isExternalTarget(src, e)) { break; }
                targets.push(target);
                if (isHover) { break; }
            }
            if (src === this._container) { break; }
            src = src.parentNode;
        }
        if (!targets.length && !dragging && !isHover && DomEvent.isExternalTarget(src, e)) {
            targets = [this];
        }
        return targets;
    },

    _handleDOMEvent: function (e) {
        if (!this._loaded || DomEvent.skipped(e)) { return; }

        var type = e.type;

        if (type === 'mousedown' || type === 'keypress') {
            // prevents outline when clicking on keyboard-focusable element
            DomUtil.preventOutline(e.target || e.srcElement);
        }

        this._fireDOMEvent(e, type);
    },

    _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],

    _fireDOMEvent: function (e, type, targets) {

        if (e.type === 'click') {
            // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
            // @event preclick: MouseEvent
            // Fired before mouse click on the map (sometimes useful when you
            // want something to happen on click before any existing click
            // handlers start running).
            var synth = Util.extend({}, e);
            synth.type = 'preclick';
            this._fireDOMEvent(synth, synth.type, targets);
        }

        if (e._stopped) { return; }

        // Find the layer the event is propagating from and its parents.
        targets = (targets || []).concat(this._findEventTargets(e, type));

        if (!targets.length) { return; }

        var target = targets[0];
        if (type === 'contextmenu' && target.listens(type, true)) {
            DomEvent.preventDefault(e);
        }

        var data = {
            originalEvent: e
        };

        if (e.type !== 'keypress') {
            var isMarker = target.getLatLng && (!target._radius || target._radius <= 10);
            data.containerPoint = isMarker ?
                this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
            data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
            data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
        }

        for (var i = 0; i < targets.length; i++) {
            targets[i].fire(type, data, true);
            if (data.originalEvent._stopped ||
                (targets[i].options.bubblingMouseEvents === false && Util.indexOf(this._mouseEvents, type) !== -1)) { return; }
        }
    },

    _draggableMoved: function (obj) {
        obj = obj.dragging && obj.dragging.enabled() ? obj : this;
        return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
    },

    _clearHandlers: function () {
        for (var i = 0, len = this._handlers.length; i < len; i++) {
            this._handlers[i].disable();
        }
    },

    // @section Other Methods

    // @method whenReady(fn: Function, context?: Object): this
    // Runs the given function `fn` when the map gets initialized with
    // a view (center and zoom) and at least one layer, or immediately
    // if it's already initialized, optionally passing a function context.
    whenReady: function (callback, context) {
        if (this._loaded) {
            callback.call(context || this, {target: this});
        } else {
            this.on('load', callback, context);
        }
        return this;
    },

    // private methods for getting map state

    _getMapPanePos: function () {
        return DomUtil.getPosition(this._mapPane) || new Point(0, 0);
    },

    _moved: function () {
        var pos = this._getMapPanePos();
        return pos && !pos.equals([0, 0]);
    },

    _getTopLeftPoint: function (center, zoom) {
        var pixelOrigin = center && zoom !== undefined ?
            this._getNewPixelOrigin(center, zoom) :
            this.getPixelOrigin();
        return pixelOrigin.subtract(this._getMapPanePos());
    },

    _getNewPixelOrigin: function (center, zoom) {
        var viewHalf = this.getSize()._divideBy(2);
        return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round();
    },

    _latLngToNewLayerPoint: function (latlng, zoom, center) {
        var topLeft = this._getNewPixelOrigin(center, zoom);
        return this.project(latlng, zoom)._subtract(topLeft);
    },

    _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
        var topLeft = this._getNewPixelOrigin(center, zoom);
        return toBounds([
            this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
            this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
            this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
            this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)
        ]);
    },

    // layer point of the current center
    _getCenterLayerPoint: function () {
        return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
    },

    // offset of the specified place to the current center in pixels
    _getCenterOffset: function (latlng) {
        return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
    },

    // adjust center for view to get inside bounds
    _limitCenter: function (center, zoom, bounds) {

        if (!bounds) { return center; }

        var centerPoint = this.project(center, zoom),
            viewHalf = this.getSize().divideBy(2),
            viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
            offset = this._getBoundsOffset(viewBounds, bounds, zoom);

        // If offset is less than a pixel, ignore.
        // This prevents unstable projections from getting into
        // an infinite loop of tiny offsets.
        if (offset.round().equals([0, 0])) {
            return center;
        }

        return this.unproject(centerPoint.add(offset), zoom);
    },

    // adjust offset for view to get inside bounds
    _limitOffset: function (offset, bounds) {
        if (!bounds) { return offset; }

        var viewBounds = this.getPixelBounds(),
            newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));

        return offset.add(this._getBoundsOffset(newBounds, bounds));
    },

    // returns offset needed for pxBounds to get inside maxBounds at a specified zoom
    _getBoundsOffset: function (pxBounds, maxBounds, zoom) {
        var projectedMaxBounds = toBounds(
                this.project(maxBounds.getNorthEast(), zoom),
                this.project(maxBounds.getSouthWest(), zoom)
            ),
            minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
            maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),

            dx = this._rebound(minOffset.x, -maxOffset.x),
            dy = this._rebound(minOffset.y, -maxOffset.y);

        return new Point(dx, dy);
    },

    _rebound: function (left, right) {
        return left + right > 0 ?
            Math.round(left - right) / 2 :
            Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
    },

    _limitZoom: function (zoom) {
        var min = this.getMinZoom(),
            max = this.getMaxZoom(),
            snap = Browser.any3d ? this.options.zoomSnap : 1;
        if (snap) {
            zoom = Math.round(zoom / snap) * snap;
        }
        return Math.max(min, Math.min(max, zoom));
    },

    _onPanTransitionStep: function () {
        this.fire('move');
    },

    _onPanTransitionEnd: function () {
        DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
        this.fire('moveend');
    },

    _tryAnimatedPan: function (center, options) {
        // difference between the new and current centers in pixels
        var offset = this._getCenterOffset(center)._trunc();

        // don't animate too far unless animate: true specified in options
        if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }

        this.panBy(offset, options);

        return true;
    },

    _createAnimProxy: function () {

        var proxy = this._proxy = DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated');
        this._panes.mapPane.appendChild(proxy);

        this.on('zoomanim', function (e) {
            var prop = DomUtil.TRANSFORM,
                transform = this._proxy.style[prop];

            DomUtil.setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));

            // workaround for case when transform is the same and so transitionend event is not fired
            if (transform === this._proxy.style[prop] && this._animatingZoom) {
                this._onZoomTransitionEnd();
            }
        }, this);

        this.on('load moveend', function () {
            var c = this.getCenter(),
                z = this.getZoom();
            DomUtil.setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1));
        }, this);

        this._on('unload', this._destroyAnimProxy, this);
    },

    _destroyAnimProxy: function () {
        DomUtil.remove(this._proxy);
        delete this._proxy;
    },

    _catchTransitionEnd: function (e) {
        if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
            this._onZoomTransitionEnd();
        }
    },

    _nothingToAnimate: function () {
        return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
    },

    _tryAnimatedZoom: function (center, zoom, options) {

        if (this._animatingZoom) { return true; }

        options = options || {};

        // don't animate if disabled, not supported or zoom difference is too large
        if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
                Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }

        // offset is the pixel coords of the zoom origin relative to the current center
        var scale = this.getZoomScale(zoom),
            offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);

        // don't animate if the zoom origin isn't within one screen from the current center, unless forced
        if (options.animate !== true && !this.getSize().contains(offset)) { return false; }

        Util.requestAnimFrame(function () {
            this
                ._moveStart(true, false)
                ._animateZoom(center, zoom, true);
        }, this);

        return true;
    },

    _animateZoom: function (center, zoom, startAnim, noUpdate) {
        if (!this._mapPane) { return; }

        if (startAnim) {
            this._animatingZoom = true;

            // remember what center/zoom to set after animation
            this._animateToCenter = center;
            this._animateToZoom = zoom;

            DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
        }

        // @event zoomanim: ZoomAnimEvent
        // Fired on every frame of a zoom animation
        this.fire('zoomanim', {
            center: center,
            zoom: zoom,
            noUpdate: noUpdate
        });

        // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
        setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250);
    },

    _onZoomTransitionEnd: function () {
        if (!this._animatingZoom) { return; }

        if (this._mapPane) {
            DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
        }

        this._animatingZoom = false;

        this._move(this._animateToCenter, this._animateToZoom);

        // This anim frame should prevent an obscure iOS webkit tile loading race condition.
        Util.requestAnimFrame(function () {
            this._moveEnd(true);
        }, this);
    }
});

// @section

// @factory L.map(id: String, options?: Map options)
// Instantiates a map object given the DOM ID of a `<div>` element
// and optionally an object literal with `Map options`.
//
// @alternative
// @factory L.map(el: HTMLElement, options?: Map options)
// Instantiates a map object given an instance of a `<div>` HTML element
// and optionally an object literal with `Map options`.
export function createMap(id, options) {
    return new Map(id, options);
}