/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */
var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');

/**
 * @classdesc
 * HTML5 Audio implementation of the sound.
 *
 * @class HTML5AudioSound
 * @extends Phaser.Sound.BaseSound
 * @memberOf Phaser.Sound
 * @constructor
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 * @since 3.0.0
 *
 * @param {Phaser.Sound.HTML5AudioSoundManager} manager - Reference to the current sound manager instance.
 * @param {string} key - Asset key for the sound.
 * @param {SoundConfig} [config={}] - An optional config object containing default sound settings.
 */
var HTML5AudioSound = new Class({
    Extends: BaseSound,
    initialize: function HTML5AudioSound (manager, key, config)
    {
        if (config === void 0) { config = {}; }

        /**
         * An array containing all HTML5 Audio tags that could be used for individual
         * sound's playback. Number of instances depends on the config value passed
         * to the Loader#audio method call, default is 1.
         *
         * @name Phaser.Sound.HTML5AudioSound#tags
         * @type {HTMLAudioElement[]}
         * @private
         * @since 3.0.0
         */
        this.tags = manager.game.cache.audio.get(key);
        if (!this.tags)
        {
            // eslint-disable-next-line no-console
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }

        /**
         * Reference to an HTML5 Audio tag used for playing sound.
         *
         * @name Phaser.Sound.HTML5AudioSound#audio
         * @type {HTMLAudioElement}
         * @private
         * @default null
         * @since 3.0.0
         */
        this.audio = null;

        /**
         * Timestamp as generated by the Request Animation Frame or SetTimeout
         * representing the time at which the delayed sound playback should start.
         * Set to 0 if sound playback is not delayed.
         *
         * @name Phaser.Sound.HTML5AudioSound#startTime
         * @type {number}
         * @private
         * @default 0
         * @since 3.0.0
         */
        this.startTime = 0;

        /**
         * Audio tag's playback position recorded on previous
         * update method call. Set to 0 if sound is not playing.
         *
         * @name Phaser.Sound.HTML5AudioSound#previousTime
         * @type {number}
         * @private
         * @default 0
         * @since 3.0.0
         */
        this.previousTime = 0;
        this.duration = this.tags[0].duration;
        this.totalDuration = this.tags[0].duration;
        BaseSound.call(this, manager, key, config);
    },

    /**
     * Play this sound, or a marked section of it.
     * It always plays the sound from the start. If you want to start playback from a specific time
     * you can set 'seek' setting of the config object, provided to this call, to that value.
     *
     * @method Phaser.Sound.HTML5AudioSound#play
     * @since 3.0.0
     *
     * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound.
     * @param {SoundConfig} [config] - Optional sound config object to be applied to this marker or entire sound if no marker name is provided. It gets memorized for future plays of current section of the sound.
     *
     * @return {boolean} Whether the sound started playing successfully.
     */
    play: function (markerName, config)
    {
        if (this.manager.isLocked(this, 'play', [ markerName, config ]))
        {
            return false;
        }
        if (!BaseSound.prototype.play.call(this, markerName, config))
        {
            return false;
        }

        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag())
        {
            return false;
        }

        /**
         * @event Phaser.Sound.HTML5AudioSound#play
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         */
        this.emit('play', this);
        return true;
    },

    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#pause
     * @since 3.0.0
     *
     * @return {boolean} Whether the sound was paused successfully.
     */
    pause: function ()
    {
        if (this.manager.isLocked(this, 'pause'))
        {
            return false;
        }
        if (this.startTime > 0)
        {
            return false;
        }
        if (!BaseSound.prototype.pause.call(this))
        {
            return false;
        }

        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.audio.currentTime
            - (this.currentMarker ? this.currentMarker.start : 0);
        this.stopAndReleaseAudioTag();

        /**
         * @event Phaser.Sound.HTML5AudioSound#pause
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         */
        this.emit('pause', this);
        return true;
    },

    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#resume
     * @since 3.0.0
     *
     * @return {boolean} Whether the sound was resumed successfully.
     */
    resume: function ()
    {
        if (this.manager.isLocked(this, 'resume'))
        {
            return false;
        }
        if (this.startTime > 0)
        {
            return false;
        }
        if (!BaseSound.prototype.resume.call(this))
        {
            return false;
        }

        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag())
        {
            return false;
        }

        /**
         * @event Phaser.Sound.HTML5AudioSound#resume
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         */
        this.emit('resume', this);
        return true;
    },

    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#stop
     * @since 3.0.0
     *
     * @return {boolean} Whether the sound was stopped successfully.
     */
    stop: function ()
    {
        if (this.manager.isLocked(this, 'stop'))
        {
            return false;
        }
        if (!BaseSound.prototype.stop.call(this))
        {
            return false;
        }

        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndReleaseAudioTag();

        /**
         * @event Phaser.Sound.HTML5AudioSound#stop
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         */
        this.emit('stop', this);
        return true;
    },

    /**
     * Used internally to do what the name says.
     *
     * @method Phaser.Sound.HTML5AudioSound#pickAndPlayAudioTag
     * @private
     * @since 3.0.0
     *
     * @return {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAndPlayAudioTag: function ()
    {
        if (!this.pickAudioTag())
        {
            this.reset();
            return false;
        }
        var seek = this.currentConfig.seek;
        var delay = this.currentConfig.delay;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        this.previousTime = offset;
        this.audio.currentTime = offset;
        this.applyConfig();
        if (delay === 0)
        {
            this.startTime = 0;
            if (this.audio.paused)
            {
                this.playCatchPromise();
            }
        }
        else
        {
            this.startTime = window.performance.now() + delay * 1000;
            if (!this.audio.paused)
            {
                this.audio.pause();
            }
        }
        this.resetConfig();
        return true;
    },

    /**
     * This method performs the audio tag pooling logic. It first looks for
     * unused audio tag to assign to this sound object. If there are no unused
     * audio tags, based on HTML5AudioSoundManager#override property value, it
     * looks for sound with most advanced playback and hijacks its audio tag or
     * does nothing.
     *
     * @method Phaser.Sound.HTML5AudioSound#pickAudioTag
     * @private
     * @since 3.0.0
     *
     * @return {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAudioTag: function ()
    {
        if (this.audio)
        {
            return true;
        }
        for (var i = 0; i < this.tags.length; i++)
        {
            var audio = this.tags[i];
            if (audio.dataset.used === 'false')
            {
                audio.dataset.used = 'true';
                this.audio = audio;
                return true;
            }
        }
        if (!this.manager.override)
        {
            return false;
        }
        var otherSounds = [];
        this.manager.forEachActiveSound(function (sound)
        {
            if (sound.key === this.key && sound.audio)
            {
                otherSounds.push(sound);
            }
        }, this);
        otherSounds.sort(function (a1, a2)
        {
            if (a1.loop === a2.loop)
            {
                // sort by progress
                return (a2.seek / a2.duration) - (a1.seek / a1.duration);
            }
            return a1.loop ? 1 : -1;
        });
        var selectedSound = otherSounds[0];
        this.audio = selectedSound.audio;
        selectedSound.reset();
        selectedSound.audio = null;
        selectedSound.startTime = 0;
        selectedSound.previousTime = 0;
        return true;
    },

    /**
     * Method used for playing audio tag and catching possible exceptions
     * thrown from rejected Promise returned from play method call.
     *
     * @method Phaser.Sound.HTML5AudioSound#playCatchPromise
     * @private
     * @since 3.0.0
     */
    playCatchPromise: function ()
    {
        var playPromise = this.audio.play();
        if (playPromise)
        {
            // eslint-disable-next-line no-unused-vars
            playPromise.catch(function (reason) { });
        }
    },

    /**
     * Used internally to do what the name says.
     *
     * @method Phaser.Sound.HTML5AudioSound#stopAndReleaseAudioTag
     * @private
     * @since 3.0.0
     */
    stopAndReleaseAudioTag: function ()
    {
        this.audio.pause();
        this.audio.dataset.used = 'false';
        this.audio = null;
        this.startTime = 0;
        this.previousTime = 0;
    },

    /**
     * Method used internally to reset sound state, usually when stopping sound
     * or when hijacking audio tag from another sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#reset
     * @private
     * @since 3.0.0
     */
    reset: function ()
    {
        BaseSound.prototype.stop.call(this);
    },

    /**
     * Method used internally by sound manager for pausing sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.HTML5AudioSoundManager#onBlur
     * @private
     * @since 3.0.0
     */
    onBlur: function ()
    {
        this.isPlaying = false;
        this.isPaused = true;
        this.currentConfig.seek = this.audio.currentTime -
            (this.currentMarker ? this.currentMarker.start : 0);
        this.currentConfig.delay = Math.max(0, (this.startTime - window.performance.now()) / 1000);
        this.stopAndReleaseAudioTag();
    },

    /**
     * Method used internally by sound manager for resuming sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.HTML5AudioSound#onFocus
     * @private
     * @since 3.0.0
     */
    onFocus: function ()
    {
        this.isPlaying = true;
        this.isPaused = false;
        this.pickAndPlayAudioTag();
    },

    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @method Phaser.Sound.HTML5AudioSound#update
     * @protected
     * @since 3.0.0
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    // eslint-disable-next-line no-unused-vars
    update: function (time, delta)
    {
        if (!this.isPlaying)
        {
            return;
        }

        // handling delayed playback
        if (this.startTime > 0)
        {
            if (this.startTime < time - this.manager.audioPlayDelay)
            {
                this.audio.currentTime += Math.max(0, time - this.startTime) / 1000;
                this.startTime = 0;
                this.previousTime = this.audio.currentTime;
                this.playCatchPromise();
            }
            return;
        }

        // handle looping and ending
        var startTime = this.currentMarker ? this.currentMarker.start : 0;
        var endTime = startTime + this.duration;
        var currentTime = this.audio.currentTime;
        if (this.currentConfig.loop)
        {
            if (currentTime >= endTime - this.manager.loopEndOffset)
            {
                this.audio.currentTime = startTime + Math.max(0, currentTime - endTime);
                currentTime = this.audio.currentTime;
            }
            else if (currentTime < startTime)
            {
                this.audio.currentTime += startTime;
                currentTime = this.audio.currentTime;
            }
            if (currentTime < this.previousTime)
            {
                /**
                 * @event Phaser.Sound.HTML5AudioSound#looped
                 * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
                 */
                this.emit('looped', this);
            }
        }
        else if (currentTime >= endTime)
        {
            this.reset();
            this.stopAndReleaseAudioTag();

            /**
             * @event Phaser.Sound.HTML5AudioSound#ended
             * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
             */
            this.emit('ended', this);
            return;
        }
        this.previousTime = currentTime;
    },

    /**
     * Calls Phaser.Sound.BaseSound#destroy method
     * and cleans up all HTML5 Audio related stuff.
     *
     * @method Phaser.Sound.HTML5AudioSound#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        BaseSound.prototype.destroy.call(this);
        this.tags = null;
        if (this.audio)
        {
            this.stopAndReleaseAudioTag();
        }
    },

    /**
     * Method used internally to determine mute setting of the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#setMute
     * @private
     * @since 3.0.0
     */
    setMute: function ()
    {
        if (this.audio)
        {
            this.audio.muted = this.currentConfig.mute || this.manager.mute;
        }
    },

    /**
     * Method used internally to calculate total volume of the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#setVolume
     * @private
     * @since 3.0.0
     */
    setVolume: function ()
    {
        if (this.audio)
        {
            this.audio.volume = this.currentConfig.volume * this.manager.volume;
        }
    },

    /**
     * Method used internally to calculate total playback rate of the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#setRate
     * @protected
     * @since 3.0.0
     */
    setRate: function ()
    {
        BaseSound.prototype.setRate.call(this);
        if (this.audio)
        {
            this.audio.playbackRate = this.totalRate;
        }
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'mute', {
    get: function ()
    {
        return this.currentConfig.mute;
    },
    set: function (value)
    {
        this.currentConfig.mute = value;
        if (this.manager.isLocked(this, 'mute', value))
        {
            return;
        }
        this.setMute();

        /**
         * @event Phaser.Sound.HTML5AudioSound#mute
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#mute property.
         */
        this.emit('mute', this, value);
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'volume', {
    get: function ()
    {
        return this.currentConfig.volume;
    },
    set: function (value)
    {
        this.currentConfig.volume = value;
        if (this.manager.isLocked(this, 'volume', value))
        {
            return;
        }
        this.setVolume();

        /**
         * @event Phaser.Sound.HTML5AudioSound#volume
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#volume property.
         */
        this.emit('volume', this, value);
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'rate', {
    get: function ()
    {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').get.call(this);
    },
    set: function (value)
    {
        this.currentConfig.rate = value;
        if (this.manager.isLocked(this, 'rate', value))
        {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').set.call(this, value);
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'detune', {
    get: function ()
    {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').get.call(this);
    },
    set: function (value)
    {
        this.currentConfig.detune = value;
        if (this.manager.isLocked(this, 'detune', value))
        {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').set.call(this, value);
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'seek', {
    get: function ()
    {
        if (this.isPlaying)
        {
            return this.audio.currentTime -
                (this.currentMarker ? this.currentMarker.start : 0);
        }
        else if (this.isPaused)
        {
            return this.currentConfig.seek;
        }
        else
        {
            return 0;
        }
    },
    set: function (value)
    {
        if (this.manager.isLocked(this, 'seek', value))
        {
            return;
        }
        if (this.startTime > 0)
        {
            return;
        }
        if (this.isPlaying || this.isPaused)
        {
            value = Math.min(Math.max(0, value), this.duration);
            if (this.isPlaying)
            {
                this.previousTime = value;
                this.audio.currentTime = value;
            }
            else if (this.isPaused)
            {
                this.currentConfig.seek = value;
            }

            /**
             * @event Phaser.Sound.HTML5AudioSound#seek
             * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
             * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#seek property.
             */
            this.emit('seek', this, value);
        }
    }
});
Object.defineProperty(HTML5AudioSound.prototype, 'loop', {
    get: function ()
    {
        return this.currentConfig.loop;
    },
    set: function (value)
    {
        this.currentConfig.loop = value;
        if (this.manager.isLocked(this, 'loop', value))
        {
            return;
        }
        if (this.audio)
        {
            this.audio.loop = value;
        }

        /**
         * @event Phaser.Sound.HTML5AudioSound#loop
         * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event.
         * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#loop property.
         */
        this.emit('loop', this, value);
    }
});
module.exports = HTML5AudioSound;


*/
updateTileTexture: function ()
{
if (!this.dirty)
{
return;
}

if (!this.dirty && this.oldFrame == this.frame)

/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

// Export the points as an array of flat numbers, following the sequence [ x,y, x,y, x,y ]

/**
 * [description]
 *
 * @function Phaser.Geom.Polygon.GetNumberArray
 * @since 3.0.0
 *
 * @param {Phaser.Geom.Polygon} polygon - [description]
 * @param {array} [output] - [description]
 *
 * @return {number[]} [description]
 */
var GetNumberArray = function (polygon, output)
{
    if (output === undefined) { output = []; }

    for (var i = 0; i < polygon.points.length; i++)
    {
        output.push(polygon.points[i].x);
        output.push(polygon.points[i].y);
    }

    return output;
};

module.exports = GetNumberArray;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var Class = require('../../utils/Class');

// https://developer.mozilla.org/en-US/docs/Web/API/Touch_events
// https://patrickhlauke.github.io/touch/tests/results/
// https://www.html5rocks.com/en/mobile/touch/

/**
 * @classdesc
 * [description]
 *
 * @class TouchManager
 * @memberOf Phaser.Input.Touch
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Input.InputManager} inputManager - [description]
 */
var TouchManager = new Class({

    initialize:

    function TouchManager (inputManager)
    {
        /**
         * [description]
         *
         * @name Phaser.Input.Touch.TouchManager#manager
         * @type {Phaser.Input.InputManager}
         * @since 3.0.0
         */
        this.manager = inputManager;

        /**
         * If true the DOM events will have event.preventDefault applied to them, if false they will propagate fully.
         *
         * @name Phaser.Input.Touch.TouchManager#capture
         * @type {boolean}
         * @default true
         * @since 3.0.0
         */
        this.capture = true;

        /**
         * [description]
         *
         * @name Phaser.Input.Touch.TouchManager#enabled
         * @type {boolean}
         * @default false
         * @since 3.0.0
         */
        this.enabled = false;

        /**
         * [description]
         *
         * @name Phaser.Input.Touch.TouchManager#target
         * @type {null}
         * @since 3.0.0
         */
        this.target;

        /**
         * [description]
         *
         * @name Phaser.Input.Touch.TouchManager#handler
         * @type {function}
         * @since 3.0.0
         */
        this.handler;
    },

    /**
     * [description]
     *
     * @method Phaser.Input.Touch.TouchManager#boot
     * @since 3.0.0
     */
    boot: function ()
    {
        var config = this.manager.config;

        this.enabled = config.inputTouch;
        this.target = config.inputTouchEventTarget;
        this.capture = config.inputTouchCapture;

        if (!this.target)
        {
            this.target = this.manager.game.canvas;
        }

        if (this.enabled)
        {
            this.startListeners();
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Input.Touch.TouchManager#startListeners
     * @since 3.0.0
     */
    startListeners: function ()
    {
        var queue = this.manager.queue;
        var target = this.target;

        var passive = { passive: true };
        var nonPassive = { passive: false };

        var handler;

        if (this.capture)
        {
            handler = function (event)
            {
                if (event.defaultPrevented)
                {
                    // Do nothing if event already handled
                    return;
                }

                // console.log('touch', event);

                queue.push(event);

                event.preventDefault();
            };

            target.addEventListener('touchstart', handler, nonPassive);
            target.addEventListener('touchmove', handler, nonPassive);
            target.addEventListener('touchend', handler, nonPassive);
        }
        else
        {
            handler = function (event)
            {
                if (event.defaultPrevented)
                {
                    // Do nothing if event already handled
                    return;
                }

                queue.push(event);
            };

            target.addEventListener('touchstart', handler, passive);
            target.addEventListener('touchmove', handler, passive);
            target.addEventListener('touchend', handler, passive);
        }

        this.handler = handler;
    },

    /**
     * [description]
     *
     * @method Phaser.Input.Touch.TouchManager#stopListeners
     * @since 3.0.0
     */
    stopListeners: function ()
    {
        var target = this.target;

        target.removeEventListener('touchstart', this.handler);
        target.removeEventListener('touchmove', this.handler);
        target.removeEventListener('touchend', this.handler);
    },

    /**
     * [description]
     *
     * @method Phaser.Input.Touch.TouchManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.stopListeners();

        this.manager = null;
    }

});

module.exports = TouchManager;


this.canvasBufferCtx.clearRect(0, 0, this.canvasBuffer.width, this.canvasBuffer.height);

/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var CanvasPool = require('../../display/canvas/CanvasPool');
var Class = require('../../utils/Class');
var Components = require('../components');
var GameObject = require('../GameObject');
var GetPowerOfTwo = require('../../math/pow2/GetPowerOfTwo');
var TileSpriteRender = require('./TileSpriteRender');

/**
 * @classdesc
 * [description]
 *
 * @class TileSprite
 * @extends Phaser.GameObjects.GameObject
 * @memberOf Phaser.GameObjects
 * @constructor
 * @since 3.0.0
 *
 * @extends Phaser.GameObjects.Components.Alpha
 * @extends Phaser.GameObjects.Components.BlendMode
 * @extends Phaser.GameObjects.Components.Depth
 * @extends Phaser.GameObjects.Components.Flip
 * @extends Phaser.GameObjects.Components.GetBounds
 * @extends Phaser.GameObjects.Components.Origin
 * @extends Phaser.GameObjects.Components.Pipeline
 * @extends Phaser.GameObjects.Components.ScaleMode
 * @extends Phaser.GameObjects.Components.ScrollFactor
 * @extends Phaser.GameObjects.Components.Size
 * @extends Phaser.GameObjects.Components.Texture
 * @extends Phaser.GameObjects.Components.Tint
 * @extends Phaser.GameObjects.Components.Transform
 * @extends Phaser.GameObjects.Components.Visible
 *
 * @param {Phaser.Scene} scene - The Scene to which this Game Object belongs. A Game Object can only belong to one Scene at a time.
 * @param {number} x - The horizontal position of this Game Object in the world.
 * @param {number} y - The vertical position of this Game Object in the world.
 * @param {number} width - The width of the Game Object.
 * @param {number} height - The height of the Game Object.
 * @param {string} texture - The key of the Texture this Game Object will use to render with, as stored in the Texture Manager.
 * @param {string|integer} [frame] - An optional frame from the Texture this Game Object is rendering with.
 */
var TileSprite = new Class({

    Extends: GameObject,

    Mixins: [
        Components.Alpha,
        Components.BlendMode,
        Components.Depth,
        Components.Flip,
        Components.GetBounds,
        Components.Origin,
        Components.Pipeline,
        Components.ScaleMode,
        Components.ScrollFactor,
        Components.Size,
        Components.Texture,
        Components.Tint,
        Components.Transform,
        Components.Visible,
        TileSpriteRender
    ],

    initialize:

    function TileSprite (scene, x, y, width, height, texture, frame)
    {
        var renderer = scene.sys.game.renderer;

        GameObject.call(this, scene, 'TileSprite');

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#tilePositionX
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.tilePositionX = 0;

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#tilePositionY
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.tilePositionY = 0;

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#dirty
         * @type {boolean}
         * @default true
         * @since 3.0.0
         */
        this.dirty = true;

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#tileTexture
         * @type {?[type]}
         * @default null
         * @since 3.0.0
         */
        this.tileTexture = null;

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#renderer
         * @type {[type]}
         * @since 3.0.0
         */
        this.renderer = renderer;

        this.setTexture(texture, frame);
        this.setPosition(x, y);
        this.setSize(width, height);
        this.setOriginFromFrame();
        this.initPipeline('TextureTintPipeline');

        /**
         * The next power of two value from the width of the Frame.
         *
         * @name Phaser.GameObjects.TileSprite#potWidth
         * @type {integer}
         * @since 3.0.0
         */
        this.potWidth = GetPowerOfTwo(this.frame.width);

        /**
         * The next power of two value from the height of the Frame.
         *
         * @name Phaser.GameObjects.TileSprite#potHeight
         * @type {integer}
         * @since 3.0.0
         */
        this.potHeight = GetPowerOfTwo(this.frame.height);

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#canvasPattern
         * @type {?CanvasPattern}
         * @default null
         * @since 3.0.0
         */
        this.canvasPattern = null;

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#canvasBuffer
         * @type {HTMLCanvasElement}
         * @since 3.0.0
         */
        this.canvasBuffer = CanvasPool.create2D(null, this.potWidth, this.potHeight);

        /**
         * [description]
         *
         * @name Phaser.GameObjects.TileSprite#canvasBufferCtx
         * @type {CanvasRenderingContext2D}
         * @since 3.0.0
         */
        this.canvasBufferCtx = this.canvasBuffer.getContext('2d');

        this.updateTileTexture();

        scene.sys.game.renderer.onContextRestored(function (renderer)
        {
            var gl = renderer.gl;
            this.tileTexture = null;
            this.dirty = true;
            this.tileTexture = renderer.createTexture2D(0, gl.LINEAR, gl.LINEAR, gl.REPEAT, gl.REPEAT, gl.RGBA, this.canvasBuffer, this.potWidth, this.potHeight);
        }, this);
    },

    /**
     * [description]
     *
     * @method Phaser.GameObjects.TileSprite#updateTileTexture
     * @since 3.0.0
     */
    updateTileTexture: function ()
    {
        if (!this.dirty)
        {
            return;
        }

        this.canvasBufferCtx.drawImage(
            this.frame.source.image,
            this.frame.cutX, this.frame.cutY,
            this.frame.cutWidth, this.frame.cutHeight,
            0, 0,
            this.potWidth, this.potHeight
        );

        if (this.renderer.gl)
        {
            this.tileTexture = this.renderer.canvasToTexture(this.canvasBuffer, this.tileTexture, (this.tileTexture === null), this.scaleMode);
        }
        else
        {
            this.canvasPattern = this.canvasBufferCtx.createPattern(this.canvasBuffer, 'repeat');
        }

        this.dirty = false;
    },

    /**
     * [description]
     *
     * @method Phaser.GameObjects.TileSprite#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        if (this.renderer)
        {
            this.renderer.deleteTexture(this.tileTexture);
        }

        CanvasPool.remove(this.canvasBuffer);

        this.canvasPattern = null;
        this.canvasBufferCtx = null;
        this.canvasBuffer = null;

        this.renderer = null;
        this.visible = false;
    }

});

module.exports = TileSprite;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

/**
 * Determines the full screen support of the browser running this Phaser Game instance.
 * These values are read-only and populated during the boot sequence of the game.
 * They are then referenced by internal game systems and are available for you to access
 * via `this.sys.game.device.fullscreen` from within any Scene.
 *
 * @name Phaser.Device.Fullscreen
 * @since 3.0.0
 *
 * @type {object}
 * @property {boolean} available - Does the browser support the Full Screen API?
 * @property {boolean} keyboard - Does the browser support access to the Keyboard during Full Screen mode?
 * @property {string} cancel - If the browser supports the Full Screen API this holds the call you need to use to cancel it.
 * @property {string} request - If the browser supports the Full Screen API this holds the call you need to use to activate it.
 */
var Fullscreen = {

    available: false,
    cancel: '',
    keyboard: false,
    request: ''

};

/**
* Checks for support of the Full Screen API.
*/
function init ()
{
    var i;

    var fs = [
        'requestFullscreen',
        'requestFullScreen',
        'webkitRequestFullscreen',
        'webkitRequestFullScreen',
        'msRequestFullscreen',
        'msRequestFullScreen',
        'mozRequestFullScreen',
        'mozRequestFullscreen'
    ];

    var element = document.createElement('div');

    for (i = 0; i < fs.length; i++)
    {
        if (element[fs[i]])
        {
            Fullscreen.available = true;
            Fullscreen.request = fs[i];
            break;
        }
    }

    var cfs = [
        'cancelFullScreen',
        'exitFullscreen',
        'webkitCancelFullScreen',
        'webkitExitFullscreen',
        'msCancelFullScreen',
        'msExitFullscreen',
        'mozCancelFullScreen',
        'mozExitFullscreen'
    ];

    if (Fullscreen.available)
    {
        for (i = 0; i < cfs.length; i++)
        {
            if (document[cfs[i]])
            {
                Fullscreen.cancel = cfs[i];
                break;
            }
        }
    }

    //  Keyboard Input?
    if (window['Element'] && Element['ALLOW_KEYBOARD_INPUT'])
    {
        Fullscreen.keyboard = true;
    }

    return Fullscreen;
}

module.exports = init();


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

/**
 * [description]
 *
 * @function Phaser.Geom.Rectangle.Perimeter
 * @since 3.0.0
 *
 * @param {Phaser.Geom.Rectangle} rect - [description]
 *
 * @return {number} [description]
 */
var Perimeter = function (rect)
{
    return 2 * (rect.width + rect.height);
};

module.exports = Perimeter;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var Class = require('../utils/Class');

var plugins = {};

/**
 * @classdesc
 * The PluginManager is global and belongs to the Game instance, not a Scene.
 * It handles the installation and removal of all global and Scene based plugins.
 * Plugins automatically register themselves with the PluginManager in their respective classes.
 *
 * @class PluginManager
 * @memberOf Phaser.Boot
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - [description]
 */
var PluginManager = new Class({

    initialize:

    function PluginManager (game)
    {
        /**
         * [description]
         *
         * @name Phaser.Boot.PluginManager#game
         * @type {Phaser.Game}
         * @since 3.0.0
         */
        this.game = game;

        game.events.once('boot', this.boot, this);
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.PluginManager#boot
     * @since 3.0.0
     */
    boot: function ()
    {
        this.game.events.once('destroy', this.destroy, this);
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.PluginManager#installGlobal
     * @since 3.0.0
     *
     * @param {Phaser.Scenes.Systems} sys - [description]
     * @param {array} globalPlugins - [description]
     */
    installGlobal: function (sys, globalPlugins)
    {
        var game = sys.game;
        var scene = sys.scene;
        var map = sys.settings.map;

        //  Reference the GlobalPlugins from Game into Scene.Systems
        for (var i = 0; i < globalPlugins.length; i++)
        {
            var pluginKey = globalPlugins[i];

            // console.log('PluginManager.global', pluginKey);

            if (game[pluginKey])
            {
                sys[pluginKey] = game[pluginKey];

                //  Scene level injection
                if (map.hasOwnProperty(pluginKey))
                {
                    scene[map[pluginKey]] = sys[pluginKey];
                }
            }
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.PluginManager#installLocal
     * @since 3.0.0
     *
     * @param {Phaser.Scenes.Systems} sys - [description]
     * @param {array} scenePlugins - [description]
     */
    installLocal: function (sys, scenePlugins)
    {
        var scene = sys.scene;
        var map = sys.settings.map;
        var isBooted = sys.settings.isBooted;

        for (var i = 0; i < scenePlugins.length; i++)
        {
            var pluginKey = scenePlugins[i];

            if (!plugins[pluginKey])
            {
                continue;
            }

            var source = plugins[pluginKey];

            var plugin = new source.plugin(scene);

            sys[source.mapping] = plugin;

            //  Scene level injection
            if (map.hasOwnProperty(source.mapping))
            {
                scene[map[source.mapping]] = plugin;
            }

            //  Scene is already booted, usually because this method is being called at run-time, so boot the plugin
            if (isBooted)
            {
                plugin.boot();
            }
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.PluginManager#remove
     * @since 3.0.0
     *
     * @param {string} key - [description]
     */
    remove: function (key)
    {
        delete plugins[key];
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.PluginManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.game = null;
    }

});

/**
 * Static method called directly by the Plugins
 * Key is a reference used to get the plugin from the plugins object (i.e. InputPlugin)
 * Plugin is the object to instantiate to create the plugin
 * Mapping is what the plugin is injected into the Scene.Systems as (i.e. input)
 *
 * @method PluginManager.register
 * @since 3.0.0
 *
 * @param {string} key - [description]
 * @param {object} plugin - [description]
 * @param {string} mapping - [description]
 */
PluginManager.register = function (key, plugin, mapping)
{
    plugins[key] = { plugin: plugin, mapping: mapping };
};

module.exports = PluginManager;


if (renderer.currentBlendMode !== src.blendMode)


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

/**
 * [description]
 *
 * @function Phaser.Math.RandomXYZW
 * @since 3.0.0
 *
 * @param {Phaser.Math.Vector4} vec4 - [description]
 * @param {float} [scale=1] - [description]
 *
 * @return {Phaser.Math.Vector4} [description]
 */
var RandomXYZW = function (vec4, scale)
{
    if (scale === undefined) { scale = 1; }

    // Not spherical; should fix this for more uniform distribution
    vec4.x = (Math.random() * 2 - 1) * scale;
    vec4.y = (Math.random() * 2 - 1) * scale;
    vec4.z = (Math.random() * 2 - 1) * scale;
    vec4.w = (Math.random() * 2 - 1) * scale;

    return vec4;
};

module.exports = RandomXYZW;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var GameObject = require('../../gameobjects/GameObject');

/**
 * Renders this Game Object with the WebGL Renderer to the given Camera.
 * The object will not render if any of its renderFlags are set or it is being actively filtered out by the Camera.
 * This method should not be called directly. It is a utility function of the Render module.
 *
 * @method Phaser.Tilemaps.DynamicTilemapLayer#renderWebGL
 * @since 3.0.0
 * @private
 *
 * @param {Phaser.Renderer.WebGLRenderer} renderer - A reference to the current active WebGL renderer.
 * @param {Phaser.Tilemaps.DynamicTilemapLayer} src - The Game Object being rendered in this call.
 * @param {number} interpolationPercentage - Reserved for future use and custom pipelines.
 * @param {Phaser.Cameras.Scene2D.Camera} camera - The Camera that is rendering the Game Object.
 */
var DynamicTilemapLayerWebGLRenderer = function (renderer, src, interpolationPercentage, camera)
{
    if (GameObject.RENDER_MASK !== src.renderFlags || (src.cameraFilter > 0 && (src.cameraFilter & camera._id)))
    {
        return;
    }

    src.cull(camera);

    this.pipeline.batchDynamicTilemapLayer(src, camera);
};

module.exports = DynamicTilemapLayerWebGLRenderer;


gl.texSubImage2D(0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, srcCanvas);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, srcCanvas);

keep keep keep replace replace replace replace replace keep keep keep

//if (!shouldReallocate && dstTexture.width >= srcCanvas.width || dstTexture.height >= srcCanvas.height)

//    gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, srcCanvas.width, srcCanvas.height, gl.RGBA, gl.UNSIGNED_BYTE, srcCanvas);


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var HSVToRGB = require('./HSVToRGB');

/**
 * Get HSV color wheel values in an array which will be 360 elements in size.
 *
 * @function Phaser.Display.Color.HSVColorWheel
 * @since 3.0.0
 *
 * @param {number} [s=1] - The saturation, in the range 0 - 1.
 * @param {number} [v=1] - The value, in the range 0 - 1.
 *
 * @return {array} An array containing 360 elements, where each contains a single numeric value corresponding to the color at that point in the HSV color wheel.
 */
var HSVColorWheel = function (s, v)
{
    if (s === undefined) { s = 1; }
    if (v === undefined) { v = 1; }

    var colors = [];

    for (var c = 0; c <= 359; c++)
    {
        colors.push(HSVToRGB(c / 359, s, v));
    }

    return colors;
};

module.exports = HSVColorWheel;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var GetTilesWithin = require('./GetTilesWithin');

/**
 * Sets a collision callback for the given rectangular area (in tile coordinates) within the layer.
 * If a callback is already set for the tile index it will be replaced. Set the callback to null to
 * remove it.
 *
 * @function Phaser.Tilemaps.Components.SetTileLocationCallback
 * @since 3.0.0
 *
 * @param {integer} [tileX=0] - [description]
 * @param {integer} [tileY=0] - [description]
 * @param {integer} [width=max width based on tileX] - [description]
 * @param {integer} [height=max height based on tileY] - [description]
 * @param {function} callback - The callback that will be invoked when the tile is collided with.
 * @param {object} callbackContext - The context under which the callback is called.
 * @param {Phaser.Tilemaps.LayerData} layer - The Tilemap Layer to act upon.
 */
var SetTileLocationCallback = function (tileX, tileY, width, height, callback, callbackContext, layer)
{
    var tiles = GetTilesWithin(tileX, tileY, width, height, null, layer);

    for (var i = 0; i < tiles.length; i++)
    {
        tiles[i].setCollisionCallback(callback, callbackContext);
    }

};

module.exports = SetTileLocationCallback;


var Matter = require('../../CustomMain');

/**
 * A coordinate wrapping plugin for matter.js.
 * See the readme for usage and examples.
 * @module MatterWrap
 */
var MatterWrap = {
  // plugin meta
  name: 'matter-wrap', // PLUGIN_NAME
  version: '0.1.4', // PLUGIN_VERSION
  for: 'matter-js@^0.13.1',
  silent: true, // no console log please

  // installs the plugin where `base` is `Matter`
  // you should not need to call this directly.
  install: function(base) {
    base.after('Engine.update', function() {
      MatterWrap.Engine.update(this);
    });
  },

  Engine: {
    /**
     * Updates the engine by wrapping bodies and composites inside `engine.world`.
     * This is called automatically by the plugin.
     * @function MatterWrap.Engine.update
     * @param {Matter.Engine} engine The engine to update.
     * @returns {void} No return value.
     */
    update: function(engine) {
      var world = engine.world,
        bodies = Matter.Composite.allBodies(world),
        composites = Matter.Composite.allComposites(world);

      for (var i = 0; i < bodies.length; i += 1) {
        var body = bodies[i];

        if (body.plugin.wrap) {
          MatterWrap.Body.wrap(body, body.plugin.wrap);
        }
      }

      for (i = 0; i < composites.length; i += 1) {
        var composite = composites[i];

        if (composite.plugin.wrap) {
          MatterWrap.Composite.wrap(composite, composite.plugin.wrap);
        }
      }
    }
  },

  Bounds: {
    /**
     * Returns a translation vector that wraps the `objectBounds` inside the `bounds`.
     * @function MatterWrap.Bounds.wrap
     * @param {Matter.Bounds} objectBounds The bounds of the object to wrap inside the bounds.
     * @param {Matter.Bounds} bounds The bounds to wrap the body inside.
     * @returns {?Matter.Vector} A translation vector (only if wrapping is required).
     */
    wrap: function(objectBounds, bounds) {
      var x = null,
        y = null;

      if (typeof bounds.min.x !== 'undefined' && typeof bounds.max.x !== 'undefined') {
        if (objectBounds.min.x > bounds.max.x) {
          x = bounds.min.x - objectBounds.max.x;
        } else if (objectBounds.max.x < bounds.min.x) {
          x = bounds.max.x - objectBounds.min.x;
        }
      }

      if (typeof bounds.min.y !== 'undefined' && typeof bounds.max.y !== 'undefined') {
        if (objectBounds.min.y > bounds.max.y) {
          y = bounds.min.y - objectBounds.max.y;
        } else if (objectBounds.max.y < bounds.min.y) {
          y = bounds.max.y - objectBounds.min.y;
        }
      }

      if (x !== null || y !== null) {
        return {
          x: x || 0,
          y: y || 0
        };
      }
    }
  },

  Body: {
    /**
     * Wraps the `body` position such that it always stays within the given bounds.
     * Upon crossing a boundary the body will appear on the opposite side of the bounds,
     * while maintaining its velocity.
     * This is called automatically by the plugin.
     * @function MatterWrap.Body.wrap
     * @param {Matter.Body} body The body to wrap.
     * @param {Matter.Bounds} bounds The bounds to wrap the body inside.
     * @returns {?Matter.Vector} The translation vector that was applied (only if wrapping was required).
     */
    wrap: function(body, bounds) {
      var translation = MatterWrap.Bounds.wrap(body.bounds, bounds);

      if (translation) {
        Matter.Body.translate(body, translation);
      }

      return translation;
    }
  },

  Composite: {
    /**
     * Returns the union of the bounds of all of the composite's bodies
     * (not accounting for constraints).
     * @function MatterWrap.Composite.bounds
     * @param {Matter.Composite} composite The composite.
     * @returns {Matter.Bounds} The composite bounds.
     */
    bounds: function(composite) {
      var bodies = Matter.Composite.allBodies(composite),
        vertices = [];

      for (var i = 0; i < bodies.length; i += 1) {
        var body = bodies[i];
        vertices.push(body.bounds.min, body.bounds.max);
      }

      return Matter.Bounds.create(vertices);
    },

    /**
     * Wraps the `composite` position such that it always stays within the given bounds.
     * Upon crossing a boundary the composite will appear on the opposite side of the bounds,
     * while maintaining its velocity.
     * This is called automatically by the plugin.
     * @function MatterWrap.Composite.wrap
     * @param {Matter.Composite} composite The composite to wrap.
     * @param {Matter.Bounds} bounds The bounds to wrap the composite inside.
     * @returns {?Matter.Vector} The translation vector that was applied (only if wrapping was required).
     */
    wrap: function(composite, bounds) {
      var translation = MatterWrap.Bounds.wrap(
        MatterWrap.Composite.bounds(composite),
        bounds
      );

      if (translation) {
        Matter.Composite.translate(composite, translation);
      }

      return translation;
    }
  }
};

module.exports = MatterWrap;

/**
 * @namespace Matter.Body
 * @see http://brm.io/matter-js/docs/classes/Body.html
 */

/**
 * This plugin adds a new property `body.plugin.wrap` to instances of `Matter.Body`.
 * This is a `Matter.Bounds` instance that specifies the wrapping region.
 * @property {Matter.Bounds} body.plugin.wrap
 * @memberof Matter.Body
 */

/**
 * This plugin adds a new property `composite.plugin.wrap` to instances of `Matter.Composite`.
 * This is a `Matter.Bounds` instance that specifies the wrapping region.
 * @property {Matter.Bounds} composite.plugin.wrap
 * @memberof Matter.Composite
 */


describe("Sanity checks for infinity", function () {

it("Throws error on map center at plus Infinity longitude", function () {

}).to.throwError('Attempted to load an infinite number of tiles');

import {Layer} from '../Layer';
import * as Browser from '../../core/Browser';
import * as Util from '../../core/Util';
import * as DomUtil from '../../dom/DomUtil';
import {Point} from '../../geometry/Point';
import {Bounds} from '../../geometry/Bounds';
import {LatLngBounds, toLatLngBounds as latLngBounds} from '../../geo/LatLngBounds';

/*
 * @class GridLayer
 * @inherits Layer
 * @aka L.GridLayer
 *
 * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
 * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you.
 *
 *
 * @section Synchronous usage
 * @example
 *
 * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile.
 *
 * ```js
 * var CanvasLayer = L.GridLayer.extend({
 *     createTile: function(coords){
 *         // create a <canvas> element for drawing
 *         var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 *
 *         // setup tile width and height according to the options
 *         var size = this.getTileSize();
 *         tile.width = size.x;
 *         tile.height = size.y;
 *
 *         // get a canvas context and draw something on it using coords.x, coords.y and coords.z
 *         var ctx = tile.getContext('2d');
 *
 *         // return the tile so it can be rendered on screen
 *         return tile;
 *     }
 * });
 * ```
 *
 * @section Asynchronous usage
 * @example
 *
 * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback.
 *
 * ```js
 * var CanvasLayer = L.GridLayer.extend({
 *     createTile: function(coords, done){
 *         var error;
 *
 *         // create a <canvas> element for drawing
 *         var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 *
 *         // setup tile width and height according to the options
 *         var size = this.getTileSize();
 *         tile.width = size.x;
 *         tile.height = size.y;
 *
 *         // draw something asynchronously and pass the tile to the done() callback
 *         setTimeout(function() {
 *             done(error, tile);
 *         }, 1000);
 *
 *         return tile;
 *     }
 * });
 * ```
 *
 * @section
 */

export var GridLayer = Layer.extend({

	// @section
	// @aka GridLayer options
	options: {
		// @option tileSize: Number|Point = 256
		// Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
		tileSize: 256,

		// @option opacity: Number = 1.0
		// Opacity of the tiles. Can be used in the `createTile()` function.
		opacity: 1,

		// @option updateWhenIdle: Boolean = depends
		// If `false`, new tiles are loaded during panning, otherwise only after it (for better performance). `true` by default on mobile browsers, otherwise `false`.
		updateWhenIdle: Browser.mobile,

		// @option updateWhenZooming: Boolean = true
		// By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends.
		updateWhenZooming: true,

		// @option updateInterval: Number = 200
		// Tiles will not update more than once every `updateInterval` milliseconds when panning.
		updateInterval: 200,

		// @option zIndex: Number = 1
		// The explicit zIndex of the tile layer.
		zIndex: 1,

		// @option bounds: LatLngBounds = undefined
		// If set, tiles will only be loaded inside the set `LatLngBounds`.
		bounds: null,

		// @option minZoom: Number = 0
		// The minimum zoom level that tiles will be loaded at. By default the entire map.
		minZoom: 0,

		// @option maxZoom: Number = undefined
		// The maximum zoom level that tiles will be loaded at.
		maxZoom: undefined,

		// @option maxNativeZoom: Number = undefined
		// Maximum zoom number the tile source has available. If it is specified,
		// the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
		// from `maxNativeZoom` level and auto-scaled.
		maxNativeZoom: undefined,

		// @option minNativeZoom: Number = undefined
		// Minimum zoom number the tile source has available. If it is specified,
		// the tiles on all zoom levels lower than `minNativeZoom` will be loaded
		// from `minNativeZoom` level and auto-scaled.
		minNativeZoom: undefined,

		// @option noWrap: Boolean = false
		// Whether the layer is wrapped around the antimeridian. If `true`, the
		// GridLayer will only be displayed once at low zoom levels. Has no
		// effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
		// in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
		// tiles outside the CRS limits.
		noWrap: false,

		// @option pane: String = 'tilePane'
		// `Map pane` where the grid layer will be added.
		pane: 'tilePane',

		// @option className: String = ''
		// A custom class name to assign to the tile layer. Empty by default.
		className: '',

		// @option keepBuffer: Number = 2
		// When panning the map, keep this many rows and columns of tiles before unloading them.
		keepBuffer: 2
	},

	initialize: function (options) {
		Util.setOptions(this, options);
	},

	onAdd: function () {
		this._initContainer();

		this._levels = {};
		this._tiles = {};

		this._resetView();
		this._update();
	},

	beforeAdd: function (map) {
		map._addZoomLimit(this);
	},

	onRemove: function (map) {
		this._removeAllTiles();
		DomUtil.remove(this._container);
		map._removeZoomLimit(this);
		this._container = null;
		this._tileZoom = null;
	},

	// @method bringToFront: this
	// Brings the tile layer to the top of all tile layers.
	bringToFront: function () {
		if (this._map) {
			DomUtil.toFront(this._container);
			this._setAutoZIndex(Math.max);
		}
		return this;
	},

	// @method bringToBack: this
	// Brings the tile layer to the bottom of all tile layers.
	bringToBack: function () {
		if (this._map) {
			DomUtil.toBack(this._container);
			this._setAutoZIndex(Math.min);
		}
		return this;
	},

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

	// @method setOpacity(opacity: Number): this
	// Changes the [opacity](#gridlayer-opacity) of the grid layer.
	setOpacity: function (opacity) {
		this.options.opacity = opacity;
		this._updateOpacity();
		return this;
	},

	// @method setZIndex(zIndex: Number): this
	// Changes the [zIndex](#gridlayer-zindex) of the grid layer.
	setZIndex: function (zIndex) {
		this.options.zIndex = zIndex;
		this._updateZIndex();

		return this;
	},

	// @method isLoading: Boolean
	// Returns `true` if any tile in the grid layer has not finished loading.
	isLoading: function () {
		return this._loading;
	},

	// @method redraw: this
	// Causes the layer to clear all the tiles and request them again.
	redraw: function () {
		if (this._map) {
			this._removeAllTiles();
			this._update();
		}
		return this;
	},

	getEvents: function () {
		var events = {
			viewprereset: this._invalidateAll,
			viewreset: this._resetView,
			zoom: this._resetView,
			moveend: this._onMoveEnd
		};

		if (!this.options.updateWhenIdle) {
			// update tiles on move, but not more often than once per given interval
			if (!this._onMove) {
				this._onMove = Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
			}

			events.move = this._onMove;
		}

		if (this._zoomAnimated) {
			events.zoomanim = this._animateZoom;
		}

		return events;
	},

	// @section Extension methods
	// Layers extending `GridLayer` shall reimplement the following method.
	// @method createTile(coords: Object, done?: Function): HTMLElement
	// Called only internally, must be overriden by classes extending `GridLayer`.
	// Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
	// is specified, it must be called when the tile has finished loading and drawing.
	createTile: function () {
		return document.createElement('div');
	},

	// @section
	// @method getTileSize: Point
	// Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
	getTileSize: function () {
		var s = this.options.tileSize;
		return s instanceof Point ? s : new Point(s, s);
	},

	_updateZIndex: function () {
		if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
			this._container.style.zIndex = this.options.zIndex;
		}
	},

	_setAutoZIndex: function (compare) {
		// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)

		var layers = this.getPane().children,
		    edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min

		for (var i = 0, len = layers.length, zIndex; i < len; i++) {

			zIndex = layers[i].style.zIndex;

			if (layers[i] !== this._container && zIndex) {
				edgeZIndex = compare(edgeZIndex, +zIndex);
			}
		}

		if (isFinite(edgeZIndex)) {
			this.options.zIndex = edgeZIndex + compare(-1, 1);
			this._updateZIndex();
		}
	},

	_updateOpacity: function () {
		if (!this._map) { return; }

		// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
		if (Browser.ielt9) { return; }

		DomUtil.setOpacity(this._container, this.options.opacity);

		var now = +new Date(),
		    nextFrame = false,
		    willPrune = false;

		for (var key in this._tiles) {
			var tile = this._tiles[key];
			if (!tile.current || !tile.loaded) { continue; }

			var fade = Math.min(1, (now - tile.loaded) / 200);

			DomUtil.setOpacity(tile.el, fade);
			if (fade < 1) {
				nextFrame = true;
			} else {
				if (tile.active) { willPrune = true; }
				tile.active = true;
			}
		}

		if (willPrune && !this._noPrune) { this._pruneTiles(); }

		if (nextFrame) {
			Util.cancelAnimFrame(this._fadeFrame);
			this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
		}
	},

	_initContainer: function () {
		if (this._container) { return; }

		this._container = DomUtil.create('div', 'leaflet-layer ' + (this.options.className || ''));
		this._updateZIndex();

		if (this.options.opacity < 1) {
			this._updateOpacity();
		}

		this.getPane().appendChild(this._container);
	},

	_updateLevels: function () {

		var zoom = this._tileZoom,
		    maxZoom = this.options.maxZoom;

		if (zoom === undefined) { return undefined; }

		for (var z in this._levels) {
			if (this._levels[z].el.children.length || z === zoom) {
				this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
			} else {
				DomUtil.remove(this._levels[z].el);
				this._removeTilesAtZoom(z);
				delete this._levels[z];
			}
		}

		var level = this._levels[zoom],
		    map = this._map;

		if (!level) {
			level = this._levels[zoom] = {};

			level.el = DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
			level.el.style.zIndex = maxZoom;

			level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
			level.zoom = zoom;

			this._setZoomTransform(level, map.getCenter(), map.getZoom());

			// force the browser to consider the newly added element for transition
			Util.falseFn(level.el.offsetWidth);
		}

		this._level = level;

		return level;
	},

	_pruneTiles: function () {
		if (!this._map) {
			return;
		}

		var key, tile;

		var zoom = this._map.getZoom();
		if (zoom > this.options.maxZoom ||
			zoom < this.options.minZoom) {
			this._removeAllTiles();
			return;
		}

		for (key in this._tiles) {
			tile = this._tiles[key];
			tile.retain = tile.current;
		}

		for (key in this._tiles) {
			tile = this._tiles[key];
			if (tile.current && !tile.active) {
				var coords = tile.coords;
				if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
					this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
				}
			}
		}

		for (key in this._tiles) {
			if (!this._tiles[key].retain) {
				this._removeTile(key);
			}
		}
	},

	_removeTilesAtZoom: function (zoom) {
		for (var key in this._tiles) {
			if (this._tiles[key].coords.z !== zoom) {
				continue;
			}
			this._removeTile(key);
		}
	},

	_removeAllTiles: function () {
		for (var key in this._tiles) {
			this._removeTile(key);
		}
	},

	_invalidateAll: function () {
		for (var z in this._levels) {
			DomUtil.remove(this._levels[z].el);
			delete this._levels[z];
		}
		this._removeAllTiles();

		this._tileZoom = null;
	},

	_retainParent: function (x, y, z, minZoom) {
		var x2 = Math.floor(x / 2),
		    y2 = Math.floor(y / 2),
		    z2 = z - 1,
		    coords2 = new Point(+x2, +y2);
		coords2.z = +z2;

		var key = this._tileCoordsToKey(coords2),
		    tile = this._tiles[key];

		if (tile && tile.active) {
			tile.retain = true;
			return true;

		} else if (tile && tile.loaded) {
			tile.retain = true;
		}

		if (z2 > minZoom) {
			return this._retainParent(x2, y2, z2, minZoom);
		}

		return false;
	},

	_retainChildren: function (x, y, z, maxZoom) {

		for (var i = 2 * x; i < 2 * x + 2; i++) {
			for (var j = 2 * y; j < 2 * y + 2; j++) {

				var coords = new Point(i, j);
				coords.z = z + 1;

				var key = this._tileCoordsToKey(coords),
				    tile = this._tiles[key];

				if (tile && tile.active) {
					tile.retain = true;
					continue;

				} else if (tile && tile.loaded) {
					tile.retain = true;
				}

				if (z + 1 < maxZoom) {
					this._retainChildren(i, j, z + 1, maxZoom);
				}
			}
		}
	},

	_resetView: function (e) {
		var animating = e && (e.pinch || e.flyTo);
		this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
	},

	_animateZoom: function (e) {
		this._setView(e.center, e.zoom, true, e.noUpdate);
	},

	_clampZoom: function (zoom) {
		var options = this.options;

		if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) {
			return options.minNativeZoom;
		}

		if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) {
			return options.maxNativeZoom;
		}

		return zoom;
	},

	_setView: function (center, zoom, noPrune, noUpdate) {
		var tileZoom = this._clampZoom(Math.round(zoom));
		if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
		    (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
			tileZoom = undefined;
		}

		var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);

		if (!noUpdate || tileZoomChanged) {

			this._tileZoom = tileZoom;

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

			this._updateLevels();
			this._resetGrid();

			if (tileZoom !== undefined) {
				this._update(center);
			}

			if (!noPrune) {
				this._pruneTiles();
			}

			// Flag to prevent _updateOpacity from pruning tiles during
			// a zoom anim or a pinch gesture
			this._noPrune = !!noPrune;
		}

		this._setZoomTransforms(center, zoom);
	},

	_setZoomTransforms: function (center, zoom) {
		for (var i in this._levels) {
			this._setZoomTransform(this._levels[i], center, zoom);
		}
	},

	_setZoomTransform: function (level, center, zoom) {
		var scale = this._map.getZoomScale(zoom, level.zoom),
		    translate = level.origin.multiplyBy(scale)
		        .subtract(this._map._getNewPixelOrigin(center, zoom)).round();

		if (Browser.any3d) {
			DomUtil.setTransform(level.el, translate, scale);
		} else {
			DomUtil.setPosition(level.el, translate);
		}
	},

	_resetGrid: function () {
		var map = this._map,
		    crs = map.options.crs,
		    tileSize = this._tileSize = this.getTileSize(),
		    tileZoom = this._tileZoom;

		var bounds = this._map.getPixelWorldBounds(this._tileZoom);
		if (bounds) {
			this._globalTileRange = this._pxBoundsToTileRange(bounds);
		}

		this._wrapX = crs.wrapLng && !this.options.noWrap && [
			Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
			Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
		];
		this._wrapY = crs.wrapLat && !this.options.noWrap && [
			Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
			Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
		];
	},

	_onMoveEnd: function () {
		if (!this._map || this._map._animatingZoom) { return; }

		this._update();
	},

	_getTiledPixelBounds: function (center) {
		var map = this._map,
		    mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
		    scale = map.getZoomScale(mapZoom, this._tileZoom),
		    pixelCenter = map.project(center, this._tileZoom).floor(),
		    halfSize = map.getSize().divideBy(scale * 2);

		return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
	},

	// Private method to load tiles in the grid's active zoom level according to map bounds
	_update: function (center) {
		var map = this._map;
		if (!map) { return; }
		var zoom = this._clampZoom(map.getZoom());

		if (center === undefined) { center = map.getCenter(); }
		if (this._tileZoom === undefined) { return; }	// if out of minzoom/maxzoom

		var pixelBounds = this._getTiledPixelBounds(center),
		    tileRange = this._pxBoundsToTileRange(pixelBounds),
		    tileCenter = tileRange.getCenter(),
		    queue = [],
		    margin = this.options.keepBuffer,
		    noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
		                              tileRange.getTopRight().add([margin, -margin]));

		// Sanity check: panic if the tile range contains Infinity somewhere.
		if (tileRange.min.x === Infinity ||
		    tileRange.min.y === Infinity ||
		    tileRange.max.x === Infinity ||
		    tileRange.max.y === Infinity) { throw new Error('Attempted to load an infinite number of tiles'); }

		for (var key in this._tiles) {
			var c = this._tiles[key].coords;
			if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) {
				this._tiles[key].current = false;
			}
		}

		// _update just loads more tiles. If the tile zoom level differs too much
		// from the map's, let _setView reset levels and prune old tiles.
		if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }

		// create a queue of coordinates to load tiles from
		for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
			for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
				var coords = new Point(i, j);
				coords.z = this._tileZoom;

				if (!this._isValidTile(coords)) { continue; }

				if (!this._tiles[this._tileCoordsToKey(coords)]) {
					queue.push(coords);
				}
			}
		}

		// sort tile queue to load tiles in order of their distance to center
		queue.sort(function (a, b) {
			return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
		});

		if (queue.length !== 0) {
			// if it's the first batch of tiles to load
			if (!this._loading) {
				this._loading = true;
				// @event loading: Event
				// Fired when the grid layer starts loading tiles.
				this.fire('loading');
			}

			// create DOM fragment to append tiles in one batch
			var fragment = document.createDocumentFragment();

			for (i = 0; i < queue.length; i++) {
				this._addTile(queue[i], fragment);
			}

			this._level.el.appendChild(fragment);
		}
	},

	_isValidTile: function (coords) {
		var crs = this._map.options.crs;

		if (!crs.infinite) {
			// don't load tile if it's out of bounds and not wrapped
			var bounds = this._globalTileRange;
			if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
			    (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
		}

		if (!this.options.bounds) { return true; }

		// don't load tile if it doesn't intersect the bounds in options
		var tileBounds = this._tileCoordsToBounds(coords);
		return latLngBounds(this.options.bounds).overlaps(tileBounds);
	},

	_keyToBounds: function (key) {
		return this._tileCoordsToBounds(this._keyToTileCoords(key));
	},

	// converts tile coordinates to its geographical bounds
	_tileCoordsToBounds: function (coords) {

		var map = this._map,
		    tileSize = this.getTileSize(),

		    nwPoint = coords.scaleBy(tileSize),
		    sePoint = nwPoint.add(tileSize),

		    nw = map.unproject(nwPoint, coords.z),
		    se = map.unproject(sePoint, coords.z),
		    bounds = new LatLngBounds(nw, se);

		if (!this.options.noWrap) {
			map.wrapLatLngBounds(bounds);
		}

		return bounds;
	},

	// converts tile coordinates to key for the tile cache
	_tileCoordsToKey: function (coords) {
		return coords.x + ':' + coords.y + ':' + coords.z;
	},

	// converts tile cache key to coordinates
	_keyToTileCoords: function (key) {
		var k = key.split(':'),
		    coords = new Point(+k[0], +k[1]);
		coords.z = +k[2];
		return coords;
	},

	_removeTile: function (key) {
		var tile = this._tiles[key];
		if (!tile) { return; }

		DomUtil.remove(tile.el);

		delete this._tiles[key];

		// @event tileunload: TileEvent
		// Fired when a tile is removed (e.g. when a tile goes off the screen).
		this.fire('tileunload', {
			tile: tile.el,
			coords: this._keyToTileCoords(key)
		});
	},

	_initTile: function (tile) {
		DomUtil.addClass(tile, 'leaflet-tile');

		var tileSize = this.getTileSize();
		tile.style.width = tileSize.x + 'px';
		tile.style.height = tileSize.y + 'px';

		tile.onselectstart = Util.falseFn;
		tile.onmousemove = Util.falseFn;

		// update opacity on tiles in IE7-8 because of filter inheritance problems
		if (Browser.ielt9 && this.options.opacity < 1) {
			DomUtil.setOpacity(tile, this.options.opacity);
		}

		// without this hack, tiles disappear after zoom on Chrome for Android
		// https://github.com/Leaflet/Leaflet/issues/2078
		if (Browser.android && !Browser.android23) {
			tile.style.WebkitBackfaceVisibility = 'hidden';
		}
	},

	_addTile: function (coords, container) {
		var tilePos = this._getTilePos(coords),
		    key = this._tileCoordsToKey(coords);

		var tile = this.createTile(this._wrapCoords(coords), Util.bind(this._tileReady, this, coords));

		this._initTile(tile);

		// if createTile is defined with a second argument ("done" callback),
		// we know that tile is async and will be ready later; otherwise
		if (this.createTile.length < 2) {
			// mark tile as ready, but delay one frame for opacity animation to happen
			Util.requestAnimFrame(Util.bind(this._tileReady, this, coords, null, tile));
		}

		DomUtil.setPosition(tile, tilePos);

		// save tile in cache
		this._tiles[key] = {
			el: tile,
			coords: coords,
			current: true
		};

		container.appendChild(tile);
		// @event tileloadstart: TileEvent
		// Fired when a tile is requested and starts loading.
		this.fire('tileloadstart', {
			tile: tile,
			coords: coords
		});
	},

	_tileReady: function (coords, err, tile) {
		if (!this._map) { return; }

		if (err) {
			// @event tileerror: TileErrorEvent
			// Fired when there is an error loading a tile.
			this.fire('tileerror', {
				error: err,
				tile: tile,
				coords: coords
			});
		}

		var key = this._tileCoordsToKey(coords);

		tile = this._tiles[key];
		if (!tile) { return; }

		tile.loaded = +new Date();
		if (this._map._fadeAnimated) {
			DomUtil.setOpacity(tile.el, 0);
			Util.cancelAnimFrame(this._fadeFrame);
			this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
		} else {
			tile.active = true;
			this._pruneTiles();
		}

		if (!err) {
			DomUtil.addClass(tile.el, 'leaflet-tile-loaded');

			// @event tileload: TileEvent
			// Fired when a tile loads.
			this.fire('tileload', {
				tile: tile.el,
				coords: coords
			});
		}

		if (this._noTilesToLoad()) {
			this._loading = false;
			// @event load: Event
			// Fired when the grid layer loaded all visible tiles.
			this.fire('load');

			if (Browser.ielt9 || !this._map._fadeAnimated) {
				Util.requestAnimFrame(this._pruneTiles, this);
			} else {
				// Wait a bit more than 0.2 secs (the duration of the tile fade-in)
				// to trigger a pruning.
				setTimeout(Util.bind(this._pruneTiles, this), 250);
			}
		}
	},

	_getTilePos: function (coords) {
		return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
	},

	_wrapCoords: function (coords) {
		var newCoords = new Point(
			this._wrapX ? Util.wrapNum(coords.x, this._wrapX) : coords.x,
			this._wrapY ? Util.wrapNum(coords.y, this._wrapY) : coords.y);
		newCoords.z = coords.z;
		return newCoords;
	},

	_pxBoundsToTileRange: function (bounds) {
		var tileSize = this.getTileSize();
		return new Bounds(
			bounds.min.unscaleBy(tileSize).floor(),
			bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
	},

	_noTilesToLoad: function () {
		for (var key in this._tiles) {
			if (!this._tiles[key].loaded) { return false; }
		}
		return true;
	}
});

// @factory L.gridLayer(options?: GridLayer options)
// Creates a new instance of GridLayer with the supplied options.
export function gridLayer(options) {
	return new GridLayer(options);
}


<!DOCTYPE html>
<html>
<head>
<meta name="viewport" id="vp" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width" />

    <link rel="stylesheet" href="../../dist/leaflet.css" />
    <script src="../leaflet-include.js"></script>

  <meta charset="utf-8">
  <title>Leaflet test for event adding/firing/removing performance</title>
</head>
<body>
  <script>

  function benchmark(name, fn, setup) {
    //warmup
    for (var i = 0; i < 100; i++) {
      if (setup) {
        setup();
      }
      fn();
    }

    var t = 0;
    for (var i = 0; i < 100; i++) {
      if (setup) {
        setup();
      }
      var t0 = performance.now();
      fn();
      t += performance.now() - t0;
    }
    console.log(name, (t / 100) + ' ms/run');
  }

  var fn = function (e) {called += e.count;},
    fns = [],
    called = 0;
  for (var p = 0; p < 1000; p++) {
    fns.push((function(p) { return function(e) { called += p;}; }));
  }
  benchmark('Adding same fn', function() {
    var obj = new L.Evented();
    var t0 = performance.now();
    for (var p = 0; p < 1000; p++) {
      obj.on('test', fn);
    }
  });

  benchmark('Adding different fn', function() {
    var obj = new L.Evented();
    var fn = function () {};
    var t0 = performance.now();
    for (var p = 0; p < 1000; p++) {
      obj.on('test', fns[p]);
    }
  });

  var obj = new L.Evented();
  var fn = function () {};
  var t0 = performance.now();
  for (var p = 0; p < 1000; p++) {
    obj.on('test', (function(p) { return function() { var x = 2 + p; }})(p));
  }
  benchmark('Fire', function() {
    for (var p = 0; p < 1000; p++) {
      obj.fire('test');
    }
  });

  benchmark('Off', function() {
    var obj = new L.Evented();
    var fn = function () {};
    var t0 = performance.now();
    for (var p = 0; p < 1000; p++) {
      obj.on('test', fns[p]);
    }
  }, function() {
    fns = [];
    var obj = new L.Evented();
    var t0 = performance.now();
    for (var p = 0; p < 1000; p++) {
      var fn = (function(p) { return function() { var x = 2 + p; }})(p);
      fns.push(fn);
      obj.on('test', fn);
    }
  });

  </script>
</body>
</html>


<!DOCTYPE html>

	{% capture title %}{% if page.title %}{{ page.title }} - {% elsif post.title %}{{ post.title }} - {% endif %}{% endcapture %}
	<title>{{ title }}Leaflet</title>

	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	{% capture root %}{% if page.root %}{{ page.root }}{% else %}{{ layout.root }}{% endif %}{% endcapture %}
	<link rel="shortcut icon" type="image/x-icon" href="{{ root }}docs/images/favicon.ico" />

    <link rel="stylesheet" href="https://unpkg.com/leaflet@{{ site.latest_leaflet_version}}/dist/leaflet.css" integrity="{{site.integrity_hash_css}}" crossorigin=""/>

{% unless page.customMapContainer == "true" %}

{% endunless %}
	{% if page.css %}{% endif %}

{% unless page.customMapContainer == "true" %}

{% endunless %}

{{ content }}


import {Layer} from './Layer';
import * as Util from '../core/Util';

/*
 * @class LayerGroup
 * @aka L.LayerGroup
 * @inherits Layer
 *
 * Used to group several layers and handle them as one. If you add it to the map,
 * any layers added or removed from the group will be added/removed on the map as
 * well. Extends `Layer`.
 *
 * @example
 *
 * ```js
 * L.layerGroup([marker1, marker2])
 * 	.addLayer(polyline)
 * 	.addTo(map);
 * ```
 */

export var LayerGroup = Layer.extend({

	initialize: function (layers) {
		this._layers = {};

		var i, len;

		if (layers) {
			for (i = 0, len = layers.length; i < len; i++) {
				this.addLayer(layers[i]);
			}
		}
	},

	// @method addLayer(layer: Layer): this
	// Adds the given layer to the group.
	addLayer: function (layer) {
		var id = this.getLayerId(layer);

		this._layers[id] = layer;

		if (this._map) {
			this._map.addLayer(layer);
		}

		return this;
	},

	// @method removeLayer(layer: Layer): this
	// Removes the given layer from the group.
	// @alternative
	// @method removeLayer(id: Number): this
	// Removes the layer with the given internal ID from the group.
	removeLayer: function (layer) {
		var id = layer in this._layers ? layer : this.getLayerId(layer);

		if (this._map && this._layers[id]) {
			this._map.removeLayer(this._layers[id]);
		}

		delete this._layers[id];

		return this;
	},

	// @method hasLayer(layer: Layer): Boolean
	// Returns `true` if the given layer is currently added to the group.
	// @alternative
	// @method hasLayer(id: Number): Boolean
	// Returns `true` if the given internal ID is currently added to the group.
	hasLayer: function (layer) {
		return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers);
	},

	// @method clearLayers(): this
	// Removes all the layers from the group.
	clearLayers: function () {
		for (var i in this._layers) {
			this.removeLayer(this._layers[i]);
		}
		return this;
	},

	// @method invoke(methodName: String, …): this
	// Calls `methodName` on every layer contained in this group, passing any
	// additional parameters. Has no effect if the layers contained do not
	// implement `methodName`.
	invoke: function (methodName) {
		var args = Array.prototype.slice.call(arguments, 1),
		    i, layer;

		for (i in this._layers) {
			layer = this._layers[i];

			if (layer[methodName]) {
				layer[methodName].apply(layer, args);
			}
		}

		return this;
	},

	onAdd: function (map) {
		for (var i in this._layers) {
			map.addLayer(this._layers[i]);
		}
	},

	onRemove: function (map) {
		for (var i in this._layers) {
			map.removeLayer(this._layers[i]);
		}
	},

	// @method eachLayer(fn: Function, context?: Object): this
	// Iterates over the layers of the group, optionally specifying context of the iterator function.
	// ```js
	// group.eachLayer(function (layer) {
	// 	layer.bindPopup('Hello');
	// });
	// ```
	eachLayer: function (method, context) {
		for (var i in this._layers) {
			method.call(context, this._layers[i]);
		}
		return this;
	},

	// @method getLayer(id: Number): Layer
	// Returns the layer with the given internal ID.
	getLayer: function (id) {
		return this._layers[id];
	},

	// @method getLayers(): Layer[]
	// Returns an array of all the layers added to the group.
	getLayers: function () {
		var layers = [];

		for (var i in this._layers) {
			layers.push(this._layers[i]);
		}
		return layers;
	},

	// @method setZIndex(zIndex: Number): this
	// Calls `setZIndex` on every layer contained in this group, passing the z-index.
	setZIndex: function (zIndex) {
		return this.invoke('setZIndex', zIndex);
	},

	// @method getLayerId(layer: Layer): Number
	// Returns the internal ID for a layer
	getLayerId: function (layer) {
		return Util.stamp(layer);
	}
});

// @factory L.layerGroup(layers: Layer[])
// Create a layer group, optionally given an initial set of layers.
export var layerGroup = function (layers) {
	return new LayerGroup(layers);
};