<commit_before>import BaseTexture from './BaseTexture';
import VideoBaseTexture from './VideoBaseTexture';
import TextureUvs from './TextureUvs';
import EventEmitter from 'eventemitter3';
import { Rectangle } from '../math';
import { TextureCache, BaseTextureCache } from '../utils';

/**
 * A texture stores the information that represents an image or part of an image. It cannot be added
 * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided
 * then the whole image is used.
 *
 * You can directly create a texture from an image and then reuse it multiple times like this :
 *
 * ```js
 * let texture = PIXI.Texture.fromImage('assets/image.png');
 * let sprite1 = new PIXI.Sprite(texture);
 * let sprite2 = new PIXI.Sprite(texture);
 * ```
 *
 * Textures made from SVGs, loaded or not, cannot be used before the file finishes processing.
 * You can check for this by checking the sprite's _textureID property.
 * ```js
 * var texture = PIXI.Texture.fromImage('assets/image.svg');
 * var sprite1 = new PIXI.Sprite(texture);
 * //sprite1._textureID should not be undefined if the texture has finished processing the SVG file
 * ```
 * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068.
 *
 * @class
 * @extends EventEmitter
 * @memberof PIXI
 */
export default class Texture extends EventEmitter
{
    /**
     * @param {PIXI.BaseTexture} baseTexture - The base texture source to create the texture from
     * @param {PIXI.Rectangle} [frame] - The rectangle frame of the texture to show
     * @param {PIXI.Rectangle} [orig] - The area of original texture
     * @param {PIXI.Rectangle} [trim] - Trimmed rectangle of original texture
     * @param {number} [rotate] - indicates how the texture was rotated by texture packer. See {@link PIXI.GroupD8}
     */
    constructor(baseTexture, frame, orig, trim, rotate)
    {
        super();

        /**
         * Does this Texture have any frame data assigned to it?
         *
         * @member {boolean}
         */
        this.noFrame = false;

        if (!frame)
        {
            this.noFrame = true;
            frame = new Rectangle(0, 0, 1, 1);
        }

        if (baseTexture instanceof Texture)
        {
            baseTexture = baseTexture.baseTexture;
        }

        /**
         * The base texture that this texture uses.
         *
         * @member {PIXI.BaseTexture}
         */
        this.baseTexture = baseTexture;

        /**
         * This is the area of the BaseTexture image to actually copy to the Canvas / WebGL when rendering,
         * irrespective of the actual frame size or placement (which can be influenced by trimmed texture atlases)
         *
         * @member {PIXI.Rectangle}
         */
        this._frame = frame;

        /**
         * This is the trimmed area of original texture, before it was put in atlas
         *
         * @member {PIXI.Rectangle}
         */
        this.trim = trim;

        /**
         * This will let the renderer know if the texture is valid. If it's not then it cannot be rendered.
         *
         * @member {boolean}
         */
        this.valid = false;

        /**
         * This will let a renderer know that a texture has been updated (used mainly for webGL uv updates)
         *
         * @member {boolean}
         */
        this.requiresUpdate = false;

        /**
         * The WebGL UV data cache.
         *
         * @member {PIXI.TextureUvs}
         * @private
         */
        this._uvs = null;

        /**
         * This is the area of original texture, before it was put in atlas
         *
         * @member {PIXI.Rectangle}
         */
        this.orig = orig || frame;// new Rectangle(0, 0, 1, 1);

        this._rotate = Number(rotate || 0);

        if (rotate === true)
        {
            // this is old texturepacker legacy, some games/libraries are passing "true" for rotated textures
            this._rotate = 2;
        }
        else if (this._rotate % 2 !== 0)
        {
            throw new Error('attempt to use diamond-shaped UVs. If you are sure, set rotation manually');
        }

        if (baseTexture.hasLoaded)
        {
            if (this.noFrame)
            {
                frame = new Rectangle(0, 0, baseTexture.width, baseTexture.height);

                // if there is no frame we should monitor for any base texture changes..
                baseTexture.on('update', this.onBaseTextureUpdated, this);
            }
            this.frame = frame;
        }
        else
        {
            baseTexture.once('loaded', this.onBaseTextureLoaded, this);
        }

        /**
         * Fired when the texture is updated. This happens if the frame or the baseTexture is updated.
         *
         * @event update
         * @memberof PIXI.Texture#
         * @protected
         */

        this._updateID = 0;

        /**
         * Extra field for extra plugins. May contain clamp settings and some matrices
         * @type {Object}
         */
        this.transform = null;
    }

    /**
     * Updates this texture on the gpu.
     *
     */
    update()
    {
        this.baseTexture.update();
    }

    /**
     * Called when the base texture is loaded
     *
     * @private
     * @param {PIXI.BaseTexture} baseTexture - The base texture.
     */
    onBaseTextureLoaded(baseTexture)
    {
        this._updateID++;

        // TODO this code looks confusing.. boo to abusing getters and setterss!
        if (this.noFrame)
        {
            this.frame = new Rectangle(0, 0, baseTexture.width, baseTexture.height);
        }
        else
        {
            this.frame = this._frame;
        }

        this.baseTexture.on('update', this.onBaseTextureUpdated, this);
        this.emit('update', this);
    }

    /**
     * Called when the base texture is updated
     *
     * @private
     * @param {PIXI.BaseTexture} baseTexture - The base texture.
     */
    onBaseTextureUpdated(baseTexture)
    {
        this._updateID++;

        this._frame.width = baseTexture.width;
        this._frame.height = baseTexture.height;

        this.emit('update', this);
    }

    /**
     * Destroys this texture
     *
     * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well
     */
    destroy(destroyBase)
    {
        if (this.baseTexture)
        {
            if (destroyBase)
            {
                // delete the texture if it exists in the texture cache..
                // this only needs to be removed if the base texture is actually destoryed too..
                if (TextureCache[this.baseTexture.imageUrl])
                {
                    delete TextureCache[this.baseTexture.imageUrl];
                }

                this.baseTexture.destroy();
            }

            this.baseTexture.off('update', this.onBaseTextureUpdated, this);
            this.baseTexture.off('loaded', this.onBaseTextureLoaded, this);

            this.baseTexture = null;
        }

        this._frame = null;
        this._uvs = null;
        this.trim = null;
        this.orig = null;

        this.valid = false;

        this.off('dispose', this.dispose, this);
        this.off('update', this.update, this);
    }

    /**
     * Creates a new texture object that acts the same as this one.
     *
     * @return {PIXI.Texture} The new texture
     */
    clone()
    {
        return new Texture(this.baseTexture, this.frame, this.orig, this.trim, this.rotate);
    }

    /**
     * Updates the internal WebGL UV cache.
     *
     * @protected
     */
    _updateUvs()
    {
        if (!this._uvs)
        {
            this._uvs = new TextureUvs();
        }

        this._uvs.set(this._frame, this.baseTexture, this.rotate);

        this._updateID++;
    }

    /**
     * Helper function that creates a Texture object from the given image url.
     * If the image is not in the texture cache it will be  created and loaded.
     *
     * @static
     * @param {string} imageUrl - The image url of the texture
     * @param {boolean} [crossorigin] - Whether requests should be treated as crossorigin
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @param {number} [sourceScale=(auto)] - Scale for the original image, used with SVG images.
     * @return {PIXI.Texture} The newly created texture
     */
    static fromImage(imageUrl, crossorigin, scaleMode, sourceScale)
    {
        let texture = TextureCache[imageUrl];

        if (!texture)
        {
            texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale));
            TextureCache[imageUrl] = texture;
        }

        return texture;
    }

    /**
     * Helper function that creates a sprite that will contain a texture from the TextureCache based on the frameId
     * The frame ids are created when a Texture packer file has been loaded
     *
     * @static
     * @param {string} frameId - The frame Id of the texture in the cache
     * @return {PIXI.Texture} The newly created texture
     */
    static fromFrame(frameId)
    {
        const texture = TextureCache[frameId];

        if (!texture)
        {
            throw new Error(`The frameId "${frameId}" does not exist in the texture cache`);
        }

        return texture;
    }

    /**
     * Helper function that creates a new Texture based on the given canvas element.
     *
     * @static
     * @param {HTMLCanvasElement} canvas - The canvas element source of the texture
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromCanvas(canvas, scaleMode)
    {
        return new Texture(BaseTexture.fromCanvas(canvas, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the given video element.
     *
     * @static
     * @param {HTMLVideoElement|string} video - The URL or actual element of the video
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromVideo(video, scaleMode)
    {
        if (typeof video === 'string')
        {
            return Texture.fromVideoUrl(video, scaleMode);
        }

        return new Texture(VideoBaseTexture.fromVideo(video, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the video url.
     *
     * @static
     * @param {string} videoUrl - URL of the video
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromVideoUrl(videoUrl, scaleMode)
    {
        return new Texture(VideoBaseTexture.fromUrl(videoUrl, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the source you provide.
     * The soucre can be - frame id, image url, video url, canvae element, video element, base texture
     *
     * @static
     * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from
     * @return {PIXI.Texture} The newly created texture
     */
    static from(source)
    {
        // TODO auto detect cross origin..
        // TODO pass in scale mode?
        if (typeof source === 'string')
        {
            const texture = TextureCache[source];

            if (!texture)
            {
                // check if its a video..
                const isVideo = source.match(/\.(mp4|webm|ogg|h264|avi|mov)$/) !== null;

                if (isVideo)
                {
                    return Texture.fromVideoUrl(source);
                }

                return Texture.fromImage(source);
            }

            return texture;
        }
        else if (source instanceof HTMLCanvasElement)
        {
            return Texture.fromCanvas(source);
        }
        else if (source instanceof HTMLVideoElement)
        {
            return Texture.fromVideo(source);
        }
        else if (source instanceof BaseTexture)
        {
            return new Texture(source);
        }

        // lets assume its a texture!
        return source;
    }

    /**
     * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object.
     *
     * @static
     * @param {PIXI.Texture} texture - The Texture to add to the cache.
     * @param {string} id - The id that the texture will be stored against.
     */
    static addTextureToCache(texture, id)
    {
        TextureCache[id] = texture;
    }

    /**
     * Remove a texture from the global TextureCache.
     *
     * @static
     * @param {string} id - The id of the texture to be removed
     * @return {PIXI.Texture} The texture that was removed
     */
    static removeTextureFromCache(id)
    {
        const texture = TextureCache[id];

        delete TextureCache[id];
        delete BaseTextureCache[id];

        return texture;
    }

    /**
     * The frame specifies the region of the base texture that this texture uses.
     *
     * @member {PIXI.Rectangle}
     * @memberof PIXI.Texture#
     */
    get frame()
    {
        return this._frame;
    }

    /**
     * Set the frame.
     *
     * @param {Rectangle} frame - The new frame to set.
     */
    set frame(frame)
    {
        this._frame = frame;

        this.noFrame = false;

        if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height)
        {
            throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`);
        }

        // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded;
        this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded;

        if (!this.trim && !this.rotate)
        {
            this.orig = frame;
        }

        if (this.valid)
        {
            this._updateUvs();
        }
    }

    /**
     * Indicates whether the texture is rotated inside the atlas
     * set to 2 to compensate for texture packer rotation
     * set to 6 to compensate for spine packer rotation
     * can be used to rotate or mirror sprites
     * See {@link PIXI.GroupD8} for explanation
     *
     * @member {number}
     */
    get rotate()
    {
        return this._rotate;
    }

    /**
     * Set the rotation
     *
     * @param {number} rotate - The new rotation to set.
     */
    set rotate(rotate)
    {
        this._rotate = rotate;
        if (this.valid)
        {
            this._updateUvs();
        }
    }

    /**
     * The width of the Texture in pixels.
     *
     * @member {number}
     */
    get width()
    {
        return this.orig ? this.orig.width : 0;
    }

    /**
     * The height of the Texture in pixels.
     *
     * @member {number}
     */
    get height()
    {
        return this.orig ? this.orig.height : 0;
    }
}

/**
 * An empty texture, used often to not have to create multiple empty textures.
 * Can not be destroyed.
 *
 * @static
 * @constant
 */
Texture.EMPTY = new Texture(new BaseTexture());
Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ };
Texture.EMPTY.on = function _emptyOn() { /* empty */ };
Texture.EMPTY.once = function _emptyOnce() { /* empty */ };
Texture.EMPTY.emit = function _emptyEmit() { /* empty */ };
<commit_msg>Revert "Fixes Texture.from variable error" (#3102)<commit_after>import BaseTexture from './BaseTexture';
import VideoBaseTexture from './VideoBaseTexture';
import TextureUvs from './TextureUvs';
import EventEmitter from 'eventemitter3';
import { Rectangle } from '../math';
import { TextureCache, BaseTextureCache } from '../utils';

/**
 * A texture stores the information that represents an image or part of an image. It cannot be added
 * to the display list directly. Instead use it as the texture for a Sprite. If no frame is provided
 * then the whole image is used.
 *
 * You can directly create a texture from an image and then reuse it multiple times like this :
 *
 * ```js
 * let texture = PIXI.Texture.fromImage('assets/image.png');
 * let sprite1 = new PIXI.Sprite(texture);
 * let sprite2 = new PIXI.Sprite(texture);
 * ```
 *
 * Textures made from SVGs, loaded or not, cannot be used before the file finishes processing.
 * You can check for this by checking the sprite's _textureID property.
 * ```js
 * var texture = PIXI.Texture.fromImage('assets/image.svg');
 * var sprite1 = new PIXI.Sprite(texture);
 * //sprite1._textureID should not be undefined if the texture has finished processing the SVG file
 * ```
 * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068.
 *
 * @class
 * @extends EventEmitter
 * @memberof PIXI
 */
export default class Texture extends EventEmitter
{
    /**
     * @param {PIXI.BaseTexture} baseTexture - The base texture source to create the texture from
     * @param {PIXI.Rectangle} [frame] - The rectangle frame of the texture to show
     * @param {PIXI.Rectangle} [orig] - The area of original texture
     * @param {PIXI.Rectangle} [trim] - Trimmed rectangle of original texture
     * @param {number} [rotate] - indicates how the texture was rotated by texture packer. See {@link PIXI.GroupD8}
     */
    constructor(baseTexture, frame, orig, trim, rotate)
    {
        super();

        /**
         * Does this Texture have any frame data assigned to it?
         *
         * @member {boolean}
         */
        this.noFrame = false;

        if (!frame)
        {
            this.noFrame = true;
            frame = new Rectangle(0, 0, 1, 1);
        }

        if (baseTexture instanceof Texture)
        {
            baseTexture = baseTexture.baseTexture;
        }

        /**
         * The base texture that this texture uses.
         *
         * @member {PIXI.BaseTexture}
         */
        this.baseTexture = baseTexture;

        /**
         * This is the area of the BaseTexture image to actually copy to the Canvas / WebGL when rendering,
         * irrespective of the actual frame size or placement (which can be influenced by trimmed texture atlases)
         *
         * @member {PIXI.Rectangle}
         */
        this._frame = frame;

        /**
         * This is the trimmed area of original texture, before it was put in atlas
         *
         * @member {PIXI.Rectangle}
         */
        this.trim = trim;

        /**
         * This will let the renderer know if the texture is valid. If it's not then it cannot be rendered.
         *
         * @member {boolean}
         */
        this.valid = false;

        /**
         * This will let a renderer know that a texture has been updated (used mainly for webGL uv updates)
         *
         * @member {boolean}
         */
        this.requiresUpdate = false;

        /**
         * The WebGL UV data cache.
         *
         * @member {PIXI.TextureUvs}
         * @private
         */
        this._uvs = null;

        /**
         * This is the area of original texture, before it was put in atlas
         *
         * @member {PIXI.Rectangle}
         */
        this.orig = orig || frame;// new Rectangle(0, 0, 1, 1);

        this._rotate = Number(rotate || 0);

        if (rotate === true)
        {
            // this is old texturepacker legacy, some games/libraries are passing "true" for rotated textures
            this._rotate = 2;
        }
        else if (this._rotate % 2 !== 0)
        {
            throw new Error('attempt to use diamond-shaped UVs. If you are sure, set rotation manually');
        }

        if (baseTexture.hasLoaded)
        {
            if (this.noFrame)
            {
                frame = new Rectangle(0, 0, baseTexture.width, baseTexture.height);

                // if there is no frame we should monitor for any base texture changes..
                baseTexture.on('update', this.onBaseTextureUpdated, this);
            }
            this.frame = frame;
        }
        else
        {
            baseTexture.once('loaded', this.onBaseTextureLoaded, this);
        }

        /**
         * Fired when the texture is updated. This happens if the frame or the baseTexture is updated.
         *
         * @event update
         * @memberof PIXI.Texture#
         * @protected
         */

        this._updateID = 0;

        /**
         * Extra field for extra plugins. May contain clamp settings and some matrices
         * @type {Object}
         */
        this.transform = null;
    }

    /**
     * Updates this texture on the gpu.
     *
     */
    update()
    {
        this.baseTexture.update();
    }

    /**
     * Called when the base texture is loaded
     *
     * @private
     * @param {PIXI.BaseTexture} baseTexture - The base texture.
     */
    onBaseTextureLoaded(baseTexture)
    {
        this._updateID++;

        // TODO this code looks confusing.. boo to abusing getters and setterss!
        if (this.noFrame)
        {
            this.frame = new Rectangle(0, 0, baseTexture.width, baseTexture.height);
        }
        else
        {
            this.frame = this._frame;
        }

        this.baseTexture.on('update', this.onBaseTextureUpdated, this);
        this.emit('update', this);
    }

    /**
     * Called when the base texture is updated
     *
     * @private
     * @param {PIXI.BaseTexture} baseTexture - The base texture.
     */
    onBaseTextureUpdated(baseTexture)
    {
        this._updateID++;

        this._frame.width = baseTexture.width;
        this._frame.height = baseTexture.height;

        this.emit('update', this);
    }

    /**
     * Destroys this texture
     *
     * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well
     */
    destroy(destroyBase)
    {
        if (this.baseTexture)
        {
            if (destroyBase)
            {
                // delete the texture if it exists in the texture cache..
                // this only needs to be removed if the base texture is actually destoryed too..
                if (TextureCache[this.baseTexture.imageUrl])
                {
                    delete TextureCache[this.baseTexture.imageUrl];
                }

                this.baseTexture.destroy();
            }

            this.baseTexture.off('update', this.onBaseTextureUpdated, this);
            this.baseTexture.off('loaded', this.onBaseTextureLoaded, this);

            this.baseTexture = null;
        }

        this._frame = null;
        this._uvs = null;
        this.trim = null;
        this.orig = null;

        this.valid = false;

        this.off('dispose', this.dispose, this);
        this.off('update', this.update, this);
    }

    /**
     * Creates a new texture object that acts the same as this one.
     *
     * @return {PIXI.Texture} The new texture
     */
    clone()
    {
        return new Texture(this.baseTexture, this.frame, this.orig, this.trim, this.rotate);
    }

    /**
     * Updates the internal WebGL UV cache.
     *
     * @protected
     */
    _updateUvs()
    {
        if (!this._uvs)
        {
            this._uvs = new TextureUvs();
        }

        this._uvs.set(this._frame, this.baseTexture, this.rotate);

        this._updateID++;
    }

    /**
     * Helper function that creates a Texture object from the given image url.
     * If the image is not in the texture cache it will be  created and loaded.
     *
     * @static
     * @param {string} imageUrl - The image url of the texture
     * @param {boolean} [crossorigin] - Whether requests should be treated as crossorigin
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @param {number} [sourceScale=(auto)] - Scale for the original image, used with SVG images.
     * @return {PIXI.Texture} The newly created texture
     */
    static fromImage(imageUrl, crossorigin, scaleMode, sourceScale)
    {
        let texture = TextureCache[imageUrl];

        if (!texture)
        {
            texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale));
            TextureCache[imageUrl] = texture;
        }

        return texture;
    }

    /**
     * Helper function that creates a sprite that will contain a texture from the TextureCache based on the frameId
     * The frame ids are created when a Texture packer file has been loaded
     *
     * @static
     * @param {string} frameId - The frame Id of the texture in the cache
     * @return {PIXI.Texture} The newly created texture
     */
    static fromFrame(frameId)
    {
        const texture = TextureCache[frameId];

        if (!texture)
        {
            throw new Error(`The frameId "${frameId}" does not exist in the texture cache`);
        }

        return texture;
    }

    /**
     * Helper function that creates a new Texture based on the given canvas element.
     *
     * @static
     * @param {HTMLCanvasElement} canvas - The canvas element source of the texture
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromCanvas(canvas, scaleMode)
    {
        return new Texture(BaseTexture.fromCanvas(canvas, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the given video element.
     *
     * @static
     * @param {HTMLVideoElement|string} video - The URL or actual element of the video
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromVideo(video, scaleMode)
    {
        if (typeof video === 'string')
        {
            return Texture.fromVideoUrl(video, scaleMode);
        }

        return new Texture(VideoBaseTexture.fromVideo(video, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the video url.
     *
     * @static
     * @param {string} videoUrl - URL of the video
     * @param {number} [scaleMode=PIXI.SCALE_MODES.DEFAULT] - See {@link PIXI.SCALE_MODES} for possible values
     * @return {PIXI.Texture} The newly created texture
     */
    static fromVideoUrl(videoUrl, scaleMode)
    {
        return new Texture(VideoBaseTexture.fromUrl(videoUrl, scaleMode));
    }

    /**
     * Helper function that creates a new Texture based on the source you provide.
     * The soucre can be - frame id, image url, video url, canvae element, video element, base texture
     *
     * @static
     * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from
     * @return {PIXI.Texture} The newly created texture
     */
    static from(source)
    {
        // TODO auto detect cross origin..
        // TODO pass in scale mode?
        if (typeof source === 'string')
        {
            const texture = TextureCache[source];

            if (!texture)
            {
                // check if its a video..
                const isVideo = source.match(/\.(mp4|webm|ogg|h264|avi|mov)$/) !== null;

                if (isVideo)
                {
                    return Texture.fromVideoUrl(source);
                }

                return Texture.fromImage(source);
            }

            return texture;
        }
        else if (source instanceof HTMLCanvasElement)
        {
            return Texture.fromCanvas(source);
        }
        else if (source instanceof HTMLVideoElement)
        {
            return Texture.fromVideo(source);
        }
        else if (source instanceof BaseTexture)
        {
            return new Texture(BaseTexture);
        }

        // lets assume its a texture!
        return source;
    }

    /**
     * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object.
     *
     * @static
     * @param {PIXI.Texture} texture - The Texture to add to the cache.
     * @param {string} id - The id that the texture will be stored against.
     */
    static addTextureToCache(texture, id)
    {
        TextureCache[id] = texture;
    }

    /**
     * Remove a texture from the global TextureCache.
     *
     * @static
     * @param {string} id - The id of the texture to be removed
     * @return {PIXI.Texture} The texture that was removed
     */
    static removeTextureFromCache(id)
    {
        const texture = TextureCache[id];

        delete TextureCache[id];
        delete BaseTextureCache[id];

        return texture;
    }

    /**
     * The frame specifies the region of the base texture that this texture uses.
     *
     * @member {PIXI.Rectangle}
     * @memberof PIXI.Texture#
     */
    get frame()
    {
        return this._frame;
    }

    /**
     * Set the frame.
     *
     * @param {Rectangle} frame - The new frame to set.
     */
    set frame(frame)
    {
        this._frame = frame;

        this.noFrame = false;

        if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height)
        {
            throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`);
        }

        // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded;
        this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded;

        if (!this.trim && !this.rotate)
        {
            this.orig = frame;
        }

        if (this.valid)
        {
            this._updateUvs();
        }
    }

    /**
     * Indicates whether the texture is rotated inside the atlas
     * set to 2 to compensate for texture packer rotation
     * set to 6 to compensate for spine packer rotation
     * can be used to rotate or mirror sprites
     * See {@link PIXI.GroupD8} for explanation
     *
     * @member {number}
     */
    get rotate()
    {
        return this._rotate;
    }

    /**
     * Set the rotation
     *
     * @param {number} rotate - The new rotation to set.
     */
    set rotate(rotate)
    {
        this._rotate = rotate;
        if (this.valid)
        {
            this._updateUvs();
        }
    }

    /**
     * The width of the Texture in pixels.
     *
     * @member {number}
     */
    get width()
    {
        return this.orig ? this.orig.width : 0;
    }

    /**
     * The height of the Texture in pixels.
     *
     * @member {number}
     */
    get height()
    {
        return this.orig ? this.orig.height : 0;
    }
}

/**
 * An empty texture, used often to not have to create multiple empty textures.
 * Can not be destroyed.
 *
 * @static
 * @constant
 */
Texture.EMPTY = new Texture(new BaseTexture());
Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ };
Texture.EMPTY.on = function _emptyOn() { /* empty */ };
Texture.EMPTY.once = function _emptyOnce() { /* empty */ };
Texture.EMPTY.emit = function _emptyEmit() { /* empty */ };
<|endoftext|>


<commit_before>//Renderer process for main window
window.$ = window.jQuery = require('jquery')
require('jquery-ui-dist/jquery-ui.js')

const electron = require('electron')
const {remote} = electron
const {Menu}  = remote.require('electron')
const {ipcRenderer} = require('electron')

import Vue from 'vue/dist/vue.js'
const Tone = require('Tone')
const fs = require('fs')
const path = require('path')

const main = remote.require('./main')
const audio = require('./audio-engine')
const ui = require('./ui')

// import songEditor from '../core-plugins/song-editor/song-editor.vue'
// Vue.component('song-editor', require('../core-plugins/song-editor/song-editor.vue'))
loadUIPlugin('song-editor')

var fileArray = [] //This is a list of all files recorded or imported
var pluginList = [
	{name: 'Song Editor', file: 'song-editor'}
]

exports.pluginList = pluginList

function getFileName(filepath) {
	filepath = filepath.replace(/\\/g,"\/") //Convert Windows paths to UNIX
	filepath = filepath.split('\/') //Split directoies
	return filepath[filepath.length - 1] //Return test after last slash
}

function getPluginData(pluginPath) {
	// var pluginPath = path.join(__dirname, '../core-plugins', pluginName)

	var pluginData = JSON.parse( fs.readFileSync(path.join(pluginPath, 'plugin.json'), 'utf-8') )

	// // This is to replace relative paths with absolute
	// if (pluginData.hasOwnProperty('htmlFile')) {
	// 	pluginData.htmlFile = path.join(pluginPath, pluginData.htmlFile)
	// }
	// if (pluginData.hasOwnProperty('javaScriptFile')) {
	// 	pluginData.javaScriptFile = path.join(pluginPath, pluginData.javaScriptFile)
	// }

	return pluginData
}

function loadUIPlugin (pluginName) {
	// //Load HTML file
	// $(targetDiv).load(pluginData.htmlFile)
	//
	// //Run the plugin script
	// fs.readFile(pluginData.javaScriptFile, 'utf-8', function (err, data) {
	// 	if (!err) {
	// 		eval(data)
	// 	} else {
	// 		console.log(err)
	// 	}
	// })
	var pluginPath = path.join(__dirname, '../core-plugins', pluginName)

	var pluginData = getPluginData(pluginPath)

	var vueFile = path.join(pluginPath, pluginData['file'])

	Vue.component(pluginName, require(vueFile))

}

function loadProject(filepath) {
	// fs.readFileSync(filepath, 'utf-8', function (err, data) {
	// 	if (!err) {
	// 		currentProject = audio.readProject( JSON.parse(data) )
	// 	} else {
	// 		displayMessage('error', 'Cannot Load Project', 'Could not open file:' + filepath)
	// 	}
	// })

	var data = fs.readFileSync(filepath, 'utf-8')
	return audio.readProject( JSON.parse(data) )
}

//IPC Functions

ipcRenderer.on('importer', (event, arg) => {
	fileArray = fileArray.concat(arg)

	for (var i = 0; i < arg.length; i++) {
		audio.audioSamples.add(getFileName(arg[i]), arg[i]) //Adds samples to buffer
		$('#file-list').append('
'+getFileName(arg[i])+'
')
	}
})

//Vue App

var mainWindow = new Vue({
	el: '#window-body'
})
<commit_msg>Cleaned up old comments<commit_after>//Renderer process for main window
window.$ = window.jQuery = require('jquery')
require('jquery-ui-dist/jquery-ui.js')

const electron = require('electron')
const {remote} = electron
const {Menu}  = remote.require('electron')
const {ipcRenderer} = require('electron')

import Vue from 'vue/dist/vue.js'
const Tone = require('Tone')
const fs = require('fs')
const path = require('path')

const main = remote.require('./main')
const audio = require('./audio-engine')
const ui = require('./ui')

loadUIPlugin('song-editor')

var fileArray = [] //This is a list of all files recorded or imported
var pluginList = [
	{name: 'Song Editor', file: 'song-editor'}
]

exports.pluginList = pluginList

function getFileName(filepath) {
	filepath = filepath.replace(/\\/g,"\/") //Convert Windows paths to UNIX
	filepath = filepath.split('\/') //Split directoies
	return filepath[filepath.length - 1] //Return test after last slash
}

function getPluginData(pluginPath) {
	var pluginData = JSON.parse( fs.readFileSync(path.join(pluginPath, 'plugin.json'), 'utf-8') )

	return pluginData

}

function loadUIPlugin (pluginName) {
	var pluginPath = path.join(__dirname, '../core-plugins', pluginName)

	var pluginData = getPluginData(pluginPath)

	var vueFile = path.join(pluginPath, pluginData['file'])

	Vue.component(pluginName, require(vueFile))

}

function loadProject(filepath) {
	// fs.readFileSync(filepath, 'utf-8', function (err, data) {
	// 	if (!err) {
	// 		currentProject = audio.readProject( JSON.parse(data) )
	// 	} else {
	// 		displayMessage('error', 'Cannot Load Project', 'Could not open file:' + filepath)
	// 	}
	// })

	var data = fs.readFileSync(filepath, 'utf-8')
	return audio.readProject( JSON.parse(data) )
}

//IPC Functions

ipcRenderer.on('importer', (event, arg) => {
	fileArray = fileArray.concat(arg)

	for (var i = 0; i < arg.length; i++) {
		audio.audioSamples.add(getFileName(arg[i]), arg[i]) //Adds samples to buffer
		$('#file-list').append('
'+getFileName(arg[i])+'
')
	}
})

//Vue App

var mainWindow = new Vue({
	el: '#window-body'
})
<|endoftext|>


<commit_before>'use strict';

const Images = require('../src/images');
const Twitter = require('../src/twitter');
const ReplyTweet = require('../src/tweets/reply_tweet');
const RequestTweet = require('../src/tweets/request_tweet');
//const Facebook = require('../src/facebook');

let stream = Twitter.stream();

stream.on('tweet', (payload) => {
  let request_tweet = new RequestTweet(payload);

  if (request_tweet.shouldReply()) {
    let image = new Images().getFromText(request_tweet.getText());
    if (!image) { return; }

    let reply_tweet = new ReplyTweet(request_tweet, image);
    Twitter.reply(reply_tweet);
  }
});

stream.on('error', (payload) => {
  console.error(payload);
});

const http = require('http');
const Bot = require('messenger-bot');
let bot = new Bot({
  token: process.env.FACEBOOK_PAGE_TOKEN,
  verify: process.env.FACEBOOK_VERIFY_TOKEN,
  app_secret: process.env.FACEBOOK_APP_SECRET
});

bot.on('error', (err) => {
  console.log(err.message);
});

bot.on('message', (payload, reply) => {
  //let text = payload.message.text; //change this to emoji
  console.log(payload.message.text);
  let image = new Images().getFromText(payload.message.text);
  if (!image) { return; }
  console.log(image);
  let text = `${image.key} ${image.url}`;
  console.log(text);

  bot.getProfile(payload.sender.id, (err, profile) => {
    if (err) throw err;

    reply({ text }, (err) => {
      if (err) throw err;

      console.log(`Echoed back to ${profile.first_name} ${profile.last_name}: ${text}`);
    });
  });
});

http.createServer(bot.middleware()).listen(3000);
console.log('Echo bot server running at port 3000.');
<commit_msg>facebook testing<commit_after>'use strict';

const Images = require('../src/images');
const Twitter = require('../src/twitter');
const ReplyTweet = require('../src/tweets/reply_tweet');
const RequestTweet = require('../src/tweets/request_tweet');
//const Facebook = require('../src/facebook');

let stream = Twitter.stream();

stream.on('tweet', (payload) => {
  let request_tweet = new RequestTweet(payload);

  if (request_tweet.shouldReply()) {
    let image = new Images().getFromText(request_tweet.getText());
    if (!image) { return; }

    let reply_tweet = new ReplyTweet(request_tweet, image);
    Twitter.reply(reply_tweet);
  }
});

stream.on('error', (payload) => {
  console.error(payload);
});

const http = require('http');
const Bot = require('messenger-bot');
let bot = new Bot({
  token: process.env.FACEBOOK_PAGE_TOKEN,
  verify: process.env.FACEBOOK_VERIFY_TOKEN,
  app_secret: process.env.FACEBOOK_APP_SECRET
});

bot.on('error', (err) => {
  console.log(err.message);
});

bot.on('message', (payload, reply) => {
  //let text = payload.message.text; //change this to emoji
  console.log(payload.message.text);
  let image = new Images().getFromText(payload.message.text);
  if (!image) { return; }
  console.log(image);
  if image.url == 'undefined' {
      image.url = 'Tyvärr! 🤔😞😬 Kanske hittar du något på https://digitaltmuseum.se?';
  }
  console.log(text);

  bot.getProfile(payload.sender.id, (err, profile) => {
    if (err) throw err;

    reply({ text }, (err) => {
      if (err) throw err;

      console.log(`Echoed back to ${profile.first_name} ${profile.last_name}: ${text}`);
    });
  });
});

http.createServer(bot.middleware()).listen(3000);
console.log('Echo bot server running at port 3000.');
<|endoftext|>


<commit_before>/**
 This Source Code is licensed under the MIT license. If a copy of the
 MIT-license was not distributed with this file, You can obtain one at:
 http://opensource.org/licenses/mit-license.html.

 @author: Tom Clement (tjclement)
 @license MIT
 @copyright Bizboard, 2015

 */

import _                            from 'lodash';
import EventEmitter                 from 'eventemitter3';
import {Context}                    from '../utils/Context.js';
import {ObjectHelper}               from '../utils/ObjectHelper.js';
import {DataSource}                 from './DataSource.js';

export class PrioritisedArray extends Array {

    /* Extending Array does not work fluently yet. The length property always returns 0,
     * regardless of how many entries are in the array. We'll override the length prop to determine
     * the amount of enumerable properties in our PrioritisedArray instead of using the built-in length property.
     */
    get length() {
        return Object.keys(this).length;
    }

    set length(value) {
        return value;
    }

    /**
     *
     * @param {Function} dataType DataType of the models being added to the PrioritisedArray.
     * @param {DataSource} dataSource Optional: dataSource to load the models from. If none is given, a new DataSource is made with a path guessed from
     * the model's DataType name.
     * @param {Snapshot} dataSnapshot Optional: snapshot already containing model data. Prevents initial subscription on all values in the DataSource.
     * @param {Object} options Optional: options to pass to the dataSource if none is provided and a new one is constructed.
     * @returns {PrioritisedArray} PrioritisedArray instance.
     */
    constructor(dataType, dataSource = null, dataSnapshot = null, options = null, modelOptions = {}) {
        super();
        /**** Callbacks ****/
        this._valueChangedCallback = null;

        /**** Private properties ****/
        this._dataType = dataType;
        this._dataSource = dataSource;
        this._isBeingReordered = false;
        this._modelOptions = modelOptions;
        /* Flag to determine when we're reordering so we don't listen to move updates */
        this._eventEmitter = new EventEmitter();

        /* Bind all local methods to the current object instance, so we can refer to "this"
         * in the methods as expected, even when they're called from event handlers.        */
        ObjectHelper.bindAllMethods(this, this);

        /* Hide all private properties (starting with '_') and methods from enumeration,
         * so when you do for( in ), only actual data properties show up. */
        ObjectHelper.hideMethodsAndPrivatePropertiesFromObject(this);

        /* Hide the priority field from enumeration, so we don't save it to the dataSource. */
        ObjectHelper.hidePropertyFromObject(Object.getPrototypeOf(this), 'length');

        /* If no dataSource is given, create own one with guessed path */
        if (!dataSource) {
            let path
                = Object.getPrototypeOf(this).constructor.name;
            // retrieve dataSource from the DI context
            dataSource = Context.getContext().get(DataSource);

            if (options) {
                dataSource = dataSource.child(options.path || path, options);
            } else {
                dataSource = dataSource.child(path);
            }

            this._dataSource = dataSource;
        }

        /* If a snapshot is present use it, otherwise generate one by subscribing to the dataSource one time. */
        if (dataSnapshot) {
            this._buildFromSnapshot(dataSnapshot);
        } else {
            this._buildFromDataSource(dataSource);
        }
    }

    /**
     * Subscribes to events emitted by this PrioritisedArray.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function that is called when the given event type is emitted.
     * @param {Object} context Optional: context of 'this' inside the handler function when it is called.
     * @returns {void}
     */
    on(event, handler, context) {
        /* If we're already ready, fire immediately */
        if ((event === 'ready' || event === 'value') && this._dataSource && this._dataSource.ready) {
            handler.call(context, this);
        }

        /* If we already have children stored locally when the subscriber calls this method,
         * fire their callback for all pre-existing children. */
        if (event === 'child_added') {
            for (let i = 0; i < this.length; i++) {
                let model = this[i];
                let previousSiblingID = i > 0 ? this[i - 1].id : null;
                handler.call(context, model, previousSiblingID);
            }
        }

        this._eventEmitter.on(event, handler, context);
    }

    /**
     * Subscribes to the given event type exactly once; it automatically unsubscribes after the first time it is triggered.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function that is called when the given event type is emitted.
     * @param {Object} context Optional: context of 'this' inside the handler function when it is called.
     * @returns {void}
     */
    once(event, handler, context = this) {
        return this.on(event, function onceWrapper() {
            handler.call(context, ...arguments);
            this.off(event, onceWrapper, context);
        }, this);
    }

    /**
     * Removes subscription to events emitted by this PrioritisedArray. If no handler or context is given, all handlers for
     * the given event are removed. If no parameters are given at all, all event types will have their handlers removed.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function to remove from event callbacks.
     * @param {Object} context Object to bind the given callback function to.
     * @returns {void}
     */
    off(event, handler, context) {
        if (event && (handler || context)) {
            this._eventEmitter.removeListener(event, handler, context);
        } else {
            this._eventEmitter.removeAllListeners(event);
        }
    }

    /**
     * Adds a model instance to the rear of the PrioritisedArray, and emits a 'child_added' and possibly 'new_child' event after successful addition.
     * @param {Model} model Instance of a Model.
     * @param {String} prevSiblingId ID of the model preceding the one that will be added.
     * @returns {Object} Same model as the one originally passed as parameter.
     */
    add(model, prevSiblingId = null) {
        if (model instanceof this._dataType) {
            if (this._findIndexById(model.id) < 0) {

                if (prevSiblingId) {
                    let newPosition = this._findIndexById(prevSiblingId) + 1;
                    this._ids[model._id] = newPosition;
                    this.insertAt(model, newPosition);
                } else {
                    this.push(model);
                    this._ids[model._id] = this.length - 1;
                }

                /* If we've already received an on('value') result, this child addition is
                 * a new entry that wasn't on the dataSource before. */
                if(this._dataSource.ready) {
                    this._eventEmitter.emit('new_child', model, prevSiblingId);
                }

                this._eventEmitter.emit('child_added', model, prevSiblingId);
                return model;
            }
        } else if (model instanceof Object) {
            /* Let's try to parse the object using property reflection */
            var options = {dataSource: this._dataSource};
            let newModel = new this._dataType(null, model, _.extend({}, this._modelOptions, options));
            this.add(newModel);
            return newModel;
        } else {
            /* TODO: change to throw exception */
            console.log('Tried to append an object that is not the same type as the one this PrioritisedArray was created with.');
        }

        /* Return model so we can do this: let newModel = PrioArray.add(new Model()); newModel.someProperty = true; */
        return  null;
    }

    /**
     * Inserts a model instance at the given position of the PrioritisedArray, and recalculates the priority (position)
     * of all models after the inserted position.
     * @param {Model} model Subclass of Model
     * @param {Number} position Zero-based index where to put the new model instance.
     * @returns {Object} Same model as the one originally passed as parameter.
     */
    insertAt(model, position) {
        if (model instanceof this._dataType) {
            this.splice(position, 0, model);
            this._ids[model._id] = position;
        } else {
            /* TODO: change to throw exception */
            console.log('Tried to append an object that is not the same type as the PrioritisedArray was created with.');
        }

        /* Return model so we can do this: let newModel = PrioArray.add(new Model()); newModel.someProperty = true; */
        return model;
    }

    /**
     * Moves a model instance from one position to another.
     * @param {Number} fromPosition Zero-based index of original position
     * @param {Number} toPosition Zero-based index of target position
     * @returns {void}
     */
    move(fromPosition, toPosition) {
        let model = this[fromPosition];
        this._ids[model._id] = toPosition;
        this._ids[fromPosition] = null;
        this.splice(fromPosition, 1);
        this.splice(toPosition, 0, model);

    }

    /**
     * Removes the model instance at the given position. Does not remove the model from the datasource, to do that
     * call model.remove() directly, or PrioArray[index].remove().
     * @param {Number} position Index in the PrioritisedArray of the model to remove.
     * @returns {void}
     */
    remove(position) {
        this.splice(position, 1);
    }

    /**
     * Interprets all childs of a given snapshot as instances of the given data type for this PrioritisedArray,
     * and attempts to instantiate new model instances based on these sub-snapshots. It adds them to the
     * PrioritisedArray, which also assigns their priority based on their inserted position.
     * @param {Snapshot} dataSnapshot Snapshot to build the PrioritisedArray from.
     * @returns {void}
     * @private
     */
    _buildFromSnapshot(dataSnapshot) {

        let numChildren = dataSnapshot.numChildren(), currentChild = 1;

        /* If there is no data at this point yet, fire a ready event */
        if (numChildren === 0) {
            this._dataSource.ready = true;
            this._eventEmitter.emit('ready');
            this._eventEmitter.emit('value', this);
        }

        dataSnapshot.forEach(
            function (child) {
                /* Create a new instance of the given data type and prefill it with the snapshot data. */
                let options = {dataSnapshot: child};
                let childRef = this._dataSource.child(child.key());

                /* whenever the ref() is a datasource, we can bind that source to the model.
                 * whenever it's not a datasource, we assume the model should instantiate a new
                 * datasource to bind the model */

                if (childRef instanceof DataSource) {
                    options.dataSource = childRef;
                } else {
                    var rootPath = dataSnapshot.ref().root().toString();
                    options.path = dataSnapshot.ref().toString().replace(rootPath, '/');
                }

                let newModel = new this._dataType(child.key(), child.val(), _.extend({}, this._modelOptions, options));
                this.add(newModel);

                /* If this is the last child, fire a ready event */
                if (currentChild++ === numChildren) {
                    this._dataSource.ready = true;
                    this._eventEmitter.emit('ready');
                    this._eventEmitter.emit('value', this);
                }

            }.bind(this));
    }

    /**
     * Clones a dataSource (to not disturb any existing callbacks defined on the original) and uses it
     * to get a dataSnapshot which is used in _buildSnapshot to build our array.
     * @param {DataSource} dataSource DataSource to subscribe to for building the PrioritisedArray.
     * @returns {void}
     * @private
     */
    _buildFromDataSource(dataSource) {
        dataSource.once('value', (dataSnapshot) => {
            this._buildFromSnapshot(dataSnapshot);
            this._registerCallbacks(dataSource);
        });
    }

    /**
     * Registers the added, moved, changed, and removed callbacks to the given DataSource.
     * @param {DataSource} dataSource DataSource to register callbacks on.
     * @return {void}
     * @private
     */
    _registerCallbacks(dataSource) {
        dataSource.setChildAddedCallback(this._onChildAdded);
        dataSource.setChildMovedCallback(this._onChildMoved);
        dataSource.setChildChangedCallback(this._onChildChanged);
        dataSource.setChildRemovedCallback(this._onChildRemoved);
    }

    /**
     * Called by dataSource when a new child is added.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildAdded(snapshot, prevSiblingId) {
        let id = snapshot.key();
        let model = new this._dataType(id, null, {
            dataSnapshot: snapshot,
            dataSource: this._dataSource.child(id)
        });

        let previousPosition = this._findIndexById(id);
        if(previousPosition >= 0) {
            let oldModel = this[previousPosition];
            let oldProperties = ObjectHelper.getEnumerableProperties(oldModel);
            let newProperties = ObjectHelper.getEnumerableProperties(model);
            if (_.isEqual(oldProperties, newProperties)) { /* Child already exists. */
                return;
            }
        }

        this.add(model, prevSiblingId);

        if (!this._dataSource.ready) {
            this._dataSource.ready = true;
            this._eventEmitter.emit('ready');
        }
        this._eventEmitter.emit('value', this);
    }

    /**
     * Called by dataSource when a child is changed.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildChanged(snapshot, prevSiblingId) {
        let id = snapshot.key();
        let changedModel = new this._dataType(id, null, {dataSnapshot: snapshot, dataSource: snapshot.ref()});

        let previousPosition = this._findIndexById(id);
        if (previousPosition < 0) {
            /* The model doesn't exist, so we won't emit a changed event. */
            return;
        }

        this.remove(previousPosition);

        let newPosition = this._findIndexById(prevSiblingId) + 1;
        this.insertAt(changedModel, newPosition);

        this._eventEmitter.emit('child_changed', changedModel, prevSiblingId);
        this._eventEmitter.emit('value', this);
    }

    /**
     * Called by dataSource when a child is moved, which changes its priority.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildMoved(snapshot, prevSiblingId) {
        /* Ignore priority updates whilst we're reordering to avoid floods */
        if (!this._isBeingReordered) {

            let id = snapshot.key();
            let previousPosition = this._findIndexById(id);
            let tempModel = this[previousPosition];
            this._ids[id] = null;
            this.remove(previousPosition);

            let newPosition = this._findIndexById(prevSiblingId) + 1;
            this.insertAt(tempModel, newPosition);

            let model = this[newPosition];

            this._eventEmitter.emit('child_moved', model, previousPosition);
            this._eventEmitter.emit('value', this);
        }
    }

    /**
     * Called by dataSource when a child is removed.
     * @param {Snapshot} oldSnapshot Snapshot of the added child.
     * @returns {void}
     * @private
     */
    _onChildRemoved(oldSnapshot) {
        /* TODO: figure out if we can use the snapshot's priority as our array index reliably, to avoid big loops. */
        let id = oldSnapshot.key();
        let position = this._findIndexById(id);
        let model = this[position];

        if (position !== -1) {
            this._ids[id] = null;
            this.remove(position);

            this._eventEmitter.emit('child_removed', model);
            this._eventEmitter.emit('value', this);
        }
    }

    /**
     * Return the position of model's id, saved in an associative array
     * @param {Number} id Id field of the model we're looking for
     * @returns {Number} Zero-based index if found, -1 otherwise
     * @private
     */
    _findIndexById(id) {
        let position = this._ids[id];
        return (position == undefined || position == null) ? -1 : position;
    }

}
<commit_msg>Cleanup and added missing private variable within PrioArray<commit_after>/**
 This Source Code is licensed under the MIT license. If a copy of the
 MIT-license was not distributed with this file, You can obtain one at:
 http://opensource.org/licenses/mit-license.html.

 @author: Tom Clement (tjclement)
 @license MIT
 @copyright Bizboard, 2015

 */

import _                            from 'lodash';
import EventEmitter                 from 'eventemitter3';
import {Context}                    from '../utils/Context.js';
import {ObjectHelper}               from '../utils/ObjectHelper.js';
import {DataSource}                 from './DataSource.js';

export class PrioritisedArray extends Array {

    /* Extending Array does not work fluently yet. The length property always returns 0,
     * regardless of how many entries are in the array. We'll override the length prop to determine
     * the amount of enumerable properties in our PrioritisedArray instead of using the built-in length property.
     */
    get length() {
        return Object.keys(this).length;
    }

    set length(value) {
        return value;
    }

    /**
     *
     * @param {Function} dataType DataType of the models being added to the PrioritisedArray.
     * @param {DataSource} dataSource Optional: dataSource to load the models from. If none is given, a new DataSource is made with a path guessed from
     * the model's DataType name.
     * @param {Snapshot} dataSnapshot Optional: snapshot already containing model data. Prevents initial subscription on all values in the DataSource.
     * @param {Object} options Optional: options to pass to the dataSource if none is provided and a new one is constructed.
     * @returns {PrioritisedArray} PrioritisedArray instance.
     */
    constructor(dataType, dataSource = null, dataSnapshot = null, options = null, modelOptions = {}) {
        super();
        /**** Callbacks ****/
        this._valueChangedCallback = null;
        this._ids = {};

        /**** Private properties ****/
        this._dataType = dataType;
        this._dataSource = dataSource;
        this._isBeingReordered = false;
        this._modelOptions = modelOptions;
        /* Flag to determine when we're reordering so we don't listen to move updates */
        this._eventEmitter = new EventEmitter();

        /* Bind all local methods to the current object instance, so we can refer to "this"
         * in the methods as expected, even when they're called from event handlers.        */
        ObjectHelper.bindAllMethods(this, this);

        /* Hide all private properties (starting with '_') and methods from enumeration,
         * so when you do for( in ), only actual data properties show up. */
        ObjectHelper.hideMethodsAndPrivatePropertiesFromObject(this);

        /* Hide the priority field from enumeration, so we don't save it to the dataSource. */
        ObjectHelper.hidePropertyFromObject(Object.getPrototypeOf(this), 'length');

        /* If no dataSource is given, create own one with guessed path */
        if (!dataSource) {
            let path = Object.getPrototypeOf(this).constructor.name;
            // retrieve dataSource from the DI context
            dataSource = Context.getContext().get(DataSource);

            if (options) {
                dataSource = dataSource.child(options.path || path, options);
            } else {
                dataSource = dataSource.child(path);
            }

            this._dataSource = dataSource;
        }

        /* If a snapshot is present use it, otherwise generate one by subscribing to the dataSource one time. */
        if (dataSnapshot) {
            this._buildFromSnapshot(dataSnapshot);
        } else {
            this._buildFromDataSource(dataSource);
        }
    }

    /**
     * Subscribes to events emitted by this PrioritisedArray.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function that is called when the given event type is emitted.
     * @param {Object} context Optional: context of 'this' inside the handler function when it is called.
     * @returns {void}
     */
    on(event, handler, context) {
        /* If we're already ready, fire immediately */
        if ((event === 'ready' || event === 'value') && this._dataSource && this._dataSource.ready) {
            handler.call(context, this);
        }

        /* If we already have children stored locally when the subscriber calls this method,
         * fire their callback for all pre-existing children. */
        if (event === 'child_added') {
            for (let i = 0; i < this.length; i++) {
                let model = this[i];
                let previousSiblingID = i > 0 ? this[i - 1].id : null;
                handler.call(context, model, previousSiblingID);
            }
        }

        this._eventEmitter.on(event, handler, context);
    }

    /**
     * Subscribes to the given event type exactly once; it automatically unsubscribes after the first time it is triggered.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function that is called when the given event type is emitted.
     * @param {Object} context Optional: context of 'this' inside the handler function when it is called.
     * @returns {void}
     */
    once(event, handler, context = this) {
        return this.on(event, function onceWrapper() {
            handler.call(context, ...arguments);
            this.off(event, onceWrapper, context);
        }, this);
    }

    /**
     * Removes subscription to events emitted by this PrioritisedArray. If no handler or context is given, all handlers for
     * the given event are removed. If no parameters are given at all, all event types will have their handlers removed.
     * @param {String} event One of the following Event Types: 'value', 'child_changed', 'child_moved', 'child_removed'.
     * @param {Function} handler Function to remove from event callbacks.
     * @param {Object} context Object to bind the given callback function to.
     * @returns {void}
     */
    off(event, handler, context) {
        if (event && (handler || context)) {
            this._eventEmitter.removeListener(event, handler, context);
        } else {
            this._eventEmitter.removeAllListeners(event);
        }
    }

    /**
     * Adds a model instance to the rear of the PrioritisedArray, and emits a 'child_added' and possibly 'new_child' event after successful addition.
     * @param {Model} model Instance of a Model.
     * @param {String} prevSiblingId ID of the model preceding the one that will be added.
     * @returns {Object} Same model as the one originally passed as parameter.
     */
    add(model, prevSiblingId = null) {
        if (model instanceof this._dataType) {
            if (this._findIndexById(model.id) < 0) {

                if (prevSiblingId) {
                    let newPosition = this._findIndexById(prevSiblingId) + 1;
                    this._ids[model._id] = newPosition;
                    this.insertAt(model, newPosition);
                } else {
                    this.push(model);
                    this._ids[model._id] = this.length - 1;
                }

                /* If we've already received an on('value') result, this child addition is
                 * a new entry that wasn't on the dataSource before. */
                if(this._dataSource.ready) {
                    this._eventEmitter.emit('new_child', model, prevSiblingId);
                }

                this._eventEmitter.emit('child_added', model, prevSiblingId);
                return model;
            }
        } else if (model instanceof Object) {
            /* Let's try to parse the object using property reflection */
            var options = {dataSource: this._dataSource};
            let newModel = new this._dataType(null, model, _.extend({}, this._modelOptions, options));
            this.add(newModel);
            return newModel;
        } else {
            /* TODO: change to throw exception */
            console.log('Tried to append an object that is not the same type as the one this PrioritisedArray was created with.');
        }

        /* Return model so we can do this: let newModel = PrioArray.add(new Model()); newModel.someProperty = true; */
        return  null;
    }

    /**
     * Inserts a model instance at the given position of the PrioritisedArray, and recalculates the priority (position)
     * of all models after the inserted position.
     * @param {Model} model Subclass of Model
     * @param {Number} position Zero-based index where to put the new model instance.
     * @returns {Object} Same model as the one originally passed as parameter.
     */
    insertAt(model, position) {
        if (model instanceof this._dataType) {
            this.splice(position, 0, model);
            this._ids[model._id] = position;
        } else {
            /* TODO: change to throw exception */
            console.log('Tried to append an object that is not the same type as the PrioritisedArray was created with.');
        }

        /* Return model so we can do this: let newModel = PrioArray.add(new Model()); newModel.someProperty = true; */
        return model;
    }

    /**
     * Moves a model instance from one position to another.
     * @param {Number} fromPosition Zero-based index of original position
     * @param {Number} toPosition Zero-based index of target position
     * @returns {void}
     */
    move(fromPosition, toPosition) {
        let model = this[fromPosition];
        this._ids[model._id] = toPosition;
        this._ids[fromPosition] = null;
        this.splice(fromPosition, 1);
        this.splice(toPosition, 0, model);

    }

    /**
     * Removes the model instance at the given position. Does not remove the model from the datasource, to do that
     * call model.remove() directly, or PrioArray[index].remove().
     * @param {Number} position Index in the PrioritisedArray of the model to remove.
     * @returns {void}
     */
    remove(position) {
        this.splice(position, 1);
    }

    /**
     * Interprets all childs of a given snapshot as instances of the given data type for this PrioritisedArray,
     * and attempts to instantiate new model instances based on these sub-snapshots. It adds them to the
     * PrioritisedArray, which also assigns their priority based on their inserted position.
     * @param {Snapshot} dataSnapshot Snapshot to build the PrioritisedArray from.
     * @returns {void}
     * @private
     */
    _buildFromSnapshot(dataSnapshot) {

        let numChildren = dataSnapshot.numChildren(), currentChild = 1;

        /* If there is no data at this point yet, fire a ready event */
        if (numChildren === 0) {
            this._dataSource.ready = true;
            this._eventEmitter.emit('ready');
            this._eventEmitter.emit('value', this);
        }

        dataSnapshot.forEach(
            function (child) {
                /* Create a new instance of the given data type and prefill it with the snapshot data. */
                let options = {dataSnapshot: child};
                let childRef = this._dataSource.child(child.key());

                /* whenever the ref() is a datasource, we can bind that source to the model.
                 * whenever it's not a datasource, we assume the model should instantiate a new
                 * datasource to bind the model */

                if (childRef instanceof DataSource) {
                    options.dataSource = childRef;
                } else {
                    var rootPath = dataSnapshot.ref().root().toString();
                    options.path = dataSnapshot.ref().toString().replace(rootPath, '/');
                }

                let newModel = new this._dataType(child.key(), child.val(), _.extend({}, this._modelOptions, options));
                this.add(newModel);

                /* If this is the last child, fire a ready event */
                if (currentChild++ === numChildren) {
                    this._dataSource.ready = true;
                    this._eventEmitter.emit('ready');
                    this._eventEmitter.emit('value', this);
                }

            }.bind(this));
    }

    /**
     * Clones a dataSource (to not disturb any existing callbacks defined on the original) and uses it
     * to get a dataSnapshot which is used in _buildSnapshot to build our array.
     * @param {DataSource} dataSource DataSource to subscribe to for building the PrioritisedArray.
     * @returns {void}
     * @private
     */
    _buildFromDataSource(dataSource) {
        dataSource.once('value', (dataSnapshot) => {
            this._buildFromSnapshot(dataSnapshot);
            this._registerCallbacks(dataSource);
        });
    }

    /**
     * Registers the added, moved, changed, and removed callbacks to the given DataSource.
     * @param {DataSource} dataSource DataSource to register callbacks on.
     * @return {void}
     * @private
     */
    _registerCallbacks(dataSource) {
        dataSource.setChildAddedCallback(this._onChildAdded);
        dataSource.setChildMovedCallback(this._onChildMoved);
        dataSource.setChildChangedCallback(this._onChildChanged);
        dataSource.setChildRemovedCallback(this._onChildRemoved);
    }

    /**
     * Called by dataSource when a new child is added.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildAdded(snapshot, prevSiblingId) {
        let id = snapshot.key();
        let model = new this._dataType(id, null, {
            dataSnapshot: snapshot,
            dataSource: this._dataSource.child(id)
        });

        let previousPosition = this._findIndexById(id);
        if(previousPosition >= 0) {
            let oldModel = this[previousPosition];
            let oldProperties = ObjectHelper.getEnumerableProperties(oldModel);
            let newProperties = ObjectHelper.getEnumerableProperties(model);
            if (_.isEqual(oldProperties, newProperties)) { /* Child already exists. */
                return;
            }
        }

        this.add(model, prevSiblingId);

        if (!this._dataSource.ready) {
            this._dataSource.ready = true;
            this._eventEmitter.emit('ready');
        }
        this._eventEmitter.emit('value', this);
    }

    /**
     * Called by dataSource when a child is changed.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildChanged(snapshot, prevSiblingId) {
        let id = snapshot.key();
        let changedModel = new this._dataType(id, null, {dataSnapshot: snapshot, dataSource: snapshot.ref()});

        let previousPosition = this._findIndexById(id);
        if (previousPosition < 0) {
            /* The model doesn't exist, so we won't emit a changed event. */
            return;
        }

        this.remove(previousPosition);

        let newPosition = this._findIndexById(prevSiblingId) + 1;
        this.insertAt(changedModel, newPosition);

        this._eventEmitter.emit('child_changed', changedModel, prevSiblingId);
        this._eventEmitter.emit('value', this);
    }

    /**
     * Called by dataSource when a child is moved, which changes its priority.
     * @param {Snapshot} snapshot Snapshot of the added child.
     * @param {String} prevSiblingId ID of the model preceding the added model.
     * @returns {void}
     * @private
     */
    _onChildMoved(snapshot, prevSiblingId) {
        /* Ignore priority updates whilst we're reordering to avoid floods */
        if (!this._isBeingReordered) {

            let id = snapshot.key();
            let previousPosition = this._findIndexById(id);
            let tempModel = this[previousPosition];
            this._ids[id] = null;
            this.remove(previousPosition);

            let newPosition = this._findIndexById(prevSiblingId) + 1;
            this.insertAt(tempModel, newPosition);

            let model = this[newPosition];

            this._eventEmitter.emit('child_moved', model, previousPosition);
            this._eventEmitter.emit('value', this);
        }
    }

    /**
     * Called by dataSource when a child is removed.
     * @param {Snapshot} oldSnapshot Snapshot of the added child.
     * @returns {void}
     * @private
     */
    _onChildRemoved(oldSnapshot) {
        /* TODO: figure out if we can use the snapshot's priority as our array index reliably, to avoid big loops. */
        let id = oldSnapshot.key();
        let position = this._findIndexById(id);
        let model = this[position];

        if (position !== -1) {
            this._ids[id] = null;
            this.remove(position);

            this._eventEmitter.emit('child_removed', model);
            this._eventEmitter.emit('value', this);
        }
    }

    /**
     * Return the position of model's id, saved in an associative array
     * @param {Number} id Id field of the model we're looking for
     * @returns {Number} Zero-based index if found, -1 otherwise
     * @private
     */
    _findIndexById(id) {
        let position = this._ids[id];
        return (position == undefined || position == null) ? -1 : position;
    }

}
<|endoftext|>


<commit_before><commit_msg>Revert "Remove "Check all Purple Flowers" requirement for Quantum Bee"<commit_after><|endoftext|>


<commit_before>define(function(require, exports, module) {
    main.consumes = [
        "Plugin", "settings", "save", "collab", "tabManager", "dialog.error", "format"
    ];
    main.provides = ["format.custom"];
    return main;

    function main(options, imports, register) {
        var settings = imports.settings;
        var tabs = imports.tabManager;
        var save = imports.save;
        var format = imports.format;
        var collab = imports.collab;
        var showError = imports["dialog.error"].show;
        var Plugin = imports.Plugin;
        var plugin = new Plugin("Ajax.org", main.consumes);

        var ERROR_NOT_FOUND = 127;

        function load() {
            collab.on("beforeSave", beforeSave, plugin);
            collab.on("postProcessorError", function(e) {
                var mode = getMode(e.docId) || "language";
                if (e.code !== ERROR_NOT_FOUND)
                    return console.error("Error running formatter for " + mode + ": " + (e.stderr || e.code));
                var formatter = (settings.get("project/" + mode + "/@formatter") || "formatter").replace(/(.*?) .*/, "$1");
                showError("Error running code formatter for " + mode + ": "
                    + formatter + " not found, please check your project settings");
            });
            format.on("format", function(e) {
                if (!settings.get("project/" + e.mode + "/@formatOnSave"))
                    return;
                if (e.mode === "javascript" && settings.getBool("project/javascript/@use_jsbeautify"))
                    return; // use built-in JS Beautify instead
                save.save(tabs.currentTab);
                return true;
            }, plugin);
        }

        function beforeSave(e) {
            var mode = getMode(e.docId);
            var enabled = settings.getBool("project/" + mode + "/@formatOnSave");
            if (!enabled)
                return;
            if (mode === "javascript" && settings.getBool("project/javascript/@use_jsbeautify"))
                return; // use built-in JS Beautify instead
            var formatter = settings.get("project/" + mode + "/@formatter");
            if (!formatter)
                return showError("No code formatter set for " + mode + ": please check your project settings");
            e.postProcessor = {
                command: "bash",
                args: ["-c", formatter]
            };
        }

        function getMode(docId) {
            var tab = tabs.findTab(docId);
            if (!tab || !tab.editor || !tab.editor.ace)
                return;
            return tab.editor.ace.session.syntax;
        }

        plugin.on("load", function() {
            load();
        });
        plugin.on("unload", function() {

        });

        /**
         * Custom code formatter extension
         *
         * Reformats code in the current document on save
         */
        plugin.freezePublicAPI({});

        register(null, {
            "format.custom": plugin
        });
    }
});
<commit_msg>fix custom formatter for the case when formatOnSave is disabled<commit_after>define(function(require, exports, module) {
    main.consumes = [
        "Plugin", "settings", "save", "collab", "tabManager", "dialog.error", "format"
    ];
    main.provides = ["format.custom"];
    return main;

    function main(options, imports, register) {
        var settings = imports.settings;
        var tabs = imports.tabManager;
        var save = imports.save;
        var format = imports.format;
        var collab = imports.collab;
        var showError = imports["dialog.error"].show;
        var Plugin = imports.Plugin;
        var plugin = new Plugin("Ajax.org", main.consumes);

        var ERROR_NOT_FOUND = 127;

        var fromFormatCommand;

        function load() {
            collab.on("beforeSave", function(e) {
                var mode = getMode(e.docId);
                var enabled = settings.getBool("project/" + mode + "/@formatOnSave");
                if (!enabled && !fromFormatCommand)
                    return;
                if (mode === "javascript" && settings.getBool("project/javascript/@use_jsbeautify"))
                    return; // use built-in JS Beautify instead
                var formatter = settings.get("project/" + mode + "/@formatter");
                if (!formatter)
                    return showError("No code formatter set for " + mode + ": please check your project settings");
                e.postProcessor = {
                    command: "bash",
                    args: ["-c", formatter]
                };
            }, plugin);
            collab.on("postProcessorError", function(e) {
                var mode = getMode(e.docId) || "language";
                if (e.code !== ERROR_NOT_FOUND)
                    return console.error("Error running formatter for " + mode + ": " + (e.stderr || e.code));
                var formatter = (settings.get("project/" + mode + "/@formatter") || "formatter").replace(/(.*?) .*/, "$1");
                showError("Error running code formatter for " + mode + ": "
                    + formatter + " not found, please check your project settings");
            });
            format.on("format", function(e) {
                if (!settings.get("project/" + e.mode + "/@formatter"))
                    return;
                if (e.mode === "javascript" && settings.getBool("project/javascript/@use_jsbeautify"))
                    return; // use built-in JS Beautify instead
                fromFormatCommand = true;
                save.save(tabs.focussedTab);
                fromFormatCommand = false;
                return true;
            }, plugin);
        }

        function getMode(docId) {
            var tab = tabs.findTab(docId);
            if (!tab || !tab.editor || !tab.editor.ace)
                return;
            return tab.editor.ace.session.syntax;
        }

        plugin.on("load", function() {
            load();
        });
        plugin.on("unload", function() {
            fromFormatCommand = false;
        });

        /**
         * Custom code formatter extension
         *
         * Reformats code in the current document on save
         */
        plugin.freezePublicAPI({});

        register(null, {
            "format.custom": plugin
        });
    }
});
<|endoftext|>


<commit_before>// Clock Visualization

// Visualization of tracking data over 24 hours

(function(visualizations){
"use strict";
const CX = 0;
const CY = 0;
const CENTRE = CX + ',' + CY;
const DOT_TRANS = 'translate(305, 5)';
const HAND_TRANS = 'translate(205, 5)';
const TIME_TRANS = 'translate(0, 5)';
const SVG_NS = 'http://www.w3.org/2000/svg';
const TIME_X1 = -275;
const TIME_X2 = 270 ;
const TIME_Y = CY - 5;

var vizcanvas, times, timeslots, offsets;

// TODO: Make visualization an event emitter, so I can call on('connection', fn) and emit('connection', connection)

var clock = new Emitter();
visualizations.clock = clock;
clock.name = "clock";

clock.on('init', onInit);
clock.on('connection', onConnection);
clock.on('remove', onRemove);

function onInit(connections){
    // draw clock dial
    console.log('initializing clock from %s connections', connections.length);
    vizcanvas = document.querySelector('.vizcanvas');
    aggregate.emit('init', connections);
    times = ['12 am', '1 am', '2 am', '3 am', '4 am', '5 am', '6 am', '7 am', '8 am', '9 am', '10 am', '11 am', '12 pm', '1 pm', '2 pm', '3 pm', '4 pm', '5 pm', '6 pm', '7 pm', '8 pm', '9 pm', '10 pm', '11 pm', '12 am'];
    timeslots = {};
    offsets = [':00', ':15', ':30', ':45'];
    times.slice(1).forEach(function(time){
        timeslots[time] = {':00': [], ':15': [], ':30': [], ':45': [] };
    });
    vizcanvas.setAttribute('viewBox', '-350 -495 700 500');
    drawTimes();
    updateTime();
    connections.forEach(function(connection){
        onConnection(connection);
    });
    fadeEarlierTrackers(timeToBucket(new Date()));
};

function onConnection(connection){
    // A connection has the following keys:
    // source (url), target (url), timestamp (int), contentType (str), cookie (bool), sourceVisited (bool), secure(bool), sourcePathDepth (int), sourceQueryDepth(int)
    aggregate.emit('connection', connection);
    var bucketIdx = timeToBucket(connection.timestamp);
    if (! clock.timeslots[bucketIdx]){
        var angle = -180 + (bucketIdx * 1.875); // in degrees
        clock.timeslots[bucketIdx] = {
            group: svg('g', {
                transform: 'rotate(' + angle + ' ' + CENTRE + ') ' + DOT_TRANS
            }),
            connections: []
        }
        vizcanvas.appendChild(clock.timeslots[bucketIdx].group);
    }
    var bucket = clock.timeslots[bucketIdx];
    var connectionIdx = bucket.connections.length;
    // see if we've already added this source-target pair to the visualization
    var existing = bucket.connections.filter(function(oldConnection){
        return connection.source === oldConnection.source && connection.target === oldConnection.target;
    });
    if (existing.length){
        if (existing.length > 1){
            throw new Error('There can be only one!');
        }
        existing[0].howMany += 1;
        existing[0].view.setAttribute('data-how-many', parseInt(existing[0].view.getAttribute('data-how-many'), 10) + 1);
        return; // bail early if we've already added to visualization
    }else{
        connection.howMany = 1;
        bucket.connections.push(connection);
    }
    var g = svg('g', {
        // transform: 'rotate(90)',
        'class': 'tracker node',
        'data-target': connection.target,
        'data-name': connection.target,
        'data-timestamp': connection.timestamp.toISOString(),
        'data-source': connection.source,
        'data-cookie': connection.cookie,
        'data-source-visited': connection.sourceVisited,
        'data-content-type': connection.contentType,
        'data-how-many': 1
    });
    var x = connectionIdx * 10;
    var y = 0;
    g.appendChild(svg('circle', {
        cx: x,
        cy: y,
        r: 3,
        'class': 'tracker'
    }));
    connection.view = g;
    tooltip.add(g);
    bucket.group.appendChild(g);
}

function onRemove(){
    clearTimeout(handTimer);
    clock.timeslots = new Array(96);
    resetCanvas();
};

function svg(name, attrs, text){
    var node = document.createElementNS(SVG_NS, name);
    if (attrs){
        Object.keys(attrs).forEach(function(key){
            node.setAttribute(key, attrs[key]);
        });
    }
    if (text){
        node.appendChild(document.createTextNode(text));
    }
    return node;
}

function resetCanvas(){
    // You will still need to remove timer events
    var parent = vizcanvas.parentNode;
    var newcanvas = vizcanvas.cloneNode(false);
    parent.replaceChild(newcanvas, vizcanvas);
    vizcanvas = newcanvas;
}

function addTracker(tracker){
    var timestamp = new Date(tracker.timestamp);
    var hour = timestamp.getHours();
    var meridian = (hour < 12) ? 'am' : 'pm';
    hour = (hour % 12) + 1; // convert to 12-hour time
    var minutes = timestamp.getMinutes() + 1;
    var offset;
    if (minutes < 15){
        offset = ':00';
    }else if(minutes < 30){
        offset = ':15';
    }else if(minutes < 45){
        offset = ':30';
    }else{
        offset = ':45';
    }
    var time = hour + ' ' + meridian;
    timeslots[time][offset].push(tracker);
    var count = timeslots[time][offset].length;
}

function timeToAngle(date){
    return (date.getHours() * 4 + Math.floor(date.getMinutes() / 15)) * 1.875;
}

function drawTimes(){
    var text;
    // Draw arcs
    vizcanvas.appendChild(svg('path', {
        stroke: '#999', // move to CSS
        fill: 'none',
        'stroke-width': 90,
        d: 'M-250,5 A185,185 0 0,1 250,5'
    }));
    vizcanvas.appendChild(svg('path', {
        stroke: '#CCC', // move to CSS
        fill: 'none',
        'stroke-width': 35,
        d: 'M-275,5 A205,205 0 0,1 275,5'
    }));
    // Draw all the :00 time increment titles
    times.slice(0,13).forEach(function(time, idx){
        vizcanvas.appendChild(svg('text', {
            x: TIME_X1,
            y: TIME_Y,
            transform: 'rotate(' + (7.5 * idx) + ' ' + CENTRE + ') ' + TIME_TRANS
        }, time)
    );});
    times.slice(13).reverse().forEach(function(time, idx){
        vizcanvas.appendChild(svg('text', {
            x: TIME_X2,
            y: TIME_Y,
            transform: 'rotate(' + (-7.5 * idx) + ' ' + CENTRE + ') ' + TIME_TRANS
        }, time)
    )});
}

function drawText(){
    var clocktime = document.querySelector('.clock-time');
    if (!clocktime){
        clocktime = svg('text', {
            x: 0,
            y: -80,
            'class': 'clock-time'
        }, timeNow());
        vizcanvas.appendChild(clocktime);
    }else{
        clocktime.firstChild.data = timeNow();
    }
    var clockdate = document.querySelector('.clock-date');
    if (!clockdate){
        clockdate = svg('text', {
            x: 0,
            y: -40,
            'class': 'clock-date'
        }, dateNow());
        vizcanvas.appendChild(clockdate);
    }else{
        clockdate.firstChild.data = dateNow();
    }
}

function timeNow(){
    var d = new Date();
    return (d.getHours() % 12) + ':' + d.toLocaleFormat('%M') + ['am','pm'][Math.round(d.getHours() / 12)];
}

function dateNow(){
    return new Date().toLocaleFormat('%e %B %Y').trim();
}

clock.timeslots = new Array(96);

function timeToBucket(timestamp){
    return timestamp.getHours() * 4 + Math.floor(timestamp.getMinutes() / 15);
}

function fadeEarlierTrackers(currentBucketIdx){
    // currentBucketIdx is the index of the bucket (0 - 95)
    // clock.timeslots[currentBucketIdx] is the current bucket
    // bucket.group is the SVG <g> container
    // bucket.connections is the array of connections
    //
    // Set opacity for each bucket
    // Clear items from the next bucket
    var total = clock.timeslots.length;
    clock.timeslots.forEach(function(bucket, idx){
        if (bucket){
            var distance = ((total + currentBucketIdx) - idx) % total; // how far behind currentBucketIdx?
            bucket.group.style.opacity = (100 - distance) / 100;
        }
    });
    var nextBucket = clock.timeslots[(currentBucketIdx + 1) % total];
    if (nextBucket){
        var group = nextBucket.group;
        while(group.firstChild){
            group.removeChild(group.firstChild);
        }
        nextBucket.connections.length = 0;
    }
}

var clockTimer = null;
var lastBucket = null;

function drawTimerHand(time){
    console.log('drawTimerHand');
    if (!time) time = new Date();
    var hand = document.getElementById('timerhand');
    if (!hand){
        var hand = svg('g', {id: 'timerhand'});
        hand.appendChild(svg('line', {x1: 0, y1: 0, x2: 400, y2: 0}));
        hand.appendChild(svg('path', {d: 'M47,-8 L47,8 73,5 73,-5 Z'}));
    }
    vizcanvas.appendChild(hand);
    if (!lastBucket){
        lastBucket = timeToBucket(time);
    }
    if (lastBucket !== timeToBucket(time)){
        lastBucket = timeToBucket(time);
        fadeEarlierTrackers(lastBucket);
    }
    hand.setAttribute('transform', 'rotate(' + (timeToAngle(time) - 180) + ' ' + CENTRE + ') ' + HAND_TRANS);
}

function updateTime(){
    drawTimerHand();
    drawText();
    clockTimer = setTimeout(updateTime, 1000);
}

})(visualizations);

<commit_msg>fix clock updating<commit_after>// Clock Visualization

// Visualization of tracking data over 24 hours

(function(visualizations){
"use strict";
const CX = 0;
const CY = 0;
const CENTRE = CX + ',' + CY;
const DOT_TRANS = 'translate(305, 5)';
const HAND_TRANS = 'translate(205, 5)';
const TIME_TRANS = 'translate(0, 5)';
const SVG_NS = 'http://www.w3.org/2000/svg';
const TIME_X1 = -275;
const TIME_X2 = 270 ;
const TIME_Y = CY - 5;

var vizcanvas, times, timeslots, offsets;

// TODO: Make visualization an event emitter, so I can call on('connection', fn) and emit('connection', connection)

var clock = new Emitter();
visualizations.clock = clock;
clock.name = "clock";

clock.on('init', onInit);
clock.on('connection', onConnection);
clock.on('remove', onRemove);

function onInit(connections){
    // draw clock dial
    console.log('initializing clock from %s connections', connections.length);
    vizcanvas = document.querySelector('.vizcanvas');
    aggregate.emit('init', connections);
    times = ['12 am', '1 am', '2 am', '3 am', '4 am', '5 am', '6 am', '7 am', '8 am', '9 am', '10 am', '11 am', '12 pm', '1 pm', '2 pm', '3 pm', '4 pm', '5 pm', '6 pm', '7 pm', '8 pm', '9 pm', '10 pm', '11 pm', '12 am'];
    timeslots = {};
    offsets = [':00', ':15', ':30', ':45'];
    times.slice(1).forEach(function(time){
        timeslots[time] = {':00': [], ':15': [], ':30': [], ':45': [] };
    });
    vizcanvas.setAttribute('viewBox', '-350 -495 700 500');
    drawTimes();
    updateTime();
    connections.forEach(function(connection){
        onConnection(connection);
    });
    fadeEarlierTrackers(timeToBucket(new Date()));
};

function onConnection(connection){
    // A connection has the following keys:
    // source (url), target (url), timestamp (int), contentType (str), cookie (bool), sourceVisited (bool), secure(bool), sourcePathDepth (int), sourceQueryDepth(int)
    aggregate.emit('connection', connection);
    var bucketIdx = timeToBucket(connection.timestamp);
    if (! clock.timeslots[bucketIdx]){
        var angle = -180 + (bucketIdx * 1.875); // in degrees
        clock.timeslots[bucketIdx] = {
            group: svg('g', {
                transform: 'rotate(' + angle + ' ' + CENTRE + ') ' + DOT_TRANS
            }),
            connections: []
        }
        vizcanvas.appendChild(clock.timeslots[bucketIdx].group);
    }
    var bucket = clock.timeslots[bucketIdx];
    var connectionIdx = bucket.connections.length;
    // see if we've already added this source-target pair to the visualization
    var existing = bucket.connections.filter(function(oldConnection){
        return connection.source === oldConnection.source && connection.target === oldConnection.target;
    });
    if (existing.length){
        if (existing.length > 1){
            throw new Error('There can be only one!');
        }
        existing[0].howMany += 1;
        existing[0].view.setAttribute('data-how-many', parseInt(existing[0].view.getAttribute('data-how-many'), 10) + 1);
        return; // bail early if we've already added to visualization
    }else{
        connection.howMany = 1;
        bucket.connections.push(connection);
    }
    var g = svg('g', {
        // transform: 'rotate(90)',
        'class': 'tracker node',
        'data-target': connection.target,
        'data-name': connection.target,
        'data-timestamp': connection.timestamp.toISOString(),
        'data-source': connection.source,
        'data-cookie': connection.cookie,
        'data-source-visited': connection.sourceVisited,
        'data-content-type': connection.contentType,
        'data-how-many': 1
    });
    var x = connectionIdx * 10;
    var y = 0;
    g.appendChild(svg('circle', {
        cx: x,
        cy: y,
        r: 3,
        'class': 'tracker'
    }));
    connection.view = g;
    tooltip.add(g);
    bucket.group.appendChild(g);
}

function onRemove(){
    clearTimeout(handTimer);
    clock.timeslots = new Array(96);
    resetCanvas();
};

function svg(name, attrs, text){
    var node = document.createElementNS(SVG_NS, name);
    if (attrs){
        Object.keys(attrs).forEach(function(key){
            node.setAttribute(key, attrs[key]);
        });
    }
    if (text){
        node.appendChild(document.createTextNode(text));
    }
    return node;
}

function resetCanvas(){
    // You will still need to remove timer events
    var parent = vizcanvas.parentNode;
    var newcanvas = vizcanvas.cloneNode(false);
    parent.replaceChild(newcanvas, vizcanvas);
    vizcanvas = newcanvas;
}

function addTracker(tracker){
    var timestamp = new Date(tracker.timestamp);
    var hour = timestamp.getHours();
    var meridian = (hour < 12) ? 'am' : 'pm';
    hour = (hour % 12) + 1; // convert to 12-hour time
    var minutes = timestamp.getMinutes() + 1;
    var offset;
    if (minutes < 15){
        offset = ':00';
    }else if(minutes < 30){
        offset = ':15';
    }else if(minutes < 45){
        offset = ':30';
    }else{
        offset = ':45';
    }
    var time = hour + ' ' + meridian;
    timeslots[time][offset].push(tracker);
    var count = timeslots[time][offset].length;
}

function timeToAngle(date){
    return (date.getHours() * 4 + Math.floor(date.getMinutes() / 15)) * 1.875;
}

function drawTimes(){
    var text;
    // Draw arcs
    vizcanvas.appendChild(svg('path', {
        stroke: '#999', // move to CSS
        fill: 'none',
        'stroke-width': 90,
        d: 'M-250,5 A185,185 0 0,1 250,5'
    }));
    vizcanvas.appendChild(svg('path', {
        stroke: '#CCC', // move to CSS
        fill: 'none',
        'stroke-width': 35,
        d: 'M-275,5 A205,205 0 0,1 275,5'
    }));
    // Draw all the :00 time increment titles
    times.slice(0,13).forEach(function(time, idx){
        vizcanvas.appendChild(svg('text', {
            x: TIME_X1,
            y: TIME_Y,
            transform: 'rotate(' + (7.5 * idx) + ' ' + CENTRE + ') ' + TIME_TRANS
        }, time)
    );});
    times.slice(13).reverse().forEach(function(time, idx){
        vizcanvas.appendChild(svg('text', {
            x: TIME_X2,
            y: TIME_Y,
            transform: 'rotate(' + (-7.5 * idx) + ' ' + CENTRE + ') ' + TIME_TRANS
        }, time)
    )});
}

function drawText(){
    var clocktime = document.querySelector('.clock-time');
    if (!clocktime){
        clocktime = svg('text', {
            x: 0,
            y: -80,
            'class': 'clock-time'
        }, timeNow());
        vizcanvas.appendChild(clocktime);
    }else{
        clocktime.firstChild.data = timeNow();
    }
    var clockdate = document.querySelector('.clock-date');
    if (!clockdate){
        clockdate = svg('text', {
            x: 0,
            y: -40,
            'class': 'clock-date'
        }, dateNow());
        vizcanvas.appendChild(clockdate);
    }else{
        clockdate.firstChild.data = dateNow();
    }
}

function timeNow(){
    var d = new Date();
    return (d.getHours() % 12) + ':' + d.toLocaleFormat('%M') + ['am','pm'][Math.floor(d.getHours() / 12)];
}

function dateNow(){
    return new Date().toLocaleFormat('%e %B %Y').trim();
}

clock.timeslots = new Array(96);

function timeToBucket(timestamp){
    return timestamp.getHours() * 4 + Math.floor(timestamp.getMinutes() / 15);
}

function fadeEarlierTrackers(currentBucketIdx){
    // currentBucketIdx is the index of the bucket (0 - 95)
    // clock.timeslots[currentBucketIdx] is the current bucket
    // bucket.group is the SVG <g> container
    // bucket.connections is the array of connections
    //
    // Set opacity for each bucket
    // Clear items from the next bucket
    var total = clock.timeslots.length;
    clock.timeslots.forEach(function(bucket, idx){
        if (bucket){
            var distance = ((total + currentBucketIdx) - idx) % total; // how far behind currentBucketIdx?
            bucket.group.style.opacity = (100 - distance) / 100;
        }
    });
    var nextBucket = clock.timeslots[(currentBucketIdx + 1) % total];
    if (nextBucket){
        var group = nextBucket.group;
        while(group.firstChild){
            group.removeChild(group.firstChild);
        }
        nextBucket.connections.length = 0;
    }
}

var clockTimer = null;
var lastBucket = null;

function drawTimerHand(time){
    console.log('drawTimerHand');
    if (!time) time = new Date();
    var hand = document.getElementById('timerhand');
    if (!hand){
        var hand = svg('g', {id: 'timerhand'});
        hand.appendChild(svg('line', {x1: 0, y1: 0, x2: 400, y2: 0}));
        hand.appendChild(svg('path', {d: 'M47,-8 L47,8 73,5 73,-5 Z'}));
    }
    vizcanvas.appendChild(hand);
    if (!lastBucket){
        lastBucket = timeToBucket(time);
    }
    if (lastBucket !== timeToBucket(time)){
        lastBucket = timeToBucket(time);
        fadeEarlierTrackers(lastBucket);
    }
    hand.setAttribute('transform', 'rotate(' + (timeToAngle(time) - 180) + ' ' + CENTRE + ') ' + HAND_TRANS);
}

function updateTime(){
    drawTimerHand();
    drawText();
    clockTimer = setTimeout(updateTime, 1000);
}

})(visualizations);

<|endoftext|>