<commit_before>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2014, Olivier BIOT
 * http://www.melonjs.org
 *
 */

(function($) {

    /**
     * Particle Emitter Object.
     * @class
     * @extends Object
     * @memberOf me
     * @constructor
     * @param {me.Vector2d} pos position of the particle emitter
     * @param {Image} image reference to the Particle Image. See {@link me.loader#getImage}
     * @example
     *
     * // Create a basic emitter at position 100, 100
     * var emitter = new me.ParticleEmitter(100, 100, me.loader.getImage("smoke"));
     *
     * // Adjust the emitter properties
     * emitter.totalParticles = 200;
     * emitter.minLife = 1000;
     * emitter.maxLife = 3000;
     * emitter.z = 10;
     *
     * // Add the emitter to the game world
     * me.game.world.addChild(emitter);
     *
     * // Launch all particles one time and stop, like a explosion
     * emitter.burstParticles();
     *
     * // Launch constantly the particles, like a fountain
     * emitter.streamParticles();
     *
     * // At the end, remove emitter from the game world
     * // call this in onDestroyEvent function
     * me.game.world.removeChild(emitter);
     *
     */
    me.ParticleEmitter = me.Renderable.extend(
    /** @scope me.ParticleEmitter.prototype */
    {
        // Emitter is Stream, launch particles constantly
            /** @ignore */
        _stream: false,

        // Frequency timer (in ms) for emitter launch new particles
        // used only in stream emitter
            /** @ignore */
        _frequencyTimer: 0,

        // Time of live (in ms) for emitter launch new particles
        // used only in stream emitter
            /** @ignore */
        _durationTimer: 0,

        // Emitter is emitting particles
            /** @ignore */
        _enabled: false,

        /**
         * @ignore
         */
        init: function(x, y, image) {
            var pos = new me.Vector2d(x, y);

            // call the parent constructor
            this.parent(pos, 1, 1);

            // Emitter will always update
            this.alwaysUpdate = true;

            // Cache the emitter start pos
            this._defaultPos = pos;

            // Cache the emitter start image
            this._defaultImage = image;

            // hold particles in an array
            this._particles = [];

            // keep count of how many particles are dead
            this._deadCount = 0;

            // Reset the emitter to defaults
            this.reset();

            /**
             * Z-order for particles

             * default value : 0
             * @type Number
             * @name z
             * @memberOf me.ParticleEmitter
             */
            this.z = 0;
        },

        /**
         * Reset the Emitter with defaults params

         * @function
         * @param {Object} params [optional] object with emitter params
         * @name reset
         * @memberOf me.ParticleEmitter
         */
        reset: function(params) {
            // check if params exists and create a dummy object
            params = params || {};

            /**
             * Start position for launch particles

             * default value : x, y

             * @public
             * @type me.Vector2d
             * @name pos
             * @memberOf me.ParticleEmitter
             */
            this.pos = params.pos || this._defaultPos.clone();

            /**
             * Variation in the start position for launch the particles (x, y)

             * Random value in range [pos - varPos, pos + varPos]

             * default value : 0, 0

             * @public
             * @type me.Vector2d
             * @name varPos
             * @memberOf me.ParticleEmitter
             */
            this.varPos = params.varPos || new me.Vector2d(0, 0);

            /**
             * Image used for particles

             * @public
             * @type me.SpriteObject
             * @name image
             * @memberOf me.ParticleEmitter
             */
            this.image = params.image || this._defaultImage;

            /**
             * Total number of particles in this emitter

             * default value : 50

             * @public
             * @type Number
             * @name totalParticles
             * @memberOf me.ParticleEmitter
             */
            this.totalParticles = params.totalParticles || 50;

            /**
             * Minimum start angle for launch particles in Radians

             * default value : Math.PI / 2

             * @public
             * @type Number
             * @name minAngle
             * @memberOf me.ParticleEmitter
             */
            this.minAngle = params.minAngle || (Math.PI / 2);

            /**
             * Maximum start angle for launch particles in Radians

             * default value : Math.PI / 2

             * @public
             * @type Number
             * @name maxAngle
             * @memberOf me.ParticleEmitter
             */
            this.maxAngle = params.maxAngle || (Math.PI / 2);

            /**
             * Minimum time each particle lives once it is emitted in ms

             * default value : 1000

             * @public
             * @type Number
             * @name minLife
             * @memberOf me.ParticleEmitter
             */
            this.minLife = params.minLife || 1000;

            /**
             * Maximum time each particle lives once it is emitted in ms

             * default value : 3000

             * @public
             * @type Number
             * @name maxLife
             * @memberOf me.ParticleEmitter
             */
            this.maxLife = params.maxLife || 3000;

            /**
             * Minimum start speed for particles

             * default value : 1

             * @public
             * @type Number
             * @name minSpeed
             * @memberOf me.ParticleEmitter
             */
            this.minSpeed = params.minSpeed || 1;

            /**
             * Maximum start speed for particles

             * default value : 3

             * @public
             * @type Number
             * @name maxSpeed
             * @memberOf me.ParticleEmitter
             */
            this.maxSpeed = params.maxSpeed || 3;

            /**
             * Minimum start rotation for particles sprites in Radians

             * default value : 0

             * @public
             * @type Number
             * @name minRotation
             * @memberOf me.ParticleEmitter
             */
            this.minRotation = params.minRotation || 0;

            /**
             * Maximum start rotation for particles sprites in Radians

             * default value : 0

             * @public
             * @type Number
             * @name maxRotation
             * @memberOf me.ParticleEmitter
             */
            this.maxRotation = params.maxRotation || 0;

            /**
             * Minimum start scale ratio for particles (1 = no scaling)

             * default value : 1

             * @public
             * @type Number
             * @name minStartScale
             * @memberOf me.ParticleEmitter
             */
            this.minStartScale = params.minStartScale || 1;

            /**
             * Maximum start scale ratio for particles (1 = no scaling)

             * default value : 1

             * @public
             * @type Number
             * @name maxStartScale
             * @memberOf me.ParticleEmitter
             */
            this.maxStartScale = params.maxStartScale || 1;

            /**
             * Minimum end scale ratio for particles

             * default value : 0

             * @public
             * @type Number
             * @name minEndScale
             * @memberOf me.ParticleEmitter
             */
            this.minEndScale = params.minEndScale || 0;

            /**
             * Maximum end scale ratio for particles

             * default value : 0

             * @public
             * @type Number
             * @name maxEndScale
             * @memberOf me.ParticleEmitter
             */
            this.maxEndScale = params.maxEndScale || 0;

            /**
             * Vertical force (Gravity) for each particle

             * default value : me.sys.gravity or 0

             * @public
             * @type Number
             * @name gravity
             * @memberOf me.ParticleEmitter
             */
            this.gravity = params.gravity || me.sys.gravity || 0;

            /**
             * Horizontal force (like a Wind) for each particle

             * default value : 0

             * @public
             * @type Number
             * @name wind
             * @memberOf me.ParticleEmitter
             */
            this.wind = params.wind || 0;

            /**
             * Update the rotation of particle in accordance the particle trajectory

             * The particle sprite should aim at zero angle (draw from left to right)

             * Override the particle minRotation and maxRotation

             * default value : false

             * @public
             * @type Boolean
             * @name followTrajectory
             * @memberOf me.ParticleEmitter
             */
            this.followTrajectory = params.followTrajectory || false;

            /**
             * Enable the Texture Additive by canvas composite operation (lighter)

             * WARNING: Composite Operation may decreases performance!

             * default value : false

             * @public
             * @type Boolean
             * @name textureAdditive
             * @memberOf me.ParticleEmitter
             */
            this.textureAdditive = params.textureAdditive || false;

            /**
             * Update particles only in the viewport, remove it when out of viewport

             * default value : true

             * @public
             * @type Boolean
             * @name onlyInViewport
             * @memberOf me.ParticleEmitter
             */
            this.onlyInViewport = params.onlyInViewport || true;

            /**
             * Maximum number of particles launched each time in this emitter (used only if emitter is Stream)

             * default value : 10

             * @public
             * @type Number
             * @name maxParticles
             * @memberOf me.ParticleEmitter
             */
            this.maxParticles = params.maxParticles || 10;

            /**
             * How often a particle is emitted in ms (used only if emitter is Stream)

             * Necessary that value is greater than zero

             * default value : 100

             * @public
             * @type Number
             * @name frequency
             * @memberOf me.ParticleEmitter
             */
            this.frequency = params.frequency || 100;

            /**
             * Duration that the emitter releases particles in ms (used only if emitter is Stream)

             * After this period, the emitter stop the launch of particles

             * default value : Infinity

             * @public
             * @type Number
             * @name duration
             * @memberOf me.ParticleEmitter
             */
            this.duration = params.duration || Infinity;

            /**
             * Determines how many particles can die before the internal cache is cleaned up.

             * default value : 10

             * @public
             * @type Number
             * @name cleanupThreshold
             * @memberOf me.ParticleEmitter
             */
            this.cleanupThreshold = params.cleanupThreshold || 10;

            // clear particle array
            this._particles.length = 0;
        },

        // Add count particles in the game world
        /** @ignore */
        addParticles: function(count) {
            for (var i = 0; i < ~~count; i++) {
                // Add particle in the emitter
                this._particles.push(me.entityPool.newInstanceOf("me.Particle", this));
            }
        },

        /**
         * Emitter is of type stream and is launching particles

         * @function
         * @returns {Boolean} Emitter is Stream and is launching particles
         * @name isRunning
         * @memberOf me.ParticleEmitter
         */
        isRunning: function() {
            return this._enabled && this._stream;
        },

        /**
         * Launch particles from emitter constantly

         * Particles example: Fountains
         * @param {Number} duration [optional] time that the emitter releases particles in ms
         * @function
         * @name streamParticles
         * @memberOf me.ParticleEmitter
         */
        streamParticles: function(duration) {
            this._enabled = true;
            this._stream = true;
            this.frequency = Math.max(this.frequency, 1);
            this._durationTimer = duration || this.duration;
        },

        /**
         * Stop the emitter from generating new particles (used only if emitter is Stream)

         * @function
         * @name stopStream
         * @memberOf me.ParticleEmitter
         */
        stopStream: function() {
            this._enabled = false;
        },

        /**
         * Launch all particles from emitter and stop

         * Particles example: Explosions

         * @param {Number} total [optional] number of particles to launch
         * @function
         * @name burstParticles
         * @memberOf me.ParticleEmitter
         */
        burstParticles: function(total) {
            this._enabled = true;
            this._stream = false;
            this.addParticles(total || this.totalParticles);
            this._enabled = false;
        },

        /**
         * Update the Emitter

         * This is automatically called by the game manager {@link me.game}
         * @name update
         * @memberOf me.ParticleEmitter
         * @function
         * @ignore
         * @param {Number} dt time since the last update in milliseconds
         */
        update: function(dt) {
            // Launch new particles, if emitter is Stream
            if ((this._enabled) && (this._stream)) {
                // Check if the emitter has duration set
                if (this._durationTimer !== Infinity) {
                    this._durationTimer -= dt;

                    if (this._durationTimer <= 0) {
                        this.stopStream();
                        return false;
                    }
                }

                // Increase the emitter launcher timer
                this._frequencyTimer += dt;

                // Check for new particles launch
                var particlesCount = this._particles.length - this._deadCount;
                if ((particlesCount < this.totalParticles) && (this._frequencyTimer >= this.frequency)) {
                    if ((particlesCount + this.maxParticles) <= this.totalParticles)
                        this.addParticles(this.maxParticles);
                    else
                        this.addParticles(this.totalParticles - particlesCount);

                    this._frequencyTimer = 0;
                }
            }

            // Update particles if they are not dead yet
            var particlesCount = this._particles.length;
			var viewport = me.game.viewport;
            var deadCount = 0;
            for ( var i = 0; i < particlesCount; ++i) {
                var particle = this._particles[i];
                if(!particle.isDead) {
                    particle.inViewport = viewport.isVisible(particle);
                    particle.update(dt);
                } else {
                	deadCount++;
                }
            }
            this._deadCount = deadCount;

            // Free dead particles if there are enough of them.
            if(deadCount > this.cleanupThreshold) {
                this._particles.sort(this._deadLast);
                while(deadCount--) {
                    var particle = this._particles.pop();
                    me.entityPool.freeInstance(particle);
                }
            }
        },

        /**
         * Move dead particles to the end of the array.
         * @ignore
         */
        _deadLast: function(a, b) {
            if(a.isDead && b.isDead)
                return 0;
            else if(a.isDead)
                return 1;
            else if(b.isDead)
                return -1;
            return 0;
        },

        /**
         * @ignore
         */
        draw : function(context) {
            var particlesCount = this._particles.length;
            if(particlesCount > 0) {
                // save context
                context.save();

                var originalAlpha = context.globalAlpha;

                // Check for additive draw
                if (this.textureAdditive) {
                    context.globalCompositeOperation = "lighter";
                }

                for ( var i = 0; i < particlesCount; ++i) {
                    var particle = this._particles[i];
                    if(!particle.isDead) {
                        // particle alpha value
                        context.globalAlpha = originalAlpha * particle.getOpacity();

                        var xpos = ~~particle.pos.x, ypos = ~~particle.pos.y;

                        var w = particle.width, h = particle.height;
                        var angle = particle.angle + particle._sourceAngle;

                        if ((particle.scaleFlag) || (angle !== 0)) {
                            // calculate pixel pos of the anchor point
                            var ax = w * particle.anchorPoint.x, ay = h * particle.anchorPoint.y;

                            // determine scale
                            var scaleX = 1, scaleY = 1;
                            if (particle.scaleFlag) {
                                scaleX = particle.scale.x;
                                scaleY = particle.scale.y;
                            }

                            // translate to the defined anchor point and scale it
                            context.setTransform(scaleX, 0, 0, scaleY, xpos + ax, ypos + ay);
                            if (angle !== 0) {
                                context.rotate(angle);
                            }

                            if (particle._sourceAngle !== 0) {
                                // swap w and h for rotated source images
                                w = particle.height;
                                h = particle.width;

                                xpos = -ay;
                                ypos = -ax;
                            } else {
                                // reset coordinates back to upper left coordinates
                                xpos = -ax;
                                ypos = -ay;
                            }
                        }
                        context.drawImage(this.image,
                                        0, 0,
                                        w, h,
                                        xpos, ypos,
                                        w, h);
                    }
                }
            }

            // restore context
            context.restore();
        }
    });

    /*---------------------------------------------------------*/
    // END END END
    /*---------------------------------------------------------*/
})(window);
<commit_msg>inlined overlap check for better performance<commit_after>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2014, Olivier BIOT
 * http://www.melonjs.org
 *
 */

(function($) {

    /**
     * Particle Emitter Object.
     * @class
     * @extends Object
     * @memberOf me
     * @constructor
     * @param {me.Vector2d} pos position of the particle emitter
     * @param {Image} image reference to the Particle Image. See {@link me.loader#getImage}
     * @example
     *
     * // Create a basic emitter at position 100, 100
     * var emitter = new me.ParticleEmitter(100, 100, me.loader.getImage("smoke"));
     *
     * // Adjust the emitter properties
     * emitter.totalParticles = 200;
     * emitter.minLife = 1000;
     * emitter.maxLife = 3000;
     * emitter.z = 10;
     *
     * // Add the emitter to the game world
     * me.game.world.addChild(emitter);
     *
     * // Launch all particles one time and stop, like a explosion
     * emitter.burstParticles();
     *
     * // Launch constantly the particles, like a fountain
     * emitter.streamParticles();
     *
     * // At the end, remove emitter from the game world
     * // call this in onDestroyEvent function
     * me.game.world.removeChild(emitter);
     *
     */
    me.ParticleEmitter = me.Renderable.extend(
    /** @scope me.ParticleEmitter.prototype */
    {
        // Emitter is Stream, launch particles constantly
            /** @ignore */
        _stream: false,

        // Frequency timer (in ms) for emitter launch new particles
        // used only in stream emitter
            /** @ignore */
        _frequencyTimer: 0,

        // Time of live (in ms) for emitter launch new particles
        // used only in stream emitter
            /** @ignore */
        _durationTimer: 0,

        // Emitter is emitting particles
            /** @ignore */
        _enabled: false,

        /**
         * @ignore
         */
        init: function(x, y, image) {
            var pos = new me.Vector2d(x, y);

            // call the parent constructor
            this.parent(pos, 1, 1);

            // Emitter will always update
            this.alwaysUpdate = true;

            // Cache the emitter start pos
            this._defaultPos = pos;

            // Cache the emitter start image
            this._defaultImage = image;

            // hold particles in an array
            this._particles = [];

            // keep count of how many particles are dead
            this._deadCount = 0;

            // Reset the emitter to defaults
            this.reset();

            /**
             * Z-order for particles

             * default value : 0
             * @type Number
             * @name z
             * @memberOf me.ParticleEmitter
             */
            this.z = 0;
        },

        /**
         * Reset the Emitter with defaults params

         * @function
         * @param {Object} params [optional] object with emitter params
         * @name reset
         * @memberOf me.ParticleEmitter
         */
        reset: function(params) {
            // check if params exists and create a dummy object
            params = params || {};

            /**
             * Start position for launch particles

             * default value : x, y

             * @public
             * @type me.Vector2d
             * @name pos
             * @memberOf me.ParticleEmitter
             */
            this.pos = params.pos || this._defaultPos.clone();

            /**
             * Variation in the start position for launch the particles (x, y)

             * Random value in range [pos - varPos, pos + varPos]

             * default value : 0, 0

             * @public
             * @type me.Vector2d
             * @name varPos
             * @memberOf me.ParticleEmitter
             */
            this.varPos = params.varPos || new me.Vector2d(0, 0);

            /**
             * Image used for particles

             * @public
             * @type me.SpriteObject
             * @name image
             * @memberOf me.ParticleEmitter
             */
            this.image = params.image || this._defaultImage;

            /**
             * Total number of particles in this emitter

             * default value : 50

             * @public
             * @type Number
             * @name totalParticles
             * @memberOf me.ParticleEmitter
             */
            this.totalParticles = params.totalParticles || 50;

            /**
             * Minimum start angle for launch particles in Radians

             * default value : Math.PI / 2

             * @public
             * @type Number
             * @name minAngle
             * @memberOf me.ParticleEmitter
             */
            this.minAngle = params.minAngle || (Math.PI / 2);

            /**
             * Maximum start angle for launch particles in Radians

             * default value : Math.PI / 2

             * @public
             * @type Number
             * @name maxAngle
             * @memberOf me.ParticleEmitter
             */
            this.maxAngle = params.maxAngle || (Math.PI / 2);

            /**
             * Minimum time each particle lives once it is emitted in ms

             * default value : 1000

             * @public
             * @type Number
             * @name minLife
             * @memberOf me.ParticleEmitter
             */
            this.minLife = params.minLife || 1000;

            /**
             * Maximum time each particle lives once it is emitted in ms

             * default value : 3000

             * @public
             * @type Number
             * @name maxLife
             * @memberOf me.ParticleEmitter
             */
            this.maxLife = params.maxLife || 3000;

            /**
             * Minimum start speed for particles

             * default value : 1

             * @public
             * @type Number
             * @name minSpeed
             * @memberOf me.ParticleEmitter
             */
            this.minSpeed = params.minSpeed || 1;

            /**
             * Maximum start speed for particles

             * default value : 3

             * @public
             * @type Number
             * @name maxSpeed
             * @memberOf me.ParticleEmitter
             */
            this.maxSpeed = params.maxSpeed || 3;

            /**
             * Minimum start rotation for particles sprites in Radians

             * default value : 0

             * @public
             * @type Number
             * @name minRotation
             * @memberOf me.ParticleEmitter
             */
            this.minRotation = params.minRotation || 0;

            /**
             * Maximum start rotation for particles sprites in Radians

             * default value : 0

             * @public
             * @type Number
             * @name maxRotation
             * @memberOf me.ParticleEmitter
             */
            this.maxRotation = params.maxRotation || 0;

            /**
             * Minimum start scale ratio for particles (1 = no scaling)

             * default value : 1

             * @public
             * @type Number
             * @name minStartScale
             * @memberOf me.ParticleEmitter
             */
            this.minStartScale = params.minStartScale || 1;

            /**
             * Maximum start scale ratio for particles (1 = no scaling)

             * default value : 1

             * @public
             * @type Number
             * @name maxStartScale
             * @memberOf me.ParticleEmitter
             */
            this.maxStartScale = params.maxStartScale || 1;

            /**
             * Minimum end scale ratio for particles

             * default value : 0

             * @public
             * @type Number
             * @name minEndScale
             * @memberOf me.ParticleEmitter
             */
            this.minEndScale = params.minEndScale || 0;

            /**
             * Maximum end scale ratio for particles

             * default value : 0

             * @public
             * @type Number
             * @name maxEndScale
             * @memberOf me.ParticleEmitter
             */
            this.maxEndScale = params.maxEndScale || 0;

            /**
             * Vertical force (Gravity) for each particle

             * default value : me.sys.gravity or 0

             * @public
             * @type Number
             * @name gravity
             * @memberOf me.ParticleEmitter
             */
            this.gravity = params.gravity || me.sys.gravity || 0;

            /**
             * Horizontal force (like a Wind) for each particle

             * default value : 0

             * @public
             * @type Number
             * @name wind
             * @memberOf me.ParticleEmitter
             */
            this.wind = params.wind || 0;

            /**
             * Update the rotation of particle in accordance the particle trajectory

             * The particle sprite should aim at zero angle (draw from left to right)

             * Override the particle minRotation and maxRotation

             * default value : false

             * @public
             * @type Boolean
             * @name followTrajectory
             * @memberOf me.ParticleEmitter
             */
            this.followTrajectory = params.followTrajectory || false;

            /**
             * Enable the Texture Additive by canvas composite operation (lighter)

             * WARNING: Composite Operation may decreases performance!

             * default value : false

             * @public
             * @type Boolean
             * @name textureAdditive
             * @memberOf me.ParticleEmitter
             */
            this.textureAdditive = params.textureAdditive || false;

            /**
             * Update particles only in the viewport, remove it when out of viewport

             * default value : true

             * @public
             * @type Boolean
             * @name onlyInViewport
             * @memberOf me.ParticleEmitter
             */
            this.onlyInViewport = params.onlyInViewport || true;

            /**
             * Maximum number of particles launched each time in this emitter (used only if emitter is Stream)

             * default value : 10

             * @public
             * @type Number
             * @name maxParticles
             * @memberOf me.ParticleEmitter
             */
            this.maxParticles = params.maxParticles || 10;

            /**
             * How often a particle is emitted in ms (used only if emitter is Stream)

             * Necessary that value is greater than zero

             * default value : 100

             * @public
             * @type Number
             * @name frequency
             * @memberOf me.ParticleEmitter
             */
            this.frequency = params.frequency || 100;

            /**
             * Duration that the emitter releases particles in ms (used only if emitter is Stream)

             * After this period, the emitter stop the launch of particles

             * default value : Infinity

             * @public
             * @type Number
             * @name duration
             * @memberOf me.ParticleEmitter
             */
            this.duration = params.duration || Infinity;

            /**
             * Determines how many particles can die before the internal cache is cleaned up.

             * default value : 10

             * @public
             * @type Number
             * @name cleanupThreshold
             * @memberOf me.ParticleEmitter
             */
            this.cleanupThreshold = params.cleanupThreshold || 10;

            // clear particle array
            this._particles.length = 0;
        },

        // Add count particles in the game world
        /** @ignore */
        addParticles: function(count) {
            for (var i = 0; i < ~~count; i++) {
                // Add particle in the emitter
                this._particles.push(me.entityPool.newInstanceOf("me.Particle", this));
            }
        },

        /**
         * Emitter is of type stream and is launching particles

         * @function
         * @returns {Boolean} Emitter is Stream and is launching particles
         * @name isRunning
         * @memberOf me.ParticleEmitter
         */
        isRunning: function() {
            return this._enabled && this._stream;
        },

        /**
         * Launch particles from emitter constantly

         * Particles example: Fountains
         * @param {Number} duration [optional] time that the emitter releases particles in ms
         * @function
         * @name streamParticles
         * @memberOf me.ParticleEmitter
         */
        streamParticles: function(duration) {
            this._enabled = true;
            this._stream = true;
            this.frequency = Math.max(this.frequency, 1);
            this._durationTimer = duration || this.duration;
        },

        /**
         * Stop the emitter from generating new particles (used only if emitter is Stream)

         * @function
         * @name stopStream
         * @memberOf me.ParticleEmitter
         */
        stopStream: function() {
            this._enabled = false;
        },

        /**
         * Launch all particles from emitter and stop

         * Particles example: Explosions

         * @param {Number} total [optional] number of particles to launch
         * @function
         * @name burstParticles
         * @memberOf me.ParticleEmitter
         */
        burstParticles: function(total) {
            this._enabled = true;
            this._stream = false;
            this.addParticles(total || this.totalParticles);
            this._enabled = false;
        },

        /**
         * Update the Emitter

         * This is automatically called by the game manager {@link me.game}
         * @name update
         * @memberOf me.ParticleEmitter
         * @function
         * @ignore
         * @param {Number} dt time since the last update in milliseconds
         */
        update: function(dt) {
            // Launch new particles, if emitter is Stream
            if ((this._enabled) && (this._stream)) {
                // Check if the emitter has duration set
                if (this._durationTimer !== Infinity) {
                    this._durationTimer -= dt;

                    if (this._durationTimer <= 0) {
                        this.stopStream();
                        return false;
                    }
                }

                // Increase the emitter launcher timer
                this._frequencyTimer += dt;

                // Check for new particles launch
                var particlesCount = this._particles.length - this._deadCount;
                if ((particlesCount < this.totalParticles) && (this._frequencyTimer >= this.frequency)) {
                    if ((particlesCount + this.maxParticles) <= this.totalParticles)
                        this.addParticles(this.maxParticles);
                    else
                        this.addParticles(this.totalParticles - particlesCount);

                    this._frequencyTimer = 0;
                }
            }

            // Update particles if they are not dead yet
            var particlesCount = this._particles.length;
            var viewport = me.game.viewport;
            var deadCount = 0;
            for ( var i = 0; i < particlesCount; ++i) {
                var particle = this._particles[i];
                if(!particle.isDead) {
                    particle.inViewport = (particle.pos.x < viewport.pos.x + viewport.width &&
                                           viewport.pos.x < particle.pos.x + particle.width &&
                                           particle.pos.y < viewport.pos.y + viewport.height &&
                                           viewport.pos.y < particle.pos.y + particle.height);
                    particle.update(dt);
                } else {
                    deadCount++;
                }
            }
            this._deadCount = deadCount;

            // Free dead particles if there are enough of them.
            if(deadCount > this.cleanupThreshold) {
                this._particles.sort(this._deadLast);
                while(deadCount--) {
                    var particle = this._particles.pop();
                    me.entityPool.freeInstance(particle);
                }
            }
        },

        /**
         * Move dead particles to the end of the array.
         * @ignore
         */
        _deadLast: function(a, b) {
            if(a.isDead && b.isDead)
                return 0;
            else if(a.isDead)
                return 1;
            else if(b.isDead)
                return -1;
            return 0;
        },

        /**
         * @ignore
         */
        draw : function(context) {
            var particlesCount = this._particles.length;
            if(particlesCount > 0) {
                // save context
                context.save();

                var originalAlpha = context.globalAlpha;

                // Check for additive draw
                if (this.textureAdditive) {
                    context.globalCompositeOperation = "lighter";
                }

                for ( var i = 0; i < particlesCount; ++i) {
                    var particle = this._particles[i];
                    if(!particle.isDead) {
                        // particle alpha value
                        context.globalAlpha = originalAlpha * particle.getOpacity();

                        var xpos = ~~particle.pos.x, ypos = ~~particle.pos.y;

                        var w = particle.width, h = particle.height;
                        var angle = particle.angle + particle._sourceAngle;

                        if ((particle.scaleFlag) || (angle !== 0)) {
                            // calculate pixel pos of the anchor point
                            var ax = w * particle.anchorPoint.x, ay = h * particle.anchorPoint.y;

                            // determine scale
                            var scaleX = 1, scaleY = 1;
                            if (particle.scaleFlag) {
                                scaleX = particle.scale.x;
                                scaleY = particle.scale.y;
                            }

                            // translate to the defined anchor point and scale it
                            context.setTransform(scaleX, 0, 0, scaleY, xpos + ax, ypos + ay);
                            if (angle !== 0) {
                                context.rotate(angle);
                            }

                            if (particle._sourceAngle !== 0) {
                                // swap w and h for rotated source images
                                w = particle.height;
                                h = particle.width;

                                xpos = -ay;
                                ypos = -ax;
                            } else {
                                // reset coordinates back to upper left coordinates
                                xpos = -ax;
                                ypos = -ay;
                            }
                        }
                        context.drawImage(this.image,
                                        0, 0,
                                        w, h,
                                        xpos, ypos,
                                        w, h);
                    }
                }
            }

            // restore context
            context.restore();
        }
    });

    /*---------------------------------------------------------*/
    // END END END
    /*---------------------------------------------------------*/
})(window);
<|endoftext|>


<commit_before>/**
 * Copyright 2012-2013 Syslab.com GmbH - JC Brand
 */
define([
    "jquery",
    "pat-registry",
    "pat-base",
    "pat-utils",
    "pat-logger",
    "pat-parser",
    "underscore"
], function($, patterns, Base, utils, logging, Parser, _) {
    var log = logging.getLogger("scroll"),
        parser = new Parser("scroll");
    parser.addArgument("trigger", "click", ["click", "auto"]);
    parser.addArgument("direction", "top", ["top", "left"]);
    parser.addArgument("selector");
    parser.addArgument("offset");

    return Base.extend({
        name: "scroll",
        trigger: ".pat-scroll",
        jquery_plugin: true,

        init: function($el, opts) {
            this.options = parser.parse(this.$el, opts);
            if (this.options.trigger == "auto") {
               this.smoothScroll();
            } else if (this.options.trigger == "click") {
                this.$el.click(this.onClick.bind(this));
            }
            this.$el.on("pat-update", this.onPatternsUpdate.bind(this));
            this.markBasedOnFragment();
            this.on('hashchange', this.clearIfHidden.bind(this));
            $(window).scroll(_.debounce(this.markIfVisible.bind(this), 50));
        },

        onClick: function(ev) {
            ev.preventDefault();
            history.pushState({}, null, this.$el.attr('href'));
            this.smoothScroll();
            this.markBasedOnFragment();
            // manually trigger the hashchange event on all instances of pat-scroll
            $('a.pat-scroll').trigger("hashchange");
        },

        markBasedOnFragment: function(ev) {
            // Get the fragment from the URL and set the corresponding this.$el as current
            var $target = $('#' + window.location.hash.substr(1));
            if ($target.length > 0) {
                this.$el.addClass("current"); // the element that was clicked on
                $target.addClass("current");
            }
        },

        clearIfHidden: function(ev) {
            var active_target = '#' + window.location.hash.substr(1),
                $active_target = $(active_target),
                target = this.$el[0].href.split('/').pop();
            if ($active_target.length > 0) {
                if (active_target != target) {
                    // if the element does not match the one listed in the url #,
                    // clear the current class from it.
                    var $target = $(this.$el[0].href.split('/').pop());
                    $target.removeClass("current");
                    this.$el.removeClass("current");
                }
            }
        },

        markIfVisible: function(ev) {
            var fragment, $target, href;
            if (this.$el.hasClass('pat-scroll-animated')) {
                // this section is triggered when the scrolling is a result of the animate function
                // ie. automatic scrolling as opposed to the user manually scrolling
                this.$el.removeClass('pat-scroll-animated');
            } else if (this.$el[0].nodeName === "A") {
                href = this.$el[0].href;
                fragment = href.indexOf('#') !== -1 && href.split('#').pop() || undefined;
                if (fragment) {
                    $target = $('#'+fragment);
                    if ($target.length) {
                        if (utils.isElementInViewport($target[0], true, this.options.offset)) {
                            // check that the anchor's target is visible
                            // if so, mark both the anchor and the target element
                            $target.addClass("current");
                            this.$el.addClass("current");
                        }
                        $(this.$el).trigger("pat-update", {pattern: "scroll"});
                    }
                }
            }
        },

        onPatternsUpdate: function(ev, data) {
            var fragment, $target, href;
            if (data.pattern === 'stacks') {
                if (data.originalEvent && data.originalEvent.type === "click") {
                    this.smoothScroll();
                }
            } else if (data.pattern === 'scroll') {
                href = this.$el[0].href;
                fragment = href.indexOf('#') !== -1 && href.split('#').pop() || undefined;
                if (fragment) {
                    $target = $('#'+fragment);
                    if ($target.length) {
                        if (utils.isElementInViewport($target[0], true, this.options.offset) === false) {
                            // if the anchor's target is invisible, remove current class from anchor and target.
                            $target.removeClass("current");
                            $(this.$el).removeClass("current");
                        }
                    }
                }
            }
        },

        smoothScroll: function() {
            var scroll = this.options.direction == "top" ? 'scrollTop' : 'scrollLeft',
                $el, options = {};
            if (typeof this.options.offset != "undefined") {
                $el = this.options.selector ? $(this.options.selector) : this.$el;
                options[scroll] = this.options.offset;
            } else {
                $el = $('body, html');
                options[scroll] = $(this.$el.attr('href')).offset().top;
            }
            $el.animate(options, {
                duration: 500,
                start: function() {
                    $('.pat-scroll').addClass('pat-scroll-animated');
                }
            });
        }
    });
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>scroll to top of scrollcontainer and wait for the images being loaded<commit_after>/**
 * Copyright 2012-2013 Syslab.com GmbH - JC Brand
 */
define([
    "jquery",
    "pat-registry",
    "pat-base",
    "pat-utils",
    "pat-logger",
    "pat-parser",
    "underscore",
    "imagesloaded"
], function($, patterns, Base, utils, logging, Parser, _, imagesLoaded) {
    var log = logging.getLogger("scroll"),
        parser = new Parser("scroll");
    parser.addArgument("trigger", "click", ["click", "auto"]);
    parser.addArgument("direction", "top", ["top", "left"]);
    parser.addArgument("selector");
    parser.addArgument("offset");

    return Base.extend({
        name: "scroll",
        trigger: ".pat-scroll",
        jquery_plugin: true,

        init: function($el, opts) {
            this.options = parser.parse(this.$el, opts);
            if (this.options.trigger == "auto") {
                // Only calculate the offset when all images are loaded
                var that = this;
                $('body').imagesLoaded( function() {
                   that.smoothScroll();
                });
            } else if (this.options.trigger == "click") {
                this.$el.click(this.onClick.bind(this));
            }
            this.$el.on("pat-update", this.onPatternsUpdate.bind(this));
            this.markBasedOnFragment();
            this.on('hashchange', this.clearIfHidden.bind(this));
            $(window).scroll(_.debounce(this.markIfVisible.bind(this), 50));
        },

        onClick: function(ev) {
            ev.preventDefault();
            history.pushState({}, null, this.$el.attr('href'));
            this.smoothScroll();
            this.markBasedOnFragment();
            // manually trigger the hashchange event on all instances of pat-scroll
            $('a.pat-scroll').trigger("hashchange");
        },

        markBasedOnFragment: function(ev) {
            // Get the fragment from the URL and set the corresponding this.$el as current
            var $target = $('#' + window.location.hash.substr(1));
            if ($target.length > 0) {
                this.$el.addClass("current"); // the element that was clicked on
                $target.addClass("current");
            }
        },

        clearIfHidden: function(ev) {
            var active_target = '#' + window.location.hash.substr(1),
                $active_target = $(active_target),
                target = this.$el[0].href.split('/').pop();
            if ($active_target.length > 0) {
                if (active_target != target) {
                    // if the element does not match the one listed in the url #,
                    // clear the current class from it.
                    var $target = $(this.$el[0].href.split('/').pop());
                    $target.removeClass("current");
                    this.$el.removeClass("current");
                }
            }
        },

        markIfVisible: function(ev) {
            var fragment, $target, href;
            if (this.$el.hasClass('pat-scroll-animated')) {
                // this section is triggered when the scrolling is a result of the animate function
                // ie. automatic scrolling as opposed to the user manually scrolling
                this.$el.removeClass('pat-scroll-animated');
            } else if (this.$el[0].nodeName === "A") {
                href = this.$el[0].href;
                fragment = href.indexOf('#') !== -1 && href.split('#').pop() || undefined;
                if (fragment) {
                    $target = $('#'+fragment);
                    if ($target.length) {
                        if (utils.isElementInViewport($target[0], true, this.options.offset)) {
                            // check that the anchor's target is visible
                            // if so, mark both the anchor and the target element
                            $target.addClass("current");
                            this.$el.addClass("current");
                        }
                        $(this.$el).trigger("pat-update", {pattern: "scroll"});
                    }
                }
            }
        },

        onPatternsUpdate: function(ev, data) {
            var fragment, $target, href;
            if (data.pattern === 'stacks') {
                if (data.originalEvent && data.originalEvent.type === "click") {
                    this.smoothScroll();
                }
            } else if (data.pattern === 'scroll') {
                href = this.$el[0].href;
                fragment = href.indexOf('#') !== -1 && href.split('#').pop() || undefined;
                if (fragment) {
                    $target = $('#'+fragment);
                    if ($target.length) {
                        if (utils.isElementInViewport($target[0], true, this.options.offset) === false) {
                            // if the anchor's target is invisible, remove current class from anchor and target.
                            $target.removeClass("current");
                            $(this.$el).removeClass("current");
                        }
                    }
                }
            }
        },

        smoothScroll: function() {
            var scroll = this.options.direction == "top" ? 'scrollTop' : 'scrollLeft',
                $el, options = {};
            if (typeof this.options.offset != "undefined") {
                $el = this.options.selector ? $(this.options.selector) : this.$el;
                options[scroll] = this.options.offset;
            } else {
                // Get the first element with overflow auto starting from the trigger
                // (the scroll container)
                // Then calculate the offset relatively to that container
                $el = $(this.$el.parents()
                    .filter(function() {
                        return $(this).css('overflow') === 'auto'; })
                    .first())
                options[scroll] = $(this.$el.attr('href')).offset().top - $el.offset().top;
            }
            $el.animate(options, {
                duration: 500,
                start: function() {
                    $('.pat-scroll').addClass('pat-scroll-animated');
                }
            });
        }
    });
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>define([
    'jquery',
    "../registry"
], function($, patterns) {
    var _ = {
        name: "sortable",
        trigger: "ul.pat-sortable",

        init: function($el) {
            if ($el.length > 1)
                return $el.each(function() { _.init($(this)); });

            // use only direct descendants to support nested lists
            var $lis = $el.children().filter('li');

            // add handles and make them draggable for HTML5 and IE8/9
            // it has to be an "a" tag (or img) to make it draggable in IE8/9
            var $handles = $('').appendTo($lis);
            if('draggable' in document.createElement('span'))
                $handles.attr('draggable', true);
            else
                $handles.bind('selectstart', function(event) {
                    event.preventDefault();
                });

            $handles.bind('dragstart', function(event) {
                // Firefox seems to need this set to any value
                event.originalEvent.dataTransfer.setData('Text', '');
                event.originalEvent.dataTransfer.effectAllowed = ['move'];
                if ('setDragImage' in event.originalEvent.dataTransfer)
                    event.originalEvent.dataTransfer.setDragImage(
                        $(this).parent()[0], 0, 0);
                $(this).parent().addClass('dragged');

                // list elements are only drop targets when one element of the
                // list is being dragged. avoids dragging between lists.
                $lis.bind('dragover.pat-sortable', function(event) {
                    var $this = $(this),
                        midlineY = $this.offset().top - $(document).scrollTop()
                            + $this.height()/2;

                    // bail if dropping on self
                    if ($(this).hasClass('dragged'))
                        return;

                    $this.removeClass('drop-target-above drop-target-below');
                    if (event.originalEvent.clientY > midlineY)
                        $this.addClass('drop-target-below');
                    else
                        $this.addClass('drop-target-above');
                    event.preventDefault();
                });

                $lis.bind('dragleave.pat-sortable', function(event) {
                    $lis.removeClass('drop-target-above drop-target-below');
                });

                $lis.bind('drop.pat-sortable', function(event) {
                    if ($(this).hasClass('drop-target-below'))
                        $(this).after($('.dragged'));
                    else
                        $(this).before($('.dragged'));
                    $(this).removeClass('drop-target-above drop-target-below');
                    event.preventDefault();
                });
            });

            $handles.bind('dragend', function(event) {
                $('.dragged').removeClass('dragged');
                $lis.unbind('.pat-sortable');
            });

        }
    };

    patterns.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>return element for chaining<commit_after>define([
    'jquery',
    "../registry"
], function($, patterns) {
    var _ = {
        name: "sortable",
        trigger: "ul.pat-sortable",

        init: function($el) {
            if ($el.length > 1)
                return $el.each(function() { _.init($(this)); });

            // use only direct descendants to support nested lists
            var $lis = $el.children().filter('li');

            // add handles and make them draggable for HTML5 and IE8/9
            // it has to be an "a" tag (or img) to make it draggable in IE8/9
            var $handles = $('').appendTo($lis);
            if('draggable' in document.createElement('span'))
                $handles.attr('draggable', true);
            else
                $handles.bind('selectstart', function(event) {
                    event.preventDefault();
                });

            $handles.bind('dragstart', function(event) {
                // Firefox seems to need this set to any value
                event.originalEvent.dataTransfer.setData('Text', '');
                event.originalEvent.dataTransfer.effectAllowed = ['move'];
                if ('setDragImage' in event.originalEvent.dataTransfer)
                    event.originalEvent.dataTransfer.setDragImage(
                        $(this).parent()[0], 0, 0);
                $(this).parent().addClass('dragged');

                // list elements are only drop targets when one element of the
                // list is being dragged. avoids dragging between lists.
                $lis.bind('dragover.pat-sortable', function(event) {
                    var $this = $(this),
                        midlineY = $this.offset().top - $(document).scrollTop()
                            + $this.height()/2;

                    // bail if dropping on self
                    if ($(this).hasClass('dragged'))
                        return;

                    $this.removeClass('drop-target-above drop-target-below');
                    if (event.originalEvent.clientY > midlineY)
                        $this.addClass('drop-target-below');
                    else
                        $this.addClass('drop-target-above');
                    event.preventDefault();
                });

                $lis.bind('dragleave.pat-sortable', function(event) {
                    $lis.removeClass('drop-target-above drop-target-below');
                });

                $lis.bind('drop.pat-sortable', function(event) {
                    if ($(this).hasClass('drop-target-below'))
                        $(this).after($('.dragged'));
                    else
                        $(this).before($('.dragged'));
                    $(this).removeClass('drop-target-above drop-target-below');
                    event.preventDefault();
                });
            });

            $handles.bind('dragend', function(event) {
                $('.dragged').removeClass('dragged');
                $lis.unbind('.pat-sortable');
            });

            return $el;
        }
    };

    patterns.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>
'use strict';

var util = require('util'),
    queries = require('./petition-queries'),
    EventEmitter = require('events'),
    PetitionPager = require('./petition-pager'),
    equal = require('deep-equal');

/**
 * Monitors the petitions data for changes and generates a notification event for changes.
 * @constructor
 */
function PetitionsMonitor(config) {
    EventEmitter.call(this);
    var self = this,
        initialInterval = 200,
        interval = 2000,
        passDebug = false,
        debug = function() {};
    if (config) {
        if (config.initialInterval) {
            initialInterval = config.initialInterval;
        }
        if (config.interval) {
            interval = config.interval;
        }
        if (config.debug) {
            debug = function() {
                passDebug = true;
                console.log.apply(null, arguments);
            };
        }
    }
    debug('Debug enabled');

    this.start = function () {
        debug('Starting monitor');
        var pager = new PetitionPager({ loadInterval : initialInterval, debug : passDebug });
        pager.addDeltaCheck = function(event, check) {
            this.on('petition', function(newData, oldData) {
                if (oldData) {
                    if (check(newData, oldData)) {
                        self.emit(event, newData);
                    }
                }
            });
            return this;
        };

        // Only update when interesting details change
        var filter = function (summary, petitions) {
            if (summary.attributes.state !== 'rejected' && summary.attributes.state !== 'closed') {
                var currentInfo = petitions[summary.id];
                if (currentInfo) {
                    return currentInfo.attributes.signature_count !== summary.attributes.signature_count ||
                        !equal(currentInfo.attributes.government_response, summary.attributes.government_response) ||
                        !equal(currentInfo.attributes.debate, summary.attributes.debate);
                }
                else {
                    return true;
                }
            }
            return false;
        };

        pager
            .setMaxListeners(20)
            .on('petition', function(newData, oldData) {
                if (!oldData) {
                    self.emit('new-petition', newData);
                }
                else {
                    self.emit('updated-petition', newData);
                }
            })
            .addDeltaCheck('reached-10-signatures', queries.checks.delta.reached10)
            .addDeltaCheck('reached-20-signatures', queries.checks.delta.reached20)
            .addDeltaCheck('reached-50-signatures', queries.checks.delta.reached50)
            .addDeltaCheck('reached-100-signatures', queries.checks.delta.reached100)
            .addDeltaCheck('reached-250-signatures', queries.checks.delta.reached250)
            .addDeltaCheck('reached-500-signatures', queries.checks.delta.reached500)
            .addDeltaCheck('reached-1000-signatures', queries.checks.delta.reached1_000)
            .addDeltaCheck('reached-5000-signatures', queries.checks.delta.reached5_000)
            .addDeltaCheck('reached-response-threshold', queries.checks.delta.reachedResponseThreshold)
            .addDeltaCheck('reached-50000-signatures', queries.checks.delta.reached50_000)
            .addDeltaCheck('reached-debate-threshold', queries.checks.delta.reachedDebateThreshold)
            .addDeltaCheck('reached-500000-signatures', queries.checks.delta.reached500_000)
            .addDeltaCheck('government-response', queries.checks.delta.governmentResponded)
            .addDeltaCheck('debate-transcript', queries.checks.delta.debateTranscriptAvailable)
            .on('loaded', function() {
                debug('Initial petitions polled, found %d petitions, going again', pager.petitions.length);
                pager
                    .setPageLoadInterval(interval)
                    .removeAllListeners('loaded')
                    .on('loaded', function() {
                        debug('All petitions polled, found %d petitions, going again', pager.petitions.length);
                        pager.populate(filter);
                    })
                    .emit('initial-load');
                pager.populate(filter);
            })
            .populate(filter);

        return self;
    };
}
util.inherits(PetitionsMonitor, EventEmitter);

module.exports = PetitionsMonitor;
<commit_msg>Renamed variable.<commit_after>
'use strict';

var util = require('util'),
    queries = require('./petition-queries'),
    EventEmitter = require('events'),
    PetitionPager = require('./petition-pager'),
    equal = require('deep-equal');

/**
 * Monitors the petitions data for changes and generates a notification event for changes.
 * @constructor
 */
function PetitionsMonitor(config) {
    EventEmitter.call(this);
    var self = this,
        initialInterval = 200,
        interval = 2000,
        passDebug = false,
        debug = function() {};
    if (config) {
        if (config.initialInterval) {
            initialInterval = config.initialInterval;
        }
        if (config.interval) {
            interval = config.interval;
        }
        if (config.debug) {
            debug = function() {
                passDebug = true;
                console.log.apply(null, arguments);
            };
        }
    }
    debug('Debug enabled');

    this.start = function () {
        debug('Starting monitor');
        var pager = new PetitionPager({ loadInterval : initialInterval, debug : passDebug });
        pager.addDeltaCheck = function(event, check) {
            this.on('petition', function(newData, oldData) {
                if (oldData) {
                    if (check(newData, oldData)) {
                        self.emit(event, newData);
                    }
                }
            });
            return this;
        };

        // Only update when interesting details change
        var accepter = function (summary, petitions) {
            if (summary.attributes.state !== 'rejected' && summary.attributes.state !== 'closed') {
                var currentInfo = petitions[summary.id];
                if (currentInfo) {
                    return currentInfo.attributes.signature_count !== summary.attributes.signature_count ||
                        !equal(currentInfo.attributes.government_response, summary.attributes.government_response) ||
                        !equal(currentInfo.attributes.debate, summary.attributes.debate);
                }
                else {
                    return true;
                }
            }
            return false;
        };

        pager
            .setMaxListeners(20)
            .on('petition', function(newData, oldData) {
                if (!oldData) {
                    self.emit('new-petition', newData);
                }
                else {
                    self.emit('updated-petition', newData);
                }
            })
            .addDeltaCheck('reached-10-signatures', queries.checks.delta.reached10)
            .addDeltaCheck('reached-20-signatures', queries.checks.delta.reached20)
            .addDeltaCheck('reached-50-signatures', queries.checks.delta.reached50)
            .addDeltaCheck('reached-100-signatures', queries.checks.delta.reached100)
            .addDeltaCheck('reached-250-signatures', queries.checks.delta.reached250)
            .addDeltaCheck('reached-500-signatures', queries.checks.delta.reached500)
            .addDeltaCheck('reached-1000-signatures', queries.checks.delta.reached1_000)
            .addDeltaCheck('reached-5000-signatures', queries.checks.delta.reached5_000)
            .addDeltaCheck('reached-response-threshold', queries.checks.delta.reachedResponseThreshold)
            .addDeltaCheck('reached-50000-signatures', queries.checks.delta.reached50_000)
            .addDeltaCheck('reached-debate-threshold', queries.checks.delta.reachedDebateThreshold)
            .addDeltaCheck('reached-500000-signatures', queries.checks.delta.reached500_000)
            .addDeltaCheck('government-response', queries.checks.delta.governmentResponded)
            .addDeltaCheck('debate-transcript', queries.checks.delta.debateTranscriptAvailable)
            .on('loaded', function() {
                debug('Initial petitions polled, found %d petitions, going again', pager.petitions.length);
                pager
                    .setPageLoadInterval(interval)
                    .removeAllListeners('loaded')
                    .on('loaded', function() {
                        debug('All petitions polled, found %d petitions, going again', pager.petitions.length);
                        pager.populate(accepter);
                    })
                    .emit('initial-load');
                pager.populate(accepter);
            })
            .populate(accepter);

        return self;
    };
}
util.inherits(PetitionsMonitor, EventEmitter);

module.exports = PetitionsMonitor;
<|endoftext|>


<commit_before>
'use strict';

var util = require('util'),
    queries = require('./petition-queries'),
    EventEmitter = require('events'),
    PetitionPager = require('./petition-pager'),
    equal = require('deep-equal');

function logDebug () {
    console.log.apply(null, arguments);
}

function standardAccepter (summary, petitions) {
    if (summary.attributes.state !== 'rejected' && summary.attributes.state !== 'closed') {
        var currentInfo = petitions[summary.id];
        if (currentInfo) {
            return currentInfo.attributes.signature_count !== summary.attributes.signature_count ||
                !equal(currentInfo.attributes.government_response, summary.attributes.government_response) ||
                !equal(currentInfo.attributes.debate, summary.attributes.debate);
        }
        else {
            return true;
        }
    }
    return false;
}

/**
 * Monitors the petitions data for changes and generates a notification event for changes.
 * @constructor
 */
function PetitionsMonitor(config) {
    EventEmitter.call(this);
    var self = this,
        initialInterval = 200,
        interval = 2000,
        passDebug = false,
        debug = function() {},
        loadDetail = true,
        events = [],
        deltaEvents = [],
        accepter = standardAccepter;

    if (config) {
        if (config.initialInterval) {
            initialInterval = config.initialInterval;
        }
        if (config.interval) {
            interval = config.interval;
        }
        if (config.debug) {
            passDebug = true;
            debug = logDebug;
        }
        if (config.loadDetail !== undefined) {
            loadDetail = config.loadDetail;
        }
        if (config.accepter !== undefined) {
            accepter = config.accepter;
        }
    }
    debug('Debug enabled');

    /**
     * Add an event to emit when the check function returns true. The check function is passed the the new data. Returns the monitor.
     */
    this.addMonitorEvent = function(event, check) {
        events.push({event : event, check : check});
        return self;
    };

    /**
     * Add an event to emit when the check function returns true. The check function is passed the the new and old data. Returns the monitor.
     */
    this.addMonitorDeltaEvent = function(event, check) {
        deltaEvents.push({event : event, check : check});
        return self;
    };

    /**
     * Start the monitor. Returns the monitor.
     */
    this.start = function () {
        debug('Starting monitor');
        var pager = new PetitionPager({
            loadInterval : initialInterval,
            debug : passDebug,
            loadDetail : loadDetail
        });

        pager
        .on('petition', function(newData, oldData) {
            // Emit new or update event
            if (!oldData) {
                self.emit('new-petition', newData);
            }
            else {
                self.emit('updated-petition', newData, oldData);
            }

            // Check for events to emit based on the current value
            events.forEach(function(registeredEvent) {
                if (registeredEvent.check(newData)) {
                    self.emit(registeredEvent.event, newData);
                }
            });

            if (oldData) {
                // Check for events to emit based on the current and previous values
                deltaEvents.forEach(function(registeredEvent) {
                    if (registeredEvent.check(newData, oldData)) {
                        self.emit(registeredEvent.event, newData);
                    }
                });
            }
        })
        .on('loaded', function() {
            debug('Initial petitions polled, found %d petitions, going again', pager.petitions.length);
            pager
                .setPageLoadInterval(interval)
                .removeAllListeners('loaded')
                .on('loaded', function() {
                    debug('All petitions polled, found %d petitions, going again', pager.petitions.length);
                    self.emit('loaded', pager.petitions);
                    pager.populate(accepter);
                });
            self.emit('initial-load', pager.petitions);
            pager.populate(accepter);
        })
        .populate(accepter);

        return self;
    };
}
util.inherits(PetitionsMonitor, EventEmitter);

/**
 * New petition event.
 * @event PetitionsMonitor#new-petition
 */

/**
 * Updated petition event.
 * @event PetitionsMonitor#updated-petition
 */

/**
 * Emited after all the data has been loaded for the first time.
 * @event PetitionsMonitor#initial-load
 */

/**
 * Emited after all the data has been loaded after the first time.
 * @event PetitionsMonitor#loaded
 */

module.exports = PetitionsMonitor;
<commit_msg>Document the configuration for the PetitionsMonitor.<commit_after>
'use strict';

var util = require('util'),
    queries = require('./petition-queries'),
    EventEmitter = require('events'),
    PetitionPager = require('./petition-pager'),
    equal = require('deep-equal');

function logDebug () {
    console.log.apply(null, arguments);
}

function standardAccepter (summary, petitions) {
    if (summary.attributes.state !== 'rejected' && summary.attributes.state !== 'closed') {
        var currentInfo = petitions[summary.id];
        if (currentInfo) {
            return currentInfo.attributes.signature_count !== summary.attributes.signature_count ||
                !equal(currentInfo.attributes.government_response, summary.attributes.government_response) ||
                !equal(currentInfo.attributes.debate, summary.attributes.debate);
        }
        else {
            return true;
        }
    }
    return false;
}

/**
 * Configuration for PetitionsMonitor.
 * @typedef {object} PetitionsMonitor~Config
 * @property {number} initialInterval - The initial interval.
 * @property {number} interval - The interval.
 * @property {boolean} debug - If debug logging should be enabled.
 * @property {boolean} loadDetail - If detailed petition information should be loaded.
 * @property {function} accepter - The accepting function.
 */

/**
 * Monitors the petitions data for changes and generates a notification event for changes.
 * @constructor
 * @param {PetitionsMonitor~Config} config - Configuration for PetitionsMonitor.
 */
function PetitionsMonitor(config) {
    EventEmitter.call(this);
    var self = this,
        initialInterval = 200,
        interval = 2000,
        passDebug = false,
        debug = function() {},
        loadDetail = true,
        events = [],
        deltaEvents = [],
        accepter = standardAccepter;

    if (config) {
        if (config.initialInterval) {
            initialInterval = config.initialInterval;
        }
        if (config.interval) {
            interval = config.interval;
        }
        if (config.debug) {
            passDebug = true;
            debug = logDebug;
        }
        if (config.loadDetail !== undefined) {
            loadDetail = config.loadDetail;
        }
        if (config.accepter !== undefined) {
            accepter = config.accepter;
        }
    }
    debug('Debug enabled');

    /**
     * Add an event to emit when the check function returns true. The check function is passed the the new data. Returns the monitor.
     */
    this.addMonitorEvent = function(event, check) {
        events.push({event : event, check : check});
        return self;
    };

    /**
     * Add an event to emit when the check function returns true. The check function is passed the the new and old data. Returns the monitor.
     */
    this.addMonitorDeltaEvent = function(event, check) {
        deltaEvents.push({event : event, check : check});
        return self;
    };

    /**
     * Start the monitor. Returns the monitor.
     */
    this.start = function () {
        debug('Starting monitor');
        var pager = new PetitionPager({
            loadInterval : initialInterval,
            debug : passDebug,
            loadDetail : loadDetail
        });

        pager
        .on('petition', function(newData, oldData) {
            // Emit new or update event
            if (!oldData) {
                self.emit('new-petition', newData);
            }
            else {
                self.emit('updated-petition', newData, oldData);
            }

            // Check for events to emit based on the current value
            events.forEach(function(registeredEvent) {
                if (registeredEvent.check(newData)) {
                    self.emit(registeredEvent.event, newData);
                }
            });

            if (oldData) {
                // Check for events to emit based on the current and previous values
                deltaEvents.forEach(function(registeredEvent) {
                    if (registeredEvent.check(newData, oldData)) {
                        self.emit(registeredEvent.event, newData);
                    }
                });
            }
        })
        .on('loaded', function() {
            debug('Initial petitions polled, found %d petitions, going again', pager.petitions.length);
            pager
                .setPageLoadInterval(interval)
                .removeAllListeners('loaded')
                .on('loaded', function() {
                    debug('All petitions polled, found %d petitions, going again', pager.petitions.length);
                    self.emit('loaded', pager.petitions);
                    pager.populate(accepter);
                });
            self.emit('initial-load', pager.petitions);
            pager.populate(accepter);
        })
        .populate(accepter);

        return self;
    };
}
util.inherits(PetitionsMonitor, EventEmitter);

/**
 * New petition event.
 * @event PetitionsMonitor#new-petition
 */

/**
 * Updated petition event.
 * @event PetitionsMonitor#updated-petition
 */

/**
 * Emited after all the data has been loaded for the first time.
 * @event PetitionsMonitor#initial-load
 */

/**
 * Emited after all the data has been loaded after the first time.
 * @event PetitionsMonitor#loaded
 */

module.exports = PetitionsMonitor;
<|endoftext|>


<commit_before>import * as I from 'immutable';

import {
  rationalLinearAlgebra,
  rationalLinearAlgebraModular
} from '../arithmetic/types';

import * as pg from './periodic';
import * as ps from './symmetries';

let _timers = null;

const ops = pg.ops;

const _solveInRows = (v, M) => {
  const tmp = rationalLinearAlgebra.solve(
    ops.transposed(M), ops.transposed(v));
  return tmp && ops.transposed(tmp);
};

const _traversal = function* _traversal(graph, v0, transform)
{
  const zero = ops.vector(graph.dim);

  const adj = pg.adjacencies(graph);
  const pos = pg.barycentricPlacement(graph);
  const old2new = {[v0]: 1};
  const mappedPos = {[v0]: ops.times(pos[v0], transform)};
  const newPos = {[v0]: mappedPos[v0]};
  const queue = [[v0, zero]];
  const essentialShifts = [];

  let next = 2;
  let basisAdjustment = null;

  while (queue.length) {
    const [vo, vShift] = queue.shift();
    const vn = old2new[vo];

    const neighbors = [];

    for (const e of pg.allIncidences(graph, vo, adj)) {
      const w = e.tail;
      if (mappedPos[w] == null)
        mappedPos[w] = ops.times(pos[w], transform);

      let s = vShift;
      if (ops.ne(zero, e.shift))
        s = ops.plus(s, ops.times(e.shift, transform));

      neighbors.push([w, s, ops.plus(s, mappedPos[w])]);
    }

    neighbors.sort(([wa, sa, pa], [wb, sb, pb]) => ops.cmp(pa, pb));

    for (const [wo, s, p] of neighbors) {
      const wn = old2new[wo];
      if (wn == null) {
        yield [vn, next, zero];
        old2new[wo] = next++;
        newPos[wo] = p;
        queue.push([wo, s]);
      }
      else if (wn < vn) {
        continue;
      }
      else {
        const rawShift = ops.minus(p, newPos[wo]);
        let shift;
        if (basisAdjustment != null) {
          shift = ops.times(rawShift, basisAdjustment);
        }
        else if (ops.sgn(rawShift) == 0) {
          shift = rawShift;
        }
        else {
          if (essentialShifts.length) {
            shift = _solveInRows(rawShift, essentialShifts);
          }
          if (shift == null) {
            shift = ops.unitVector(graph.dim, essentialShifts.length);
            essentialShifts.push(rawShift);
            if (essentialShifts.length == graph.dim) {
              basisAdjustment = ops.inverse(essentialShifts);
            }
          }
          else {
            shift = shift[0].concat(ops.vector(graph.dim - shift[0].length));
          }
        }
        if (vn < wn || (vn == wn && ops.sgn(shift) < 0)) {
          yield [vn, wn, shift];
        }
      }
    }
  }
};

const _cmpSteps = ([headA, tailA, shiftA], [headB, tailB, shiftB]) =>
  (headA - headB) || (tailA - tailB) || ops.cmp(shiftA, shiftB);

const _postprocessTraversal = trav => {
  let basis = null;
  for (const [head, tail, shift] of trav)
    basis = rationalLinearAlgebraModular.extendBasis(shift, basis);
  basis = basis.map(v => ops.lt(v, []) ? ops.negative(v) : v);

  const basisChange = ops.inverse(basis);

  return trav.map(([head, tail, shift]) => {
    const newShift = ops.times(shift, basisChange);
    if (newShift.some(x => !ops.isInteger(x))) {
      throw new Error("panic: produced non-integer shift");
    }
    if (head == tail && ops.sgn(newShift) > 0) {
      return [head, tail, ops.negative(newShift)];
    }
    else {
      return [head, tail, newShift];
    }
  });
};

const _goodBases = (graph, bases) => {
  const adj = pg.adjacencies(graph);

  const atLoop = bases.filter(basis => {
    const v = basis[0].head;
    return adj[v].every(e => e.tail == v);
  });

  if (atLoop.length > 0)
    return atLoop;

  const atLune = bases.filter(basis => {
    const v = basis[0].head;
    const neighbours = adj[v].map(e => e.tail).sort();
    return neighbours.some((w, i) => i == 0 || w == neighbours[i - 1]);
  });

  if (atLune.length > 0)
    return atLune;

  const maxDeg = Object.keys(adj).map(v => adj[v].length).max();
  return bases.filter(basis => adj[basis[0].head].length == maxDeg);
};

export function invariant(graph)
{
  const pos = pg.barycentricPlacement(graph);
  const sym = ps.symmetries(graph);

  _timers && _timers.start('invariant');

  let best = null;

  for (const basis of _goodBases(graph, sym.representativeBases)) {
    const v = basis[0].head;
    const transform = ops.inverse(basis.map(e => pg.edgeVector(e, pos)));
    const trav = I.Seq(_traversal(graph, v, transform));

    if (best == null) {
      best = trav;
    }
    else {
      for (let i = 0; ; ++i) {
        const next = trav.get(i);
        if (next == null) {
          break;
        }

        const d = _cmpSteps(next, best.get(i));
        if (d < 0) {
          best = trav;
        }
        else if (d > 0) {
          break;
        }
      }
    }
  }

  const result = _postprocessTraversal(I.List(best).toArray()).sort(_cmpSteps);

  _timers && _timers.stop('invariant');

  return result;
}

export function useTimers(timers) {
  _timers = timers;
}

if (require.main == module) {
  Array.prototype.toString = function() {
    return `[ ${this.map(x => x.toString()).join(', ')} ]`;
  };

  const test = g => {
    const trav = invariant(g);

    for (const e of trav)
      console.log(e);
    console.log();
  };

  test(pg.make([ [ 1, 2, [ 0, 0, 0 ] ],
                 [ 1, 2, [ 1, 0, 0 ] ],
                 [ 1, 2, [ 0, 1, 0 ] ],
                 [ 1, 2, [ 0, 0, 1 ] ] ]));

  test(pg.make([ [ 1, 2, [ 0, 0 ] ],
                 [ 1, 2, [ 1, 0 ] ],
                 [ 2, 3, [ 0, 0 ] ],
                 [ 2, 3, [ 0, 1 ] ],
                 [ 1, 3, [ 0, 0 ] ],
                 [ 1, 3, [ 1, 1 ] ] ]));
}
<commit_msg>revise pgraph invariant code<commit_after>import * as S from '../common/lazyseq';

import * as pg from './periodic';
import * as ps from './symmetries';

import {
  rationalLinearAlgebra,
  rationalLinearAlgebraModular
} from '../arithmetic/types';

const ops = pg.ops;

const _solveInRows = (v, M) => {
  const tmp = rationalLinearAlgebra.solve(
    ops.transposed(M), ops.transposed(v));
  return tmp && ops.transposed(tmp);
};

const _traversal = function*(graph, v0, transform) {
  const zero = ops.vector(graph.dim);

  const adj = pg.adjacencies(graph);
  const pos = pg.barycentricPlacement(graph);
  const old2new = {[v0]: 1};
  const mappedPos = {[v0]: ops.times(pos[v0], transform)};
  const newPos = {[v0]: mappedPos[v0]};
  const queue = [[v0, zero]];
  const essentialShifts = [];

  let next = 2;
  let basisAdjustment = null;

  while (queue.length) {
    const [vo, vShift] = queue.shift();
    const vn = old2new[vo];

    const neighbors = [];

    for (const e of pg.allIncidences(graph, vo, adj)) {
      const w = e.tail;
      if (mappedPos[w] == null)
        mappedPos[w] = ops.times(pos[w], transform);

      let s = vShift;
      if (ops.ne(zero, e.shift))
        s = ops.plus(s, ops.times(e.shift, transform));

      neighbors.push([w, s, ops.plus(s, mappedPos[w])]);
    }

    neighbors.sort(([wa, sa, pa], [wb, sb, pb]) => ops.cmp(pa, pb));

    for (const [wo, s, p] of neighbors) {
      const wn = old2new[wo];
      if (wn == null) {
        yield [vn, next, zero];
        old2new[wo] = next++;
        newPos[wo] = p;
        queue.push([wo, s]);
      }
      else if (wn < vn) {
        continue;
      }
      else {
        const rawShift = ops.minus(p, newPos[wo]);
        let shift;
        if (basisAdjustment != null) {
          shift = ops.times(rawShift, basisAdjustment);
        }
        else if (ops.sgn(rawShift) == 0) {
          shift = rawShift;
        }
        else {
          if (essentialShifts.length) {
            shift = _solveInRows(rawShift, essentialShifts);
          }
          if (shift == null) {
            shift = ops.unitVector(graph.dim, essentialShifts.length);
            essentialShifts.push(rawShift);
            if (essentialShifts.length == graph.dim) {
              basisAdjustment = ops.inverse(essentialShifts);
            }
          }
          else {
            shift = shift[0].concat(ops.vector(graph.dim - shift[0].length));
          }
        }
        if (vn < wn || (vn == wn && ops.sgn(shift) < 0)) {
          yield [vn, wn, shift];
        }
      }
    }
  }
};

const _cmpSteps = ([headA, tailA, shiftA], [headB, tailB, shiftB]) =>
  (headA - headB) || (tailA - tailB) || ops.cmp(shiftA, shiftB);

const _postprocessTraversal = trav => {
  let basis = null;
  for (const [head, tail, shift] of trav)
    basis = rationalLinearAlgebraModular.extendBasis(shift, basis);
  basis = basis.map(v => ops.lt(v, []) ? ops.negative(v) : v);

  const basisChange = ops.inverse(basis);

  return trav.map(([head, tail, shift]) => {
    const newShift = ops.times(shift, basisChange);
    if (newShift.some(x => !ops.isInteger(x))) {
      throw new Error("panic: produced non-integer shift");
    }
    if (head == tail && ops.sgn(newShift) > 0) {
      return [head, tail, ops.negative(newShift)];
    }
    else {
      return [head, tail, newShift];
    }
  });
};

const _goodBases = (graph, bases) => {
  const adj = pg.adjacencies(graph);

  const atLoop = bases.filter(basis => {
    const v = basis[0].head;
    return adj[v].every(e => e.tail == v);
  });

  if (atLoop.length > 0)
    return atLoop;

  const atLune = bases.filter(basis => {
    const v = basis[0].head;
    const neighbours = adj[v].map(e => e.tail).sort();
    return neighbours.some((w, i) => i == 0 || w == neighbours[i - 1]);
  });

  if (atLune.length > 0)
    return atLune;

  const maxDeg = Object.keys(adj).map(v => adj[v].length).max();
  return bases.filter(basis => adj[basis[0].head].length == maxDeg);
};

export const invariant = graph => {
  const pos = pg.barycentricPlacement(graph);
  const sym = ps.symmetries(graph);

  let best = null;

  for (const basis of _goodBases(graph, sym.representativeBases)) {
    const v = basis[0].head;
    const transform = ops.inverse(basis.map(e => pg.edgeVector(e, pos)));
    const trav = S.seq(_traversal(graph, v, transform));

    if (best == null)
      best = trav;
    else {
      for (let i = 0; ; ++i) {
        if (trav.drop(i).isNil)
          break;

        const d = _cmpSteps(trav.pick(i), best.pick(i));
        if (d < 0)
          best = trav;
        else if (d > 0)
          break;
      }
    }
  }

  return _postprocessTraversal(best.toArray()).sort(_cmpSteps);
}

if (require.main == module) {
  Array.prototype.toString = function() {
    return `[ ${this.map(x => x.toString()).join(', ')} ]`;
  };

  const test = g => {
    const trav = invariant(g);

    for (const e of trav)
      console.log(e);
    console.log();
  };

  test(pg.make([ [ 1, 2, [ 0, 0, 0 ] ],
                 [ 1, 2, [ 1, 0, 0 ] ],
                 [ 1, 2, [ 0, 1, 0 ] ],
                 [ 1, 2, [ 0, 0, 1 ] ] ]));

  test(pg.make([ [ 1, 2, [ 0, 0 ] ],
                 [ 1, 2, [ 1, 0 ] ],
                 [ 2, 3, [ 0, 0 ] ],
                 [ 2, 3, [ 0, 1 ] ],
                 [ 1, 3, [ 0, 0 ] ],
                 [ 1, 3, [ 1, 1 ] ] ]));
}
<|endoftext|>


<commit_before>import * as I from 'immutable';

import { rationalMethods, rationals } from '../arithmetic/types';
import * as mats from '../arithmetic/matrices';

import * as pg from './periodic';
import * as ps from './symmetries';

const ops = pg.ops;

const X = rationalMethods.register(
  mats.methods(rationals, ['Integer', 'LongInt', 'Fraction'], false)
).ops();

const _solveInRows = (v, M) => {
  const tmp = ops.solution(ops.transposed(M), ops.transposed(v));
  return tmp && ops.transposed(tmp);
};

const _traversal = function* _traversal(
  graph,
  v0,
  transform,
  adj = pg.adjacencies(graph),
  pos = pg.barycentricPlacement(graph))
{
  const zero = ops.vector(graph.dim);

  const old2new = {[v0]: 1};
  const newPos = {[v0]: zero};
  const queue = [v0];
  const essentialShifts = [];

  let next = 2;
  let basisAdjustment = null;

  while (queue.length) {
    const vo = queue.shift();
    const vn = old2new[vo];
    const pv = newPos[vo];

    const incident = pg.allIncidences(graph, vo, adj);
    const M = ops.times(incident.map(e => pg.edgeVector(e, pos)), transform);

    const neighbors = incident
      .map((e, i) => [ops.plus(pv, M[i]), e.tail])
      .sort(([sa, wa], [sb, wb]) => ops.cmp(sa, sb));

    for (const [s, wo] of neighbors) {
      const wn = old2new[wo];
      if (wn == null) {
        yield [vn, next, zero];
        old2new[wo] = next++;
        newPos[wo] = s;
        queue.push(wo);
      }
      else if (wn < vn) {
        continue;
      }
      else {
        const rawShift = ops.minus(s, newPos[wo]);
        let shift;
        if (basisAdjustment != null) {
          shift = ops.times(rawShift, basisAdjustment);
        }
        else if (ops.sgn(rawShift) == 0) {
          shift = rawShift;
        }
        else {
          if (essentialShifts.length) {
            shift = _solveInRows(rawShift, essentialShifts);
          }
          if (shift == null) {
            shift = ops.unitVector(graph.dim, essentialShifts.length);
            essentialShifts.push(rawShift);
            if (essentialShifts.length == graph.dim) {
              basisAdjustment = ops.inverse(essentialShifts);
            }
          }
          else {
            shift = shift[0].concat(ops.vector(graph.dim - shift[0].length));
          }
        }
        if (vn < wn || (vn == wn && ops.sgn(shift) < 0)) {
          yield [vn, wn, shift];
        }
      }
    }
  }
};

const _cmpSteps = ([headA, tailA, shiftA], [headB, tailB, shiftB]) =>
  (headA - headB) || (tailA - tailB) || ops.cmp(shiftA, shiftB);

const _postprocessTraversal = trav => {
  const A = trav.map(([head, tail, shift]) => shift);
  const basis = X.triangulation(A).R.slice(0, A[0].length);
  const basisChange = ops.inverse(basis);

  return trav.map(([head, tail, shift]) => {
    const newShift = ops.times(shift, basisChange);
    if (head == tail && ops.sgn(shift) > 0) {
      return [head, tail, ops.negative(newShift)];
    }
    else {
      return [head, tail, newShift];
    }
  });
};

export function invariant(
  graph,
  adj = pg.adjacencies(graph),
  pos = pg.barycentricPlacement(graph),
  bases = ps.characteristicBases(graph, adj, pos),
  sym = ps.symmetries(graph, adj, pos, bases))
{
  let best = null;

  for (const basis of sym.representativeBases) {
    const v = basis[0].head;
    const transform = ops.inverse(basis.map(e => pg.edgeVector(e, pos)));
    const trav = I.Seq(_traversal(graph, v, transform, adj, pos));

    if (best == null) {
      best = trav;
    }
    else {
      for (let i = 0; ; ++i) {
        const next = trav.get(i);
        if (next == null) {
          break;
        }

        const d = _cmpSteps(next, best.get(i));
        if (d < 0) {
          best = trav;
        }
        else if (d > 0) {
          break;
        }
      }
    }
  }

  return _postprocessTraversal(I.List(best).toArray()).sort(_cmpSteps);
}

if (require.main == module) {
  Array.prototype.toString = function() {
    return `[ ${this.map(x => x.toString()).join(', ')} ]`;
  };

  const test = g => {
    const trav = invariant(g);

    for (const e of trav)
      console.log(e);
    console.log();
  };

  test(pg.make([ [ 1, 2, [ 0, 0, 0 ] ],
                 [ 1, 2, [ 1, 0, 0 ] ],
                 [ 1, 2, [ 0, 1, 0 ] ],
                 [ 1, 2, [ 0, 0, 1 ] ] ]));

  test(pg.make([ [ 1, 2, [ 0, 0 ] ],
                 [ 1, 2, [ 1, 0 ] ],
                 [ 2, 3, [ 0, 0 ] ],
                 [ 2, 3, [ 0, 1 ] ],
                 [ 1, 3, [ 0, 0 ] ],
                 [ 1, 3, [ 1, 1 ] ] ]));
}
<commit_msg>fix small problem in invariant postprocessing; throw on non-integer shift<commit_after>import * as I from 'immutable';

import { rationalMethods, rationals } from '../arithmetic/types';
import * as mats from '../arithmetic/matrices';

import * as pg from './periodic';
import * as ps from './symmetries';

const ops = pg.ops;

const X = rationalMethods.register(
  mats.methods(rationals, ['Integer', 'LongInt', 'Fraction'], false)
).ops();

const _solveInRows = (v, M) => {
  const tmp = ops.solution(ops.transposed(M), ops.transposed(v));
  return tmp && ops.transposed(tmp);
};

const _traversal = function* _traversal(
  graph,
  v0,
  transform,
  adj = pg.adjacencies(graph),
  pos = pg.barycentricPlacement(graph))
{
  const zero = ops.vector(graph.dim);

  const old2new = {[v0]: 1};
  const newPos = {[v0]: zero};
  const queue = [v0];
  const essentialShifts = [];

  let next = 2;
  let basisAdjustment = null;

  while (queue.length) {
    const vo = queue.shift();
    const vn = old2new[vo];
    const pv = newPos[vo];

    const incident = pg.allIncidences(graph, vo, adj);
    const M = ops.times(incident.map(e => pg.edgeVector(e, pos)), transform);

    const neighbors = incident
      .map((e, i) => [ops.plus(pv, M[i]), e.tail])
      .sort(([sa, wa], [sb, wb]) => ops.cmp(sa, sb));

    for (const [s, wo] of neighbors) {
      const wn = old2new[wo];
      if (wn == null) {
        yield [vn, next, zero];
        old2new[wo] = next++;
        newPos[wo] = s;
        queue.push(wo);
      }
      else if (wn < vn) {
        continue;
      }
      else {
        const rawShift = ops.minus(s, newPos[wo]);
        let shift;
        if (basisAdjustment != null) {
          shift = ops.times(rawShift, basisAdjustment);
        }
        else if (ops.sgn(rawShift) == 0) {
          shift = rawShift;
        }
        else {
          if (essentialShifts.length) {
            shift = _solveInRows(rawShift, essentialShifts);
          }
          if (shift == null) {
            shift = ops.unitVector(graph.dim, essentialShifts.length);
            essentialShifts.push(rawShift);
            if (essentialShifts.length == graph.dim) {
              basisAdjustment = ops.inverse(essentialShifts);
            }
          }
          else {
            shift = shift[0].concat(ops.vector(graph.dim - shift[0].length));
          }
        }
        if (vn < wn || (vn == wn && ops.sgn(shift) < 0)) {
          yield [vn, wn, shift];
        }
      }
    }
  }
};

const _cmpSteps = ([headA, tailA, shiftA], [headB, tailB, shiftB]) =>
  (headA - headB) || (tailA - tailB) || ops.cmp(shiftA, shiftB);

const _postprocessTraversal = trav => {
  const A = trav.map(([head, tail, shift]) => shift);
  const basis = X.triangulation(A).R.slice(0, A[0].length);
  const basisChange = ops.inverse(basis);

  return trav.map(([head, tail, shift]) => {
    const newShift = ops.times(shift, basisChange);
    if (newShift.some(x => !ops.isInteger(x))) {
      throw new Error("panic: produced non-integer shift");
    }
    if (head == tail && ops.sgn(newShift) > 0) {
      return [head, tail, ops.negative(newShift)];
    }
    else {
      return [head, tail, newShift];
    }
  });
};

export function invariant(
  graph,
  adj = pg.adjacencies(graph),
  pos = pg.barycentricPlacement(graph),
  bases = ps.characteristicBases(graph, adj, pos),
  sym = ps.symmetries(graph, adj, pos, bases))
{
  let best = null;

  for (const basis of sym.representativeBases) {
    const v = basis[0].head;
    const transform = ops.inverse(basis.map(e => pg.edgeVector(e, pos)));
    const trav = I.Seq(_traversal(graph, v, transform, adj, pos));

    if (best == null) {
      best = trav;
    }
    else {
      for (let i = 0; ; ++i) {
        const next = trav.get(i);
        if (next == null) {
          break;
        }

        const d = _cmpSteps(next, best.get(i));
        if (d < 0) {
          best = trav;
        }
        else if (d > 0) {
          break;
        }
      }
    }
  }

  return _postprocessTraversal(I.List(best).toArray()).sort(_cmpSteps);
}

if (require.main == module) {
  Array.prototype.toString = function() {
    return `[ ${this.map(x => x.toString()).join(', ')} ]`;
  };

  const test = g => {
    const trav = invariant(g);

    for (const e of trav)
      console.log(e);
    console.log();
  };

  test(pg.make([ [ 1, 2, [ 0, 0, 0 ] ],
                 [ 1, 2, [ 1, 0, 0 ] ],
                 [ 1, 2, [ 0, 1, 0 ] ],
                 [ 1, 2, [ 0, 0, 1 ] ] ]));

  test(pg.make([ [ 1, 2, [ 0, 0 ] ],
                 [ 1, 2, [ 1, 0 ] ],
                 [ 2, 3, [ 0, 0 ] ],
                 [ 2, 3, [ 0, 1 ] ],
                 [ 1, 3, [ 0, 0 ] ],
                 [ 1, 3, [ 1, 1 ] ] ]));
}
<|endoftext|>