<commit_before>/**
 * @file BilliardBot GUI module.
 * @author paranoid-androids@googlegroups.com
 */

define(function(require) {
    "use strict";

    var GUIListener = require('guilistener');
    var GameLogic = require('gamelogic');

    /**
     * Constructs a new pool GUI.
     * @constructor
     * @exports GUI
     * @param container {!Element} The container to use.
     */
    function GUI(container) {
        this.canvas = container;
    }

    /**
     * The simulation speed of the engine. 1.0 is normal; higher values make
     * the game faster.
     */
    GUI.TIME_SCALE = 0.7;

    /**
     * The width of the table, in meters.
     * @constant {number}
     */
    GUI.TABLE_WIDTH = 2.7;

    /**
     * The height of the table, in meters.
     * @constant {number}
     */
    GUI.TABLE_HEIGHT = GUI.TABLE_WIDTH / 2;

    /**
     * The width of the table, in pixels.
     * @constant {number}
     */
    GUI.WIDTH = 800;

    /**
     * The pixels per meter.
     * @constant {number}
     */
    GUI.PPM = GUI.WIDTH / GUI.TABLE_WIDTH;

    /**
     * The height of the table, in pixels.
     * @constant {number}
     */
    GUI.HEIGHT = GUI.TABLE_HEIGHT * GUI.PPM;

    /**
     * The radius of each ball, in pixels.
     * @constant {number}
     */
    GUI.BALL_RADIUS = (0.05715 * GUI.PPM) / 2;

    /**
     * Matter.js options for each ball.
     * @constant {object}
     */
    GUI.BALL_OPTIONS = {
        frictionAir: 0.015,
        friction: 0.0001,
        restitution: 0.92,
        render: {
            lineWidth: 0,
            strokeStyle: "transparent",
            fillStyle: "white"
        }
    };

    // TODO: if needed, improve the appearance of the pockets.
    /**
     * The radius of the pocket mouth on the corners, in pixels.
     * @constant {number}
     */
    GUI.POCKET_RADIUS_CORNER = (0.115 * GUI.PPM) / 2 * 1.75;

    /**
     * The radius of the pocket mouth on the side, in pixels.
     * @constant {number}
     */
    GUI.POCKET_RADIUS_SIDE = (0.129 * GUI.PPM) / 2;

    /**
     * Matter.js options for each ball.
     * @constant {object}
     */
    GUI.POCKET_OPTIONS = {
        isStatic: true,
        label: 'pocket',
        slop: 3,
        render: {
            lineWidth: 1,
            strokeStyle: "white",
            fillStyle: "black"
        }
    };

    /**
     * The speed of the fastest ball below which the turn is considered to be
     * over.
     * @constant {number}
     */
    GUI.SPEED_THRESHOLD = 0.05;

    /**
     * The width of the wall, in pixels.
     * @constant {number}
     */
    GUI.WALL_WIDTH = 40;

    /**
     * Matter.js options for the wall.
     */
    GUI.WALL_OPTIONS = {
        isStatic: true,
        slop: 0.02,
        render: {
            lineWidth: 3,
            strokeStyle: "white",
            fillStyle: "brown"
        }
    }

    GUI.POCKETS = [
        {x: 0, y: 0, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH, y: 0, radius: GUI.POCKET_RADIUS_CORNER},
        {x: 0, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH / 2, y: 0, radius: GUI.POCKET_RADIUS_SIDE},
        {x: GUI.WIDTH / 2, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_SIDE},
    ]

    /**
     * Whether the GUI is currently idle and waiting for someone to take a
     * shot.
     * @type {!boolean}
     * @private
     */
    GUI.prototype.waitingForShot = true;

    /**
     * The current listener.
     * @type {?GUIListener}
     * @private
     */
    GUI.prototype.listener = null;

    /**
     * The number of balls that have fallen into a pocket.
     * @type {!number}
     */
    GUI.prototype.numPocketedBalls = 0;

    var Matter = require('third_party/matter');

    // Matter module aliases
    var Engine = Matter.Engine,
        World = Matter.World,
        Body = Matter.Body,
        Bodies = Matter.Bodies,
        Common = Matter.Common,
        Composites = Matter.Composites,
        Composite = Matter.Composite,
        Events = Matter.Events,
        MouseConstraint = Matter.MouseConstraint,
        Runner = Matter.Runner;

    /**
     * Sets the event listener.
     * @param {GUIListener} listener
     */
    GUI.prototype.setListener = function(listener) {
        this.listener = listener;
    }

    GUI.prototype.init = function() {
        var self = this;

        // Create the Matter.js engine.
        this.canvas.width = GUI.WIDTH;
        this.canvas.height = GUI.HEIGHT;
        this.engine = Engine.create({
            render: {
                canvas: this.canvas,
                options: {
                    width: GUI.WIDTH,
                    height: GUI.HEIGHT,
                    wireframes: false,
                    background: "#31B94D"
                }
            }
        });

        // TODO: Temporary fix: show balls sunk as text.
        this.ballsSunk = document.getElementById("balls-sunk");
        this.currentPlayer = document.getElementById("current-player");

        // Add some some walls to the world.
        // Note that these values were carefully calculated by first hiding all the pockets and
        // seeing that the balls could fit.
        var wallOffset = GUI.WALL_WIDTH / 4;
        var pocketOffset = GUI.POCKET_RADIUS_CORNER * 2;
        World.add(this.engine.world, [
            // Left wall.
            Bodies.rectangle(-wallOffset, GUI.HEIGHT / 2, GUI.WALL_WIDTH, GUI.HEIGHT -
                pocketOffset, GUI.WALL_OPTIONS),
            // Right wall.
            Bodies.rectangle(GUI.WIDTH + wallOffset, GUI.HEIGHT / 2, GUI.WALL_WIDTH,
                GUI.HEIGHT - pocketOffset, GUI.WALL_OPTIONS),
            // Top wall (broken up into two).
            Bodies.rectangle(GUI.WIDTH / 4 + pocketOffset / 12, -wallOffset, (
                GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH, GUI.WALL_OPTIONS),
            Bodies.rectangle(GUI.WIDTH * 3 / 4 - pocketOffset / 12, -wallOffset, (
                GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH, GUI.WALL_OPTIONS),
            // Bottom wall (broken up into two).
            Bodies.rectangle(GUI.WIDTH / 4 + pocketOffset / 12, GUI.HEIGHT +
                wallOffset, (GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH,
                GUI.WALL_OPTIONS
            ),
            Bodies.rectangle(GUI.WIDTH * 3 / 4 - pocketOffset / 12, GUI.HEIGHT +
                wallOffset, (GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH,
                GUI.WALL_OPTIONS
            )
        ]);

        // Add pockets.
        GUI.POCKETS.forEach(function (pocket) {
            World.add(self.engine.world, Bodies.circle(pocket.x, pocket.y, pocket.radius, GUI.POCKET_OPTIONS));
        });

        // Create the rack.
        this.setupRack();

        // Create the cue ball.
        this.placeCue();

        Events.on(this.engine, 'afterTick', function(event) {
            if (!self.waitingForShot) {
                // Remove balls that fall in pockets.
                self.removePocketedBalls();

                // Detect when the balls have stopped moving.
                var totalSpeed = self.calculateMaximumBallSpeed();
                if (totalSpeed >= 0 && totalSpeed < GUI.SPEED_THRESHOLD) {
                    self.waitingForShot = true;
                    if (self.listener) {
                        self.listener.onBallsStopped();
                    }
                }
            }

        });

        // Run the engine, turning off gravity.
        this.engine.world.gravity = {
            x: 0,
            y: 0
        };
        this.engine.timing.timeScale = GUI.TIME_SCALE;
        Engine.run(this.engine);
    };

    /**
     * Takes a shot from the given position with the given force vector.
     * @param {!object} The position.
     * @param {!forceVector} The force vector.
     */
    GUI.prototype.takeShot = function(position, forceVector) {
        Body.applyForce(this.cue, position, forceVector);
        this.waitingForShot = false;
    }

    GUI.prototype.takeShot2 = function(ball, position, forceVector) {
        Body.applyForce(ball, position, forceVector);
        this.waitingForShot = false;
    }

    /**
     * @return {!number} The width of the table, in pixels.
     */
    GUI.prototype.getWidth = function() {
        return WIDTH;
    }

    /**
     * @return {!number} The height of the table, in pixels.
     */
    GUI.prototype.getHeight = function() {
        return HEIGHT;
    }

    /**
     * @return {!object} The position of the cue ball.
     */
    GUI.prototype.getCue = function() {
        return this.cue;
    }

    GUI.prototype.placeBall = function(position, ball) {
        this.cue = Bodies.circle(GUI.WIDTH / 4, GUI.HEIGHT / 2, GUI.BALL_RADIUS,
            GUI.BALL_OPTIONS);
        Body.setMass(this.cue, .17);
        this.cue.label = GameLogic.BALL_LABEL_PREFIX + "cue";
        World.add(this.engine.world, this.cue);
    }

    GUI.prototype.placeCue = function() {
        this.cue = Bodies.circle(GUI.WIDTH / 4, GUI.HEIGHT / 2, GUI.BALL_RADIUS,
            GUI.BALL_OPTIONS);
        Body.setMass(this.cue, .17);
        this.cue.label = GameLogic.BALL_LABEL_PREFIX + "cue";
        World.add(this.engine.world, this.cue);
    }

    /**
     * Shows which player is currently taking their turn.
     * @param {number} player The active player.
     */
    GUI.prototype.updateCurrentPlayer = function(player) {
        this.currentPlayer.innerHTML = player;
    }

    GUI.prototype.getBallsOnTable = function() {
        var allBodies = Composite.allBodies(this.engine.world);
        var ballsOnTable = [];
        allBodies.forEach(function(ball) {
            if (ball.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0) {
                ballsOnTable.push(ball);
            }
        });

        return ballsOnTable;
    }

    GUI.prototype.getPockets = function() {
        return GUI.POCKETS;
    }

    /**
     * Used to detect when all the balls have stopped moving on screen in order
     * to alert the listener module to take its next turn.
     * @return {number} The speed of the fastest balls.
     */
    GUI.prototype.calculateMaximumBallSpeed = function() {
        var allBodies = Composite.allBodies(this.engine.world);
        var biggestSpeed = 0;
        for (var i = 0; i < allBodies.length; i++) {
            var ball = allBodies[i];
            if (ball.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0) {
                biggestSpeed = Math.max(biggestSpeed, ball.speed);
            }
        }

        return biggestSpeed;
    }

    /**
     * Sets up the default rack.
     */
    GUI.prototype.setupRack = function() {
        var x = (3 * GUI.WIDTH / 4);
        var y = GUI.HEIGHT / 2;
        var ballsPerRow = 1;
        var ballList = this.listener.createInitialBallList();
        var currentBall = 0;

        for (var i = 0; i < 5; i++) {
            var currentY = y
            for (var j = 0; j < ballsPerRow; j++) {
                var ball = Bodies.circle(x, currentY, GUI.BALL_RADIUS, GUI.BALL_OPTIONS);
                Body.setMass(ball, .17);
                ball.render.fillStyle = GameLogic.BALL_COLORS[ballList[currentBall] - 1];
                ball.label = GameLogic.BALL_LABEL_PREFIX + ballList[currentBall];

                World.add(this.engine.world, ball);
                currentY += (GUI.BALL_RADIUS * 2);
                currentBall += 1;
            }
            ballsPerRow += 1;

            x += Math.sqrt(Math.pow(GUI.BALL_RADIUS * 2, 2) - Math.pow(GUI.BALL_RADIUS,
                2));
            y -= GUI.BALL_RADIUS;
        }
    }

    /**
     * Remove any balls that have fallen into pockets.
     */
    GUI.prototype.removePocketedBalls = function() {
        var self = this;

        var allBodies = Composite.allBodies(this.engine.world);
        var activeBalls = allBodies.filter(function(b) {
            return b.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0;
        });
        var wallOffset = GUI.WALL_WIDTH / 3 * (1 - GUI.WALL_OPTIONS.slop);

        activeBalls.forEach(function(ball) {
            if (ball.position.x < wallOffset || ball.position.x > GUI.WIDTH -
                wallOffset || ball.position.y < wallOffset || ball.position.y >
                GUI.HEIGHT - wallOffset) {
                Composite.removeBody(self.engine.world, ball)

                self.numPocketedBalls++;

                if(self.listener) {
                    self.listener.ballSunk(ball);
                }
            }
        });
    };

    GUI.prototype.endGame = function() {
        console.log("end game");
        Runner.stop(this.engine);
    }

    return GUI;
});<commit_msg>abstract out ball placement<commit_after>/**
 * @file BilliardBot GUI module.
 * @author paranoid-androids@googlegroups.com
 */

define(function(require) {
    "use strict";

    var GUIListener = require('guilistener');
    var GameLogic = require('gamelogic');

    /**
     * Constructs a new pool GUI.
     * @constructor
     * @exports GUI
     * @param container {!Element} The container to use.
     */
    function GUI(container) {
        this.canvas = container;
    }

    /**
     * The simulation speed of the engine. 1.0 is normal; higher values make
     * the game faster.
     */
    GUI.TIME_SCALE = 0.7;

    /**
     * The width of the table, in meters.
     * @constant {number}
     */
    GUI.TABLE_WIDTH = 2.7;

    /**
     * The height of the table, in meters.
     * @constant {number}
     */
    GUI.TABLE_HEIGHT = GUI.TABLE_WIDTH / 2;

    /**
     * The width of the table, in pixels.
     * @constant {number}
     */
    GUI.WIDTH = 800;

    /**
     * The pixels per meter.
     * @constant {number}
     */
    GUI.PPM = GUI.WIDTH / GUI.TABLE_WIDTH;

    /**
     * The height of the table, in pixels.
     * @constant {number}
     */
    GUI.HEIGHT = GUI.TABLE_HEIGHT * GUI.PPM;

    /**
     * The radius of each ball, in pixels.
     * @constant {number}
     */
    GUI.BALL_RADIUS = (0.05715 * GUI.PPM) / 2;

    /**
     * Matter.js options for each ball.
     * @constant {object}
     */
    GUI.BALL_OPTIONS = {
        frictionAir: 0.015,
        friction: 0.0001,
        restitution: 0.92,
        render: {
            lineWidth: 0,
            strokeStyle: "transparent",
            fillStyle: "white"
        }
    };

    // TODO: if needed, improve the appearance of the pockets.
    /**
     * The radius of the pocket mouth on the corners, in pixels.
     * @constant {number}
     */
    GUI.POCKET_RADIUS_CORNER = (0.115 * GUI.PPM) / 2 * 1.75;

    /**
     * The radius of the pocket mouth on the side, in pixels.
     * @constant {number}
     */
    GUI.POCKET_RADIUS_SIDE = (0.129 * GUI.PPM) / 2;

    /**
     * Matter.js options for each ball.
     * @constant {object}
     */
    GUI.POCKET_OPTIONS = {
        isStatic: true,
        label: 'pocket',
        slop: 3,
        render: {
            lineWidth: 1,
            strokeStyle: "white",
            fillStyle: "black"
        }
    };

    /**
     * The speed of the fastest ball below which the turn is considered to be
     * over.
     * @constant {number}
     */
    GUI.SPEED_THRESHOLD = 0.05;

    /**
     * The width of the wall, in pixels.
     * @constant {number}
     */
    GUI.WALL_WIDTH = 40;

    /**
     * Matter.js options for the wall.
     */
    GUI.WALL_OPTIONS = {
        isStatic: true,
        slop: 0.02,
        render: {
            lineWidth: 3,
            strokeStyle: "white",
            fillStyle: "brown"
        }
    }

    GUI.POCKETS = [
        {x: 0, y: 0, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH, y: 0, radius: GUI.POCKET_RADIUS_CORNER},
        {x: 0, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_CORNER},
        {x: GUI.WIDTH / 2, y: 0, radius: GUI.POCKET_RADIUS_SIDE},
        {x: GUI.WIDTH / 2, y: GUI.HEIGHT, radius: GUI.POCKET_RADIUS_SIDE},
    ]

    /**
     * Whether the GUI is currently idle and waiting for someone to take a
     * shot.
     * @type {!boolean}
     * @private
     */
    GUI.prototype.waitingForShot = true;

    /**
     * The current listener.
     * @type {?GUIListener}
     * @private
     */
    GUI.prototype.listener = null;

    /**
     * The number of balls that have fallen into a pocket.
     * @type {!number}
     */
    GUI.prototype.numPocketedBalls = 0;

    var Matter = require('third_party/matter');

    // Matter module aliases
    var Engine = Matter.Engine,
        World = Matter.World,
        Body = Matter.Body,
        Bodies = Matter.Bodies,
        Common = Matter.Common,
        Composites = Matter.Composites,
        Composite = Matter.Composite,
        Events = Matter.Events,
        MouseConstraint = Matter.MouseConstraint,
        Runner = Matter.Runner;

    /**
     * Sets the event listener.
     * @param {GUIListener} listener
     */
    GUI.prototype.setListener = function(listener) {
        this.listener = listener;
    }

    GUI.prototype.init = function() {
        var self = this;

        // Create the Matter.js engine.
        this.canvas.width = GUI.WIDTH;
        this.canvas.height = GUI.HEIGHT;
        this.engine = Engine.create({
            render: {
                canvas: this.canvas,
                options: {
                    width: GUI.WIDTH,
                    height: GUI.HEIGHT,
                    wireframes: false,
                    background: "#31B94D"
                }
            }
        });

        // TODO: Temporary fix: show balls sunk as text.
        this.ballsSunk = document.getElementById("balls-sunk");
        this.currentPlayer = document.getElementById("current-player");

        // Add some some walls to the world.
        // Note that these values were carefully calculated by first hiding all the pockets and
        // seeing that the balls could fit.
        var wallOffset = GUI.WALL_WIDTH / 4;
        var pocketOffset = GUI.POCKET_RADIUS_CORNER * 2;
        World.add(this.engine.world, [
            // Left wall.
            Bodies.rectangle(-wallOffset, GUI.HEIGHT / 2, GUI.WALL_WIDTH, GUI.HEIGHT -
                pocketOffset, GUI.WALL_OPTIONS),
            // Right wall.
            Bodies.rectangle(GUI.WIDTH + wallOffset, GUI.HEIGHT / 2, GUI.WALL_WIDTH,
                GUI.HEIGHT - pocketOffset, GUI.WALL_OPTIONS),
            // Top wall (broken up into two).
            Bodies.rectangle(GUI.WIDTH / 4 + pocketOffset / 12, -wallOffset, (
                GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH, GUI.WALL_OPTIONS),
            Bodies.rectangle(GUI.WIDTH * 3 / 4 - pocketOffset / 12, -wallOffset, (
                GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH, GUI.WALL_OPTIONS),
            // Bottom wall (broken up into two).
            Bodies.rectangle(GUI.WIDTH / 4 + pocketOffset / 12, GUI.HEIGHT +
                wallOffset, (GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH,
                GUI.WALL_OPTIONS
            ),
            Bodies.rectangle(GUI.WIDTH * 3 / 4 - pocketOffset / 12, GUI.HEIGHT +
                wallOffset, (GUI.WIDTH - pocketOffset * 7 / 5) / 2, GUI.WALL_WIDTH,
                GUI.WALL_OPTIONS
            )
        ]);

        // Add pockets.
        GUI.POCKETS.forEach(function (pocket) {
            World.add(self.engine.world, Bodies.circle(pocket.x, pocket.y, pocket.radius, GUI.POCKET_OPTIONS));
        });

        // Create the rack.
        this.setupRack();

        // Create the cue ball.
        this.placeCue();

        Events.on(this.engine, 'afterTick', function(event) {
            if (!self.waitingForShot) {
                // Remove balls that fall in pockets.
                self.removePocketedBalls();

                // Detect when the balls have stopped moving.
                var totalSpeed = self.calculateMaximumBallSpeed();
                if (totalSpeed >= 0 && totalSpeed < GUI.SPEED_THRESHOLD) {
                    self.waitingForShot = true;
                    if (self.listener) {
                        self.listener.onBallsStopped();
                    }
                }
            }

        });

        // Run the engine, turning off gravity.
        this.engine.world.gravity = {
            x: 0,
            y: 0
        };
        this.engine.timing.timeScale = GUI.TIME_SCALE;
        Engine.run(this.engine);
    };

    /**
     * Takes a shot from the given position with the given force vector.
     * @param {!object} The position.
     * @param {!forceVector} The force vector.
     */
    GUI.prototype.takeShot = function(position, forceVector) {
        Body.applyForce(this.cue, position, forceVector);
        this.waitingForShot = false;
    }

    GUI.prototype.takeShot2 = function(ball, position, forceVector) {
        Body.applyForce(ball, position, forceVector);
        this.waitingForShot = false;
    }

    /**
     * @return {!number} The width of the table, in pixels.
     */
    GUI.prototype.getWidth = function() {
        return WIDTH;
    }

    /**
     * @return {!number} The height of the table, in pixels.
     */
    GUI.prototype.getHeight = function() {
        return HEIGHT;
    }

    /**
     * @return {!object} The position of the cue ball.
     */
    GUI.prototype.getCue = function() {
        return this.cue;
    }

    GUI.prototype.createBall = function(position) {
        var ball = Bodies.circle(position.x, position.y, GUI.BALL_RADIUS, GUI.BALL_OPTIONS);
        Body.setMass(ball, .17);
        return ball;
    }

    GUI.prototype.placeBall = function(position, ball) {
        ball.position = position;
        World.add(this.engine.world, ball);
    }

    GUI.prototype.placeCueWithP = function(position) {
        this.cue = this.createBall(position);
        this.cue.label = GameLogic.BALL_LABEL_PREFIX + "cue";
        this.placeBall(position, this.cue);
    }

    GUI.prototype.placeCue = function() {
        this.placeCueWithP({x: GUI.WIDTH / 4, y: GUI.HEIGHT / 2});
    }

    /**
     * Shows which player is currently taking their turn.
     * @param {number} player The active player.
     */
    GUI.prototype.updateCurrentPlayer = function(player) {
        this.currentPlayer.innerHTML = player;
    }

    GUI.prototype.getBallsOnTable = function() {
        var allBodies = Composite.allBodies(this.engine.world);
        var ballsOnTable = [];
        allBodies.forEach(function(ball) {
            if (ball.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0) {
                ballsOnTable.push(ball);
            }
        });

        return ballsOnTable;
    }

    GUI.prototype.getPockets = function() {
        return GUI.POCKETS;
    }

    /**
     * Used to detect when all the balls have stopped moving on screen in order
     * to alert the listener module to take its next turn.
     * @return {number} The speed of the fastest balls.
     */
    GUI.prototype.calculateMaximumBallSpeed = function() {
        var allBodies = Composite.allBodies(this.engine.world);
        var biggestSpeed = 0;
        for (var i = 0; i < allBodies.length; i++) {
            var ball = allBodies[i];
            if (ball.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0) {
                biggestSpeed = Math.max(biggestSpeed, ball.speed);
            }
        }

        return biggestSpeed;
    }

    /**
     * Sets up the default rack.
     */
    GUI.prototype.setupRack = function() {
        var ball_x = (3 * GUI.WIDTH / 4);
        var ball_y = GUI.HEIGHT / 2;
        var ballsPerRow = 1;
        var ballList = this.listener.createInitialBallList();
        var currentBall = 0;

        for (var i = 0; i < 5; i++) {
            var currentY = ball_y
            for (var j = 0; j < ballsPerRow; j++) {

                var ball = this.createBall({x:ball_x, y:currentY});
                ball.render.fillStyle = GameLogic.BALL_COLORS[ballList[currentBall] - 1];
                ball.label = GameLogic.BALL_LABEL_PREFIX + ballList[currentBall];

                World.add(this.engine.world, ball);
                currentY += (GUI.BALL_RADIUS * 2);
                currentBall += 1;
            }
            ballsPerRow += 1;

            ball_x += Math.sqrt(Math.pow(GUI.BALL_RADIUS * 2, 2) - Math.pow(GUI.BALL_RADIUS, 2));
            ball_y -= GUI.BALL_RADIUS;
        }
    }

    /**
     * Remove any balls that have fallen into pockets.
     */
    GUI.prototype.removePocketedBalls = function() {
        var self = this;

        var allBodies = Composite.allBodies(this.engine.world);
        var activeBalls = allBodies.filter(function(b) {
            return b.label.indexOf(GameLogic.BALL_LABEL_PREFIX) == 0;
        });
        var wallOffset = GUI.WALL_WIDTH / 3 * (1 - GUI.WALL_OPTIONS.slop);

        activeBalls.forEach(function(ball) {
            if (ball.position.x < wallOffset || ball.position.x > GUI.WIDTH -
                wallOffset || ball.position.y < wallOffset || ball.position.y >
                GUI.HEIGHT - wallOffset) {
                Composite.removeBody(self.engine.world, ball)

                self.numPocketedBalls++;

                if(self.listener) {
                    self.listener.ballSunk(ball);
                }
            }
        });
    };

    GUI.prototype.endGame = function() {
        console.log("end game");
        Runner.stop(this.engine);
    }

    return GUI;
});<|endoftext|>


<commit_before>Application.Provenance.Services.factory('ProvDrafts', ["mcapi", "pubsub",
    function (mcapi, pubsub) {
        var service = {
            current: null,
            drafts: [],
            channel: 'drafts.update',

            newDraft: function () {
                var draft = {
                    id: "",
                    project_id: "",
                    owner: "",
                    description: "",
                    name: "",
                    birthtime: "",
                    attributes: {
                        process: {},
                        input_conditions: {},
                        output_conditions: {},
                        input_files: [],
                        output_files: [],
                        machine: {},
                        project_id: ""
                    }
                };

                return draft;
            },

            findDraft: function (id) {
                var i;
                for (i = 0; i < service.drafts.length; i++) {
                    if (service.drafts[i].id === id) {
                        return service.drafts[i];
                    }
                }
                return null;
            },

            _publishChange: function () {
                pubsub.send(service.channel, '');
            },

            saveDraft: function (f) {
                var callfunc = arguments.length === 1;
                if (service.current.id === "") {
                    // We haven't saved this draft before
                    mcapi('/drafts')
                        .success(function (draft) {
                            service.current.id = draft.id;
                            service.current.birthtime = draft.birthtime;
                            service.drafts.push(service.current);
                            service._publishChange();
                            if (callfunc) {
                                f();
                            }
                        }).post({
                            name: service.current.name,
                            description: service.current.description,
                            project_id: service.current.project_id,
                            process: service.current.process,
                            clone_number: service.current.clone_number
                        });
                } else {
                    // Need to update the draft
                    mcapi('/drafts/%', service.current.id)
                        .success(function () {
                            if (callfunc) {
                                f();
                            }
                        }).put({
                            name: service.current.name,
                            description: service.current.description,
                            project_id: service.current.project_id,
                            process: service.current.process,
                            clone_number: service.current.clone_number
                        });
                }
            },

            loadRemoteDrafts: function (f) {
                var callfunc = arguments.length === 1;
                mcapi('/drafts')
                    .success(function (drafts) {
                        service.drafts = [];
                        drafts.forEach(function (draft) {
                            service.drafts.push(draft);
                        });
                        service._publishChange();
                    }).jsonp();
            },

            prepareClone: function (df, clone_num) {
                var i;
                var clone_numbers = service.get_existing_clones(), new_draft = {}, count = 0, pattern = '', split_name = [], count_list = [], largest = 0;
                if (clone_num === '') {
                    pattern = df.process.name;
                } else {
                    split_name = df.clone_number.split('---');
                    pattern = split_name[0];
                }

                for (i = 0; i < clone_numbers.length; i++) {
                    if (clone_numbers[i].indexOf(pattern) > -1) {
                        split_name = clone_numbers[i].split('---');
                        if (split_name !== [""]) {
                            count_list.push(parseInt(split_name[1]));
                        }
                    }
                }
                if (count_list.length !== 0) {
                    largest = Math.max.apply(Math, count_list);
                    count = largest + 1;
                } else {
                    count = largest + 1;
                }
                new_draft = df;
                new_draft.id = '';
                new_draft.clone_number = pattern + '---' + count;
                new_draft.process.name = new_draft.clone_number;
                new_draft.process.input_files = [];
                new_draft.process.output_files = [];
                return new_draft;
            },

            get_existing_clones: function () {
                var clones = [];
                service.drafts.forEach(function (item) {
                    if (item.clone_number) {
                        clones.push(item.clone_number);
                    }

                });
                return clones;
            },

            deleteDraft: function (draft_id) {
                var i = _.indexOf(service.drafts, function (item) {
                    if (item.id === draft_id) {
                        return true;
                    }
                    return false;
                });
                if (i !== -1) {
                    service.drafts.splice(i, 1);
                    service._publishChange();
                }
            },

            deleteRemoteDraft: function (draft_id) {
                mcapi('/drafts/%', draft_id)
                    .success(function () {
                        var i = _.indexOf(service.drafts, function (item) {
                            if (item.id === draft_id) {
                                return true;
                            }
                            return false;
                        });
                        if (i !== -1) {
                            service.drafts.splice(i, 1);
                            service._publishChange();
                        }
                    }).delete();
            },

            clear: function () {
                service.drafts = [];
                service.current = null;
            }
        };

        return service;
    }]);
<commit_msg>update<commit_after>Application.Provenance.Services.factory('ProvDrafts', ["mcapi", "pubsub",
    function (mcapi, pubsub) {
        var service = {
            current: null,
            drafts: [],
            channel: 'drafts.update',

            newDraft: function () {
                var draft = {
                    id: "",
                    project_id: "",
                    owner: "",
                    description: "",
                    name: "",
                    birthtime: "",
                    process: {},
                    project_id: ""
                };
                return draft;
            },

            findDraft: function (id) {
                var i;
                for (i = 0; i < service.drafts.length; i++) {
                    if (service.drafts[i].id === id) {
                        return service.drafts[i];
                    }
                }
                return null;
            },

            _publishChange: function () {
                pubsub.send(service.channel, '');
            },

            saveDraft: function (f) {
                var callfunc = arguments.length === 1;
                if (service.current.id === "") {
                    // We haven't saved this draft before
                    mcapi('/drafts')
                        .success(function (draft) {
                            service.current.id = draft.id;
                            service.current.birthtime = draft.birthtime;
                            service.drafts.push(service.current);
                            service._publishChange();
                            if (callfunc) {
                                f();
                            }
                        }).post({
                            name: service.current.name,
                            description: service.current.description,
                            project_id: service.current.project_id,
                            process: service.current.process,
                            clone_number: service.current.clone_number
                        });
                } else {
                    // Need to update the draft
                    mcapi('/drafts/%', service.current.id)
                        .success(function () {
                            if (callfunc) {
                                f();
                            }
                        }).put({
                            name: service.current.name,
                            description: service.current.description,
                            project_id: service.current.project_id,
                            process: service.current.process,
                            clone_number: service.current.clone_number
                        });
                }
            },

            loadRemoteDrafts: function (f) {
                var callfunc = arguments.length === 1;
                mcapi('/drafts')
                    .success(function (drafts) {
                        service.drafts = [];
                        drafts.forEach(function (draft) {
                            service.drafts.push(draft);
                        });
                        service._publishChange();
                    }).jsonp();
            },

            prepareClone: function (df, clone_num) {
                var i;
                var clone_numbers = service.get_existing_clones(), new_draft = {}, count = 0, pattern = '', split_name = [], count_list = [], largest = 0;
                if (clone_num === '') {
                    pattern = df.process.name;
                } else {
                    split_name = df.clone_number.split('---');
                    pattern = split_name[0];
                }

                for (i = 0; i < clone_numbers.length; i++) {
                    if (clone_numbers[i].indexOf(pattern) > -1) {
                        split_name = clone_numbers[i].split('---');
                        if (split_name !== [""]) {
                            count_list.push(parseInt(split_name[1]));
                        }
                    }
                }
                if (count_list.length !== 0) {
                    largest = Math.max.apply(Math, count_list);
                    count = largest + 1;
                } else {
                    count = largest + 1;
                }
                new_draft = df;
                new_draft.id = '';
                new_draft.clone_number = pattern + '---' + count;
                new_draft.process.name = new_draft.clone_number;
                new_draft.process.input_files = [];
                new_draft.process.output_files = [];
                return new_draft;
            },

            get_existing_clones: function () {
                var clones = [];
                service.drafts.forEach(function (item) {
                    if (item.clone_number) {
                        clones.push(item.clone_number);
                    }

                });
                return clones;
            },

            deleteDraft: function (draft_id) {
                var i = _.indexOf(service.drafts, function (item) {
                    if (item.id === draft_id) {
                        return true;
                    }
                    return false;
                });
                if (i !== -1) {
                    service.drafts.splice(i, 1);
                    service._publishChange();
                }
            },

            deleteRemoteDraft: function (draft_id) {
                mcapi('/drafts/%', draft_id)
                    .success(function () {
                        var i = _.indexOf(service.drafts, function (item) {
                            if (item.id === draft_id) {
                                return true;
                            }
                            return false;
                        });
                        if (i !== -1) {
                            service.drafts.splice(i, 1);
                            service._publishChange();
                        }
                    }).delete();
            },

            clear: function () {
                service.drafts = [];
                service.current = null;
            }
        };

        return service;
    }]);
<|endoftext|>


<commit_before>/* =========================================================
 * bootstrap-modal.js v2.3.2
 * http://getbootstrap.com/2.3.2/javascript.html#modals
 * =========================================================
 * Copyright 2013 Twitter, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================= */

!function ($) {

  "use strict"; // jshint ;_;

 /* MODAL CLASS DEFINITION
  * ====================== */

  var Modal = function (element, options) {
    this.options = options
    this.$element = $(element)
      .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
    this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
  }

  Modal.prototype = {

      constructor: Modal

    , toggle: function () {
        return this[!this.isShown ? 'show' : 'hide']()
      }

    , show: function () {
        var that = this
          , e = $.Event('show')

        this.$element.trigger(e)

        if (this.isShown || e.isDefaultPrevented()) return

        this.isShown = true

        this.escape()

        this.backdrop(function () {
          var transition = $.support.transition && that.$element.hasClass('fade')

          if (!that.$element.parent().length) {
            that.$element.appendTo(document.body) //don't move modals dom position
          }

          that.$element.show()

          if (transition) {
            that.$element[0].offsetWidth // force reflow
          }

          that.$element
            .addClass('in')
            .attr('aria-hidden', false)

          that.enforceFocus()

          transition ?
            that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) :
            that.$element.focus().trigger('shown')

        })
      }

    , hide: function (e) {
        e && e.preventDefault()

        var that = this

        e = $.Event('hide')

        this.$element.trigger(e)

        if (!this.isShown || e.isDefaultPrevented()) return

        this.isShown = false

        this.escape()

        $(document).off('focusin.modal')

        this.$element
          .removeClass('in')
          .attr('aria-hidden', true)

        $.support.transition && this.$element.hasClass('fade') ?
          this.hideWithTransition() :
          this.hideModal()
      }

    , enforceFocus: function () {
        var that = this
        $(document)
          .off('focusin.bs.modal') // guard against infinite focus loop
          .on('focusin.bs.modal', function (e) {
          if (that.$element[0] !== e.target && !$(e.target).parents('.modal').length) {
            that.$element.focus()
          }
        })
      }

    , escape: function () {
        var that = this
        if (this.isShown && this.options.keyboard) {
          this.$element.on('keyup.dismiss.modal', function ( e ) {
            e.which == 27 && that.hide()
          })
        } else if (!this.isShown) {
          this.$element.off('keyup.dismiss.modal')
        }
      }

    , hideWithTransition: function () {
        var that = this
          , timeout = setTimeout(function () {
              that.$element.off($.support.transition.end)
              that.hideModal()
            }, 500)

        this.$element.one($.support.transition.end, function () {
          clearTimeout(timeout)
          that.hideModal()
        })
      }

    , hideModal: function () {
        var that = this
        this.$element.hide()
        this.backdrop(function () {
          that.removeBackdrop()
          that.$element.trigger('hidden')
        })
      }

    , removeBackdrop: function () {
        this.$backdrop && this.$backdrop.remove()
        this.$backdrop = null
      }

    , backdrop: function (callback) {
        var that = this
          , animate = this.$element.hasClass('fade') ? 'fade' : ''

        if (this.isShown && this.options.backdrop) {
          var doAnimate = $.support.transition && animate

          this.$backdrop = $('
')
            .appendTo(document.body)

          this.$backdrop.click(
            this.options.backdrop == 'static' ?
              $.proxy(this.$element[0].focus, this.$element[0])
            : $.proxy(this.hide, this)
          )

          if (doAnimate) this.$backdrop[0].offsetWidth // force reflow

          this.$backdrop.addClass('in')

          if (!callback) return

          doAnimate ?
            this.$backdrop.one($.support.transition.end, callback) :
            callback()

        } else if (!this.isShown && this.$backdrop) {
          this.$backdrop.removeClass('in')

          $.support.transition && this.$element.hasClass('fade')?
            this.$backdrop.one($.support.transition.end, callback) :
            callback()

        } else if (callback) {
          callback()
        }
      }
  }

 /* MODAL PLUGIN DEFINITION
  * ======================= */

  var old = $.fn.modal

  $.fn.modal = function (option) {
    return this.each(function () {
      var $this = $(this)
        , data = $this.data('modal')
        , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
      if (!data) $this.data('modal', (data = new Modal(this, options)))
      if (typeof option == 'string') data[option]()
      else if (options.show) data.show()
    })
  }

  $.fn.modal.defaults = {
      backdrop: true
    , keyboard: true
    , show: true
  }

  $.fn.modal.Constructor = Modal

 /* MODAL NO CONFLICT
  * ================= */

  $.fn.modal.noConflict = function () {
    $.fn.modal = old
    return this
  }

 /* MODAL DATA-API
  * ============== */

  $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) {
    var $this = $(this)
      , href = $this.attr('href')
      , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
      , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())

    e.preventDefault()

    $target
      .modal(option)
      .one('hide', function () {
        $this.focus()
      })
  })

}(window.jQuery);
<commit_msg>Change `enforceFocus` to use a synthesized .bootstrap-modal class<commit_after>/* =========================================================
 * bootstrap-modal.js v2.3.2
 * http://getbootstrap.com/2.3.2/javascript.html#modals
 * =========================================================
 * Copyright 2013 Twitter, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================= */

!function ($) {

  "use strict"; // jshint ;_;

 /* MODAL CLASS DEFINITION
  * ====================== */

  var Modal = function (element, options) {
    this.options = options
    this.$element = $(element)
      .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
    this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
  }

  Modal.prototype = {

      constructor: Modal

    , toggle: function () {
        return this[!this.isShown ? 'show' : 'hide']()
      }

    , show: function () {
        var that = this
          , e = $.Event('show')

        this.$element.trigger(e)

        if (this.isShown || e.isDefaultPrevented()) return

        this.isShown = true

        this.escape()

        this.backdrop(function () {
          var transition = $.support.transition && that.$element.hasClass('fade')

          if (!that.$element.parent().length) {
            that.$element.appendTo(document.body) //don't move modals dom position
          }

          that.$element.show()

          if (transition) {
            that.$element[0].offsetWidth // force reflow
          }

          that.$element
            .addClass('bootstrap-modal')
            .addClass('in')
            .attr('aria-hidden', false)

          that.enforceFocus()

          transition ?
            that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) :
            that.$element.focus().trigger('shown')

        })
      }

    , hide: function (e) {
        e && e.preventDefault()

        var that = this

        e = $.Event('hide')

        this.$element.trigger(e)

        if (!this.isShown || e.isDefaultPrevented()) return

        this.isShown = false

        this.escape()

        $(document).off('focusin.modal')

        this.$element
          .removeClass('in')
          .removeClass('bootstrap-modal')
          .attr('aria-hidden', true)

        $.support.transition && this.$element.hasClass('fade') ?
          this.hideWithTransition() :
          this.hideModal()
      }

    , enforceFocus: function () {
        var that = this
        $(document)
          .off('focusin.bs.modal') // guard against infinite focus loop
          .on('focusin.bs.modal', function (e) {
          if (that.$element[0] !== e.target && !$(e.target).parents('.bootstrap-modal').length) {
            that.$element.focus()
          }
        })
      }

    , escape: function () {
        var that = this
        if (this.isShown && this.options.keyboard) {
          this.$element.on('keyup.dismiss.modal', function ( e ) {
            e.which == 27 && that.hide()
          })
        } else if (!this.isShown) {
          this.$element.off('keyup.dismiss.modal')
        }
      }

    , hideWithTransition: function () {
        var that = this
          , timeout = setTimeout(function () {
              that.$element.off($.support.transition.end)
              that.hideModal()
            }, 500)

        this.$element.one($.support.transition.end, function () {
          clearTimeout(timeout)
          that.hideModal()
        })
      }

    , hideModal: function () {
        var that = this
        this.$element.hide()
        this.backdrop(function () {
          that.removeBackdrop()
          that.$element.trigger('hidden')
        })
      }

    , removeBackdrop: function () {
        this.$backdrop && this.$backdrop.remove()
        this.$backdrop = null
      }

    , backdrop: function (callback) {
        var that = this
          , animate = this.$element.hasClass('fade') ? 'fade' : ''

        if (this.isShown && this.options.backdrop) {
          var doAnimate = $.support.transition && animate

          this.$backdrop = $('
')
            .appendTo(document.body)

          this.$backdrop.click(
            this.options.backdrop == 'static' ?
              $.proxy(this.$element[0].focus, this.$element[0])
            : $.proxy(this.hide, this)
          )

          if (doAnimate) this.$backdrop[0].offsetWidth // force reflow

          this.$backdrop.addClass('in')

          if (!callback) return

          doAnimate ?
            this.$backdrop.one($.support.transition.end, callback) :
            callback()

        } else if (!this.isShown && this.$backdrop) {
          this.$backdrop.removeClass('in')

          $.support.transition && this.$element.hasClass('fade')?
            this.$backdrop.one($.support.transition.end, callback) :
            callback()

        } else if (callback) {
          callback()
        }
      }
  }

 /* MODAL PLUGIN DEFINITION
  * ======================= */

  var old = $.fn.modal

  $.fn.modal = function (option) {
    return this.each(function () {
      var $this = $(this)
        , data = $this.data('modal')
        , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
      if (!data) $this.data('modal', (data = new Modal(this, options)))
      if (typeof option == 'string') data[option]()
      else if (options.show) data.show()
    })
  }

  $.fn.modal.defaults = {
      backdrop: true
    , keyboard: true
    , show: true
  }

  $.fn.modal.Constructor = Modal

 /* MODAL NO CONFLICT
  * ================= */

  $.fn.modal.noConflict = function () {
    $.fn.modal = old
    return this
  }

 /* MODAL DATA-API
  * ============== */

  $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) {
    var $this = $(this)
      , href = $this.attr('href')
      , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
      , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())

    e.preventDefault()

    $target
      .modal(option)
      .one('hide', function () {
        $this.focus()
      })
  })

}(window.jQuery);
<|endoftext|>


<commit_before>/**
 * Created by diakabanab on 10/26/2016.
 */
define(["jquery", "chord/Positions"], function($, Positions) {
    var Interface = function(container) {
        this.element = $("<div>", {
            "id": "Interaction"
        }).appendTo(container);

        this.element.on("mousedown", this.mouseup.bind(this));
        this.element.on("touchstart", this.click.bind(this));
        this.element.on("touchend", this.mouseup.bind(this));
        this.element.on("mouseup", this.mouseup.bind(this));

        this.onstart = function(){};
        this.onend = function(){};
    };

    /**
     * Gets the chord at its given position
     */
    Interface.prototype.getChord = function(x, y) {
        var twoPi = Math.PI * 2;
        var width = this.element.width;
        var height = this.element.height;
        x = ((x - width / 2) / width) * 2;
        y = ((y - height / 2) / height) * 2;
        var theta = Math.atan2(y, x);
        var radius = Math.sqrt(x*x + y*y);

        var i = 0;
        for (var letter in Positions.major) {
            var coordinate = Positions.major[letter];
            if (theta > coordinate.startAngle && theta < coordinate.endAngle) {
                break;
            } else if (theta + twoPu > coordinate.startAngle && theta + twoPi < coordinate.endAngle) {
                break;
            }
            i++;
        }
        if (radius < Positions.innerRadius) {
            return [Positions.minorOrder[i], "minor"];
        } else {
            return [Positions.majorOrder[i], "major"];
        }
    };
});<commit_msg>adding finished interface.js<commit_after>/**
 * Created by diakabanab on 10/26/2016.
 */
define(["jquery", "chord/Positions"], function($, Positions) {
    var Interface = function(container) {
        this.element = $("<div>", {
            "id": "Interaction"
        }).appendTo(container);

        this.element.on("mousedown", this.mouseup.bind(this));
        this.element.on("touchstart", this.touchstart.bind(this));
        this.element.on("touchend", this.mouseup.bind(this));
        this.element.on("mouseup", this.mouseup.bind(this));

        this.onstart = function(){};
        this.onend = function(){};
    };

    /**
     * Gets the chord at its given position
     */
    Interface.prototype.getChord = function(x, y) {
        var twoPi = Math.PI * 2;
        var width = this.element.width;
        var height = this.element.height;
        x = ((x - width / 2) / width) * 2;
        y = ((y - height / 2) / height) * 2;
        var theta = Math.atan2(y, x);
        var radius = Math.sqrt(x*x + y*y);

        var i = 0;
        for (var letter in Positions.major) {
            var coordinate = Positions.major[letter];
            if (theta > coordinate.startAngle && theta < coordinate.endAngle) {
                break;
            } else if (theta + twoPu > coordinate.startAngle && theta + twoPi < coordinate.endAngle) {
                break;
            }
            i++;
        }
        if (radius < Positions.innerRadius) {
            return [Positions.minorOrder[i], "minor"];
        } else {
            return [Positions.majorOrder[i], "major"];
        }
    };

    Interface.prototype.mousedown = function(e) {
        e.preventDefault();
        e.stopPropagation();
        var res = this.getChord(e.offsetX, e.offsetY);
        this.onstart(res[0], res[1]);
    };

    Interface.prototype.touchstart = function(e) {
        e.preventDefault();
        e.stopPropagation();
        var touches = e.originalEvent.touches;
        var offset = this.element.offset();

        if (touches.length > 0){
            var touch = touches[0];
            var res = this.getChord(touch.pageX - offset.left, touch.pageY - offset.top);
            this.onstart(res[0], res[1]);
        }
    };

    Interface.prototype.mouseup = function(e) {
        this.onend();
    };

    return Interface;
});<|endoftext|>


<commit_before>/**
 * Converts a string of a given format into an array.
 *
 * For example:
 *
 *  "foo[bar][baz]"
 *
 * Will be converted to:
 *
 *  ["foo", "bar", "baz"]
 *
 * @method toArray
 * @param {String}
 * @return {Array}
 */
export default function(str) {
  let result = [],
      index = str.indexOf("[");

  if (index === -1) {
    // if there are no square brackets
    result.push(str);
  } else {
    if (index > 0) {
      // if the square bracket is not the first character
      let name = str.slice(0, index);
      result.push(name);
    }

    let nestedNames = str.match(/[^[\]]+(?=])/g);

    result = result.concat(nestedNames);
  }

  return result;
}
<commit_msg>added more comments to to-array<commit_after>/**
 * Converts a string of a given format into an array.
 *
 * For example:
 *
 *  "foo[bar][baz]"
 *
 * Will be converted to:
 *
 *  ["foo", "bar", "baz"]
 *
 * @method toArray
 * @param {String}
 * @return {Array}
 */
export default function(str) {
  let result = [],
      index = str.indexOf("[");

  if (index === -1) {
    // if there are no square brackets
    result.push(str);
  } else {
    if (index > 0) {
      // if the square bracket is not the first character, add the name at the
      // start of the string (not in brackets)
      let name = str.slice(0, index);
      result.push(name);
    }

    // pull out the names in brackets and add to the array
    let nestedNames = str.match(/[^[\]]+(?=])/g);

    result = result.concat(nestedNames);
  }

  return result;
}
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc service
 * @name polestar.Spec
 * @description
 * # Spec
 * Service in the polestar.
 */
angular.module('polestar')
  .service('Spec', function(_, vg, vl, cql, ZSchema, Alerts, Config, Dataset, Schema, Pills, Chart, consts, util, FilterManager, ANY, Logger) {

    var keys =  _.keys(Schema.schema.definitions.Encoding.properties);

    function instantiate() {
      return {
        data: Config.data,
        transform: {
          filterInvalid: undefined
        },
        mark: ANY,
        encoding: keys.reduce(function(e, c) {
          e[c] = {};
          return e;
        }, {}),
        config: Config.config
      };
    }

    var Spec = {
      /** @type {Object} verbose spec edited by the UI */
      spec: null,
      chart: Chart.getChart(null),
      isSpecific: true // Polestar always have specific spec (except for mark)
    };

    Spec._removeEmptyFieldDefs = function(spec) {
      spec.encoding = _.omit(spec.encoding, function(fieldDef, channel) {
        return !fieldDef || (fieldDef.field === undefined && fieldDef.value === undefined) ||
          (spec.mark && ! vl.channel.supportMark(channel, spec.mark));
      });
    };

    function deleteNulls(obj) {
      for (var prop in obj) {
        if (_.isObject(obj[prop])) {
          deleteNulls(obj[prop]);
        }
        // This is why I hate js
        if (obj[prop] === null ||
          obj[prop] === undefined ||
          (
            // In general, {} should be removed from spec. bin:{} is an exception.
            _.isObject(obj[prop]) &&
            vg.util.keys(obj[prop]).length === 0 &&
            prop !== 'bin'
          ) ||
          obj[prop] === []) {
          delete obj[prop];
        }
      }
    }

    function parse(spec) {
      var oldSpec = util.duplicate(spec);
      var oldFilter = null;

      if (oldSpec) {
        // Store oldFilter, copy oldSpec that exclude transform.filter
        oldFilter = (oldSpec.transform || {}).filter;
        var transform = _.omit(oldSpec.transform || {}, 'filter');
        oldSpec = _.omit(oldSpec, 'transform');
        if (transform) {
          oldSpec.transform = transform;
        }
      }

      var newSpec = vl.util.mergeDeep(instantiate(), oldSpec);

      // This is not Vega-Lite filter object, but rather our FilterModel
      newSpec.transform.filter = FilterManager.reset(oldFilter);

      return newSpec;
    }

    // takes a partial spec
    Spec.parseSpec = function(newSpec) {
      // TODO: revise this
      Spec.spec = parse(newSpec);
    };

    Spec.reset = function() {
      var spec = instantiate();
      spec.transform.filter = FilterManager.reset();
      Spec.spec = spec;
    };

    /**
     * Takes a full spec, validates it and then rebuilds all members of the chart object.
     */
    Spec.update = function(spec) {
      spec = _.cloneDeep(spec || Spec.spec);

      Spec._removeEmptyFieldDefs(spec);
      deleteNulls(spec);

      if (spec.transform && spec.transform.filter) {
        delete spec.transform.filter;
      }

      var filter = FilterManager.getVlFilter();
      if (filter) {
        spec.transform = spec.transform || {};
        spec.transform.filter = filter;
      }

      // we may have removed encoding
      if (!('encoding' in spec)) {
        spec.encoding = {};
      }
      if (!('config' in spec)) {
        spec.config = {};
      }
      // var validator = new ZSchema();

      // validator.setRemoteReference('http://json-schema.org/draft-04/schema', {});

      // var schema = Schema.schema;

      // ZSchema.registerFormat('color', function (str) {
      //   // valid colors are in list or hex color
      //   return /^#([0-9a-f]{3}){1,2}$/i.test(str);
      //   // TODO: support color name
      // });
      // ZSchema.registerFormat('font', function () {
      //   // right now no fonts are valid
      //   return false;
      // });

      // // now validate the spec
      // var valid = validator.validate(spec, schema);

      // if (!valid) {
      //   //FIXME: move this dependency to directive/controller layer
      //   Alerts.add({
      //     msg: validator.getLastErrors()
      //   });
      // } else {
        vg.util.extend(spec.config, Config.small());

        if (!Dataset.schema) { return Spec; }

        var query = getQuery(spec);
        if (_.isEqual(query, Spec.cleanQuery)) {
          return Spec; // no need to update charts
        }
        Spec.cleanQuery = query;
        var output = cql.query(query, Dataset.schema);
        Spec.query = output.query;
        var topItem = output.result.getTopSpecQueryModel();
        Spec.chart = Chart.getChart(topItem);
    // }
      return Spec;
    };

    function getSpecQuery(spec, convertFilter /*HACK*/) {
      if (convertFilter) {
        spec = util.duplicate(spec);

        // HACK convert filter manager to proper filter spec
        if (spec.transform && spec.transform.filter) {
          delete spec.transform.filter;
        }

        var filter = FilterManager.getVlFilter();
        if (filter) {
          spec.transform = spec.transform || {};
          spec.transform.filter = filter;
        }
      }

      return {
        data: Config.data,
        mark: spec.mark === ANY ? '?' : spec.mark,

        // TODO: support transform enumeration
        transform: spec.transform,
        encodings: vg.util.keys(spec.encoding).reduce(function(encodings, channelId) {
          var encQ = vg.util.extend(
            // Add channel
            { channel: Pills.isAnyChannel(channelId) ? '?' : channelId },
            // Field Def
            spec.encoding[channelId],
            // Remove Title
            {title: undefined}
          );

          if (cql.enumSpec.isEnumSpec(encQ.field)) {
            // replace the name so we should it's the field from this channelId
            encQ.field = {
              name: 'f' + channelId,
              enum: encQ.field.enum
            };
          }

          encodings.push(encQ);
          return encodings;
        }, []),
        config: spec.config
      };
    }

    function getQuery(spec, convertFilter /*HACK */) {
      var specQuery = getSpecQuery(spec, convertFilter);

      var hasAnyField = _.some(specQuery.encodings, function(encQ) {
        return cql.enumSpec.isEnumSpec(encQ.field);
      });

      var groupBy = hasAnyField ?
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack'] :
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack', 'channel']; // do not group by mark

      return {
        spec: specQuery,
        groupBy: groupBy,
        orderBy: ['aggregationQuality', 'effectiveness'], // FIXME add field order
        chooseBy: 'effectiveness',
        config: {
          omitTableWithOcclusion: false
        }
      };
    }

    function instantiatePill(channel) { // jshint ignore:line
      return {};
    }

    /** copy value from the pill to the fieldDef */
    function updateChannelDef(encoding, pill, channel){
      var type = pill.type,
        supportedRole = vl.channel.getSupportedRole(channel),
        dimensionOnly = supportedRole.dimension && !supportedRole.measure;

      // auto cast binning / time binning for dimension only encoding type.
      if (pill.field && dimensionOnly) {
        if (pill.aggregate==='count') {
          pill = {};
        } else if (type === vl.type.QUANTITATIVE && !pill.bin) {
          pill.aggregate = undefined;
          pill.bin = {maxbins: vl.bin.MAXBINS_DEFAULT};
        } else if(type === vl.type.TEMPORAL && !pill.timeUnit) {
          pill.timeUnit = consts.defaultTimeFn;
        }
      } else if (!pill.field) {
        // no field, it's actually the empty shelf that
        // got processed in the opposite direction
        pill = {};
      }

      // filter unsupported properties
      var fieldDef = instantiatePill(channel),
        shelfProps = Schema.getChannelSchema(channel).properties;

      for (var prop in shelfProps) {
        if (pill[prop]) {
          if (prop==='value' && pill.field) {
            // only copy value if field is not defined
            // (which should never be the case)
            delete fieldDef[prop];
          } else {
            //FXIME In some case this should be merge / recursive merge instead ?
            fieldDef[prop] = pill[prop];
          }
        }
      }
      encoding[channel] = fieldDef;
    }

    Pills.listener = {
      set: function(channelId, pill) {
        updateChannelDef(Spec.spec.encoding, pill, channelId);
      },
      remove: function(channelId) {
        updateChannelDef(Spec.spec.encoding, {}, channelId); // remove all pill detail from the fieldDef
      },
      add: function(fieldDef) {
        var oldMarkIsEnumSpec = cql.enumSpec.isEnumSpec(Spec.cleanQuery.spec.mark);

        Logger.logInteraction(Logger.actions.ADD_FIELD, fieldDef, {
          from: Spec.chart.shorthand
        });

        if (Spec.isSpecific && !cql.enumSpec.isEnumSpec(fieldDef.field)) {
          // Call CompassQL to run query and load the top-ranked result
          var specQuery = Spec.cleanQuery.spec;
          var encQ = _.extend(
            {},
            fieldDef,
            {
              channel: cql.enumSpec.SHORT_ENUM_SPEC
            },
            fieldDef.aggregate === 'count' ? {} : {
              aggregate: cql.enumSpec.SHORT_ENUM_SPEC,
              bin: cql.enumSpec.SHORT_ENUM_SPEC,
              timeUnit: cql.enumSpec.SHORT_ENUM_SPEC
            }
          );
          specQuery.encodings.push(encQ);

          var query = {
            spec: specQuery,
            groupBy: ['field', 'aggregate', 'bin', 'timeUnit', 'stack'],
            orderBy: 'aggregationQuality',
            chooseBy: 'effectiveness',
            config: {omitTableWithOcclusion: false}
          };

          var output = cql.query(query, Dataset.schema);
          var result = output.result;

          // The top spec will always have specific mark.
          // We need to restore the mark to ANY if applicable.
          var topSpec = result.getTopSpecQueryModel().toSpec();
          if (oldMarkIsEnumSpec) {
            topSpec.mark = ANY;
          }
          Spec.parseSpec(topSpec);
        } else {
          var encoding = _.clone(Spec.spec.encoding);
          // Just add to any channel because CompassQL do not support partial filling yet.
          var emptyAnyChannel = Pills.getEmptyAnyChannelId();
          updateChannelDef(encoding, _.clone(fieldDef), emptyAnyChannel);

          // Add new any as a placeholder
          var newAnyChannel = Pills.getNextAnyChannelId();
          if (newAnyChannel !== null) {
            updateChannelDef(encoding, {}, newAnyChannel);
          }

          Spec.spec.encoding = encoding;
        }

      },
      select: function(spec) {
        var specQuery = getSpecQuery(spec);
        specQuery.mark = '?';

        var query = {
          spec: specQuery,
          chooseBy: 'effectiveness'
        };
        var output = cql.query(query, Dataset.schema);
        var result = output.result;

        if (result.getTopSpecQueryModel().getMark() === spec.mark) {
          // make a copy and replace mark with '?'
          spec = util.duplicate(spec);
          spec.mark = ANY;
        }
        Spec.parseSpec(spec);
      },
      parse: function(spec) {
        Spec.parseSpec(spec);
      },
      update: function(spec) {
        Spec.update(spec);
      },
      reset: function() {
        Spec.reset();
      },
      dragDrop: function(cidDragTo, cidDragFrom) {
        // Make a copy and update the clone of the encoding to prevent glitches
        var encoding = _.clone(Spec.spec.encoding);
        // console.log('dragDrop', encoding, Pills, 'from:', cidDragFrom, Pills.get(cidDragFrom));

        // If pill is dragged from another shelf, not the schemalist
        if (cidDragFrom) {
          // console.log('pillDragFrom', Pills.get(cidDragFrom));
          updateChannelDef(encoding, Pills.get(cidDragFrom) || {}, cidDragFrom);
        }
        updateChannelDef(encoding, Pills.get(cidDragTo) || {}, cidDragTo);

        // console.log('Pills.dragDrop',
        //   'from:', cidDragFrom, Pills.get(cidDragFrom), encoding[cidDragFrom],
        //   'to:', cidDragTo, Pills.get(cidDragTo), encoding[cidDragTo]);

        // Finally, update the encoding only once to prevent glitches
        Spec.spec.encoding = encoding;
      },
      rescale: function (channelId, scaleType) {
        var fieldDef = Spec.spec.encoding[channelId];
        if (fieldDef.scale) {
          fieldDef.scale.type = scaleType;
        } else {
          fieldDef.scale = {type: scaleType};
        }
      },
      sort: function(channelId, sort) {
        Spec.spec.encoding[channelId].sort = sort;
      },
      transpose: function() {
        Chart.transpose(Spec.spec);
      },
      toggleFilterInvalid: function () {
        Spec.spec.transform.filterInvalid = Spec.spec.transform.filterInvalid ? undefined : true;
      }
    };

    Spec.reset();
    Dataset.onUpdate.push(Spec.reset);

    return Spec;
  });
<commit_msg>Fix spec doesn't sync<commit_after>'use strict';

/**
 * @ngdoc service
 * @name polestar.Spec
 * @description
 * # Spec
 * Service in the polestar.
 */
angular.module('polestar')
  .service('Spec', function(_, vg, vl, cql, ZSchema, Alerts, Config, Dataset, Schema, Pills, Chart, consts, util, FilterManager, ANY, Logger) {

    var keys =  _.keys(Schema.schema.definitions.Encoding.properties);

    function instantiate() {
      return {
        data: Config.data,
        transform: {
          filterInvalid: undefined
        },
        mark: ANY,
        encoding: keys.reduce(function(e, c) {
          e[c] = {};
          return e;
        }, {}),
        config: Config.config
      };
    }

    var Spec = {
      /** @type {Object} verbose spec edited by the UI */
      spec: null,
      chart: Chart.getChart(null),
      isSpecific: true // Polestar always have specific spec (except for mark)
    };

    Spec._removeEmptyFieldDefs = function(spec) {
      spec.encoding = _.omit(spec.encoding, function(fieldDef, channel) {
        return !fieldDef || (fieldDef.field === undefined && fieldDef.value === undefined) ||
          (spec.mark && ! vl.channel.supportMark(channel, spec.mark));
      });
    };

    function deleteNulls(obj) {
      for (var prop in obj) {
        if (_.isObject(obj[prop])) {
          deleteNulls(obj[prop]);
        }
        // This is why I hate js
        if (obj[prop] === null ||
          obj[prop] === undefined ||
          (
            // In general, {} should be removed from spec. bin:{} is an exception.
            _.isObject(obj[prop]) &&
            vg.util.keys(obj[prop]).length === 0 &&
            prop !== 'bin'
          ) ||
          obj[prop] === []) {
          delete obj[prop];
        }
      }
    }

    function parse(spec) {
      var oldSpec = util.duplicate(spec);
      var oldFilter = null;

      if (oldSpec) {
        // Store oldFilter, copy oldSpec that exclude transform.filter
        oldFilter = (oldSpec.transform || {}).filter;
        var transform = _.omit(oldSpec.transform || {}, 'filter');
        oldSpec = _.omit(oldSpec, 'transform');
        if (transform) {
          oldSpec.transform = transform;
        }
      }

      var newSpec = vl.util.mergeDeep(instantiate(), oldSpec);

      // This is not Vega-Lite filter object, but rather our FilterModel
      newSpec.transform.filter = FilterManager.reset(oldFilter);

      return newSpec;
    }

    // takes a partial spec
    Spec.parseSpec = function(newSpec) {
      // TODO: revise this
      Spec.spec = parse(newSpec);
    };

    Spec.reset = function() {
      var spec = instantiate();
      spec.transform.filter = FilterManager.reset();
      Spec.spec = spec;
    };

    /**
     * Takes a full spec, validates it and then rebuilds all members of the chart object.
     */
    Spec.update = function(spec) {
      spec = _.cloneDeep(spec || Spec.spec);

      Spec._removeEmptyFieldDefs(spec);
      deleteNulls(spec);

      if (spec.transform && spec.transform.filter) {
        delete spec.transform.filter;
      }

      var filter = FilterManager.getVlFilter();
      if (filter) {
        spec.transform = spec.transform || {};
        spec.transform.filter = filter;
      }

      // we may have removed encoding
      if (!('encoding' in spec)) {
        spec.encoding = {};
      }
      if (!('config' in spec)) {
        spec.config = {};
      }
      // var validator = new ZSchema();

      // validator.setRemoteReference('http://json-schema.org/draft-04/schema', {});

      // var schema = Schema.schema;

      // ZSchema.registerFormat('color', function (str) {
      //   // valid colors are in list or hex color
      //   return /^#([0-9a-f]{3}){1,2}$/i.test(str);
      //   // TODO: support color name
      // });
      // ZSchema.registerFormat('font', function () {
      //   // right now no fonts are valid
      //   return false;
      // });

      // // now validate the spec
      // var valid = validator.validate(spec, schema);

      // if (!valid) {
      //   //FIXME: move this dependency to directive/controller layer
      //   Alerts.add({
      //     msg: validator.getLastErrors()
      //   });
      // } else {
        vg.util.extend(spec.config, Config.small());

        if (!Dataset.schema) { return Spec; }

        var query = Spec.cleanQuery = getQuery(spec);
        if (_.isEqual(query, Spec.oldCleanQuery)) {
          return Spec; // no need to update charts
        }
        Spec.oldCleanQuery = _.cloneDeep(query);
        var output = cql.query(query, Dataset.schema);
        Spec.query = output.query;
        var topItem = output.result.getTopSpecQueryModel();
        Spec.chart = Chart.getChart(topItem);
    // }
      return Spec;
    };

    function getSpecQuery(spec, convertFilter /*HACK*/) {
      if (convertFilter) {
        spec = util.duplicate(spec);

        // HACK convert filter manager to proper filter spec
        if (spec.transform && spec.transform.filter) {
          delete spec.transform.filter;
        }

        var filter = FilterManager.getVlFilter();
        if (filter) {
          spec.transform = spec.transform || {};
          spec.transform.filter = filter;
        }
      }

      return {
        data: Config.data,
        mark: spec.mark === ANY ? '?' : spec.mark,

        // TODO: support transform enumeration
        transform: spec.transform,
        encodings: vg.util.keys(spec.encoding).reduce(function(encodings, channelId) {
          var encQ = vg.util.extend(
            // Add channel
            { channel: Pills.isAnyChannel(channelId) ? '?' : channelId },
            // Field Def
            spec.encoding[channelId],
            // Remove Title
            {title: undefined}
          );

          if (cql.enumSpec.isEnumSpec(encQ.field)) {
            // replace the name so we should it's the field from this channelId
            encQ.field = {
              name: 'f' + channelId,
              enum: encQ.field.enum
            };
          }

          encodings.push(encQ);
          return encodings;
        }, []),
        config: spec.config
      };
    }

    function getQuery(spec, convertFilter /*HACK */) {
      var specQuery = getSpecQuery(spec, convertFilter);

      var hasAnyField = _.some(specQuery.encodings, function(encQ) {
        return cql.enumSpec.isEnumSpec(encQ.field);
      });

      var groupBy = hasAnyField ?
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack'] :
        ['field', 'aggregate', 'bin', 'timeUnit', 'stack', 'channel']; // do not group by mark

      return {
        spec: specQuery,
        groupBy: groupBy,
        orderBy: ['aggregationQuality', 'effectiveness'], // FIXME add field order
        chooseBy: 'effectiveness',
        config: {
          omitTableWithOcclusion: false
        }
      };
    }

    function instantiatePill(channel) { // jshint ignore:line
      return {};
    }

    /** copy value from the pill to the fieldDef */
    function updateChannelDef(encoding, pill, channel){
      var type = pill.type,
        supportedRole = vl.channel.getSupportedRole(channel),
        dimensionOnly = supportedRole.dimension && !supportedRole.measure;

      // auto cast binning / time binning for dimension only encoding type.
      if (pill.field && dimensionOnly) {
        if (pill.aggregate==='count') {
          pill = {};
        } else if (type === vl.type.QUANTITATIVE && !pill.bin) {
          pill.aggregate = undefined;
          pill.bin = {maxbins: vl.bin.MAXBINS_DEFAULT};
        } else if(type === vl.type.TEMPORAL && !pill.timeUnit) {
          pill.timeUnit = consts.defaultTimeFn;
        }
      } else if (!pill.field) {
        // no field, it's actually the empty shelf that
        // got processed in the opposite direction
        pill = {};
      }

      // filter unsupported properties
      var fieldDef = instantiatePill(channel),
        shelfProps = Schema.getChannelSchema(channel).properties;

      for (var prop in shelfProps) {
        if (pill[prop]) {
          if (prop==='value' && pill.field) {
            // only copy value if field is not defined
            // (which should never be the case)
            delete fieldDef[prop];
          } else {
            //FXIME In some case this should be merge / recursive merge instead ?
            fieldDef[prop] = pill[prop];
          }
        }
      }
      encoding[channel] = fieldDef;
    }

    Pills.listener = {
      set: function(channelId, pill) {
        updateChannelDef(Spec.spec.encoding, pill, channelId);
      },
      remove: function(channelId) {
        updateChannelDef(Spec.spec.encoding, {}, channelId); // remove all pill detail from the fieldDef
      },
      add: function(fieldDef) {
        var oldMarkIsEnumSpec = cql.enumSpec.isEnumSpec(Spec.cleanQuery.spec.mark);

        Logger.logInteraction(Logger.actions.ADD_FIELD, fieldDef, {
          from: Spec.chart.shorthand
        });

        if (Spec.isSpecific && !cql.enumSpec.isEnumSpec(fieldDef.field)) {
          // Call CompassQL to run query and load the top-ranked result
          var specQuery = Spec.cleanQuery.spec;
          var encQ = _.extend(
            {},
            fieldDef,
            {
              channel: cql.enumSpec.SHORT_ENUM_SPEC
            },
            fieldDef.aggregate === 'count' ? {} : {
              aggregate: cql.enumSpec.SHORT_ENUM_SPEC,
              bin: cql.enumSpec.SHORT_ENUM_SPEC,
              timeUnit: cql.enumSpec.SHORT_ENUM_SPEC
            }
          );
          specQuery.encodings.push(encQ);

          var query = {
            spec: specQuery,
            groupBy: ['field', 'aggregate', 'bin', 'timeUnit', 'stack'],
            orderBy: 'aggregationQuality',
            chooseBy: 'effectiveness',
            config: {omitTableWithOcclusion: false}
          };

          var output = cql.query(query, Dataset.schema);
          var result = output.result;

          // The top spec will always have specific mark.
          // We need to restore the mark to ANY if applicable.
          var topSpec = result.getTopSpecQueryModel().toSpec();
          if (oldMarkIsEnumSpec) {
            topSpec.mark = ANY;
          }
          Spec.parseSpec(topSpec);
        } else {
          var encoding = _.clone(Spec.spec.encoding);
          // Just add to any channel because CompassQL do not support partial filling yet.
          var emptyAnyChannel = Pills.getEmptyAnyChannelId();
          updateChannelDef(encoding, _.clone(fieldDef), emptyAnyChannel);

          // Add new any as a placeholder
          var newAnyChannel = Pills.getNextAnyChannelId();
          if (newAnyChannel !== null) {
            updateChannelDef(encoding, {}, newAnyChannel);
          }

          Spec.spec.encoding = encoding;
        }

      },
      select: function(spec) {
        var specQuery = getSpecQuery(spec);
        specQuery.mark = '?';

        var query = {
          spec: specQuery,
          chooseBy: 'effectiveness'
        };
        var output = cql.query(query, Dataset.schema);
        var result = output.result;

        if (result.getTopSpecQueryModel().getMark() === spec.mark) {
          // make a copy and replace mark with '?'
          spec = util.duplicate(spec);
          spec.mark = ANY;
        }
        Spec.parseSpec(spec);
      },
      parse: function(spec) {
        Spec.parseSpec(spec);
      },
      update: function(spec) {
        Spec.update(spec);
      },
      reset: function() {
        Spec.reset();
      },
      dragDrop: function(cidDragTo, cidDragFrom) {
        // Make a copy and update the clone of the encoding to prevent glitches
        var encoding = _.clone(Spec.spec.encoding);
        // console.log('dragDrop', encoding, Pills, 'from:', cidDragFrom, Pills.get(cidDragFrom));

        // If pill is dragged from another shelf, not the schemalist
        if (cidDragFrom) {
          // console.log('pillDragFrom', Pills.get(cidDragFrom));
          updateChannelDef(encoding, Pills.get(cidDragFrom) || {}, cidDragFrom);
        }
        updateChannelDef(encoding, Pills.get(cidDragTo) || {}, cidDragTo);

        // console.log('Pills.dragDrop',
        //   'from:', cidDragFrom, Pills.get(cidDragFrom), encoding[cidDragFrom],
        //   'to:', cidDragTo, Pills.get(cidDragTo), encoding[cidDragTo]);

        // Finally, update the encoding only once to prevent glitches
        Spec.spec.encoding = encoding;
      },
      rescale: function (channelId, scaleType) {
        var fieldDef = Spec.spec.encoding[channelId];
        if (fieldDef.scale) {
          fieldDef.scale.type = scaleType;
        } else {
          fieldDef.scale = {type: scaleType};
        }
      },
      sort: function(channelId, sort) {
        Spec.spec.encoding[channelId].sort = sort;
      },
      transpose: function() {
        Chart.transpose(Spec.spec);
      },
      toggleFilterInvalid: function () {
        Spec.spec.transform.filterInvalid = Spec.spec.transform.filterInvalid ? undefined : true;
      }
    };

    Spec.reset();
    Dataset.onUpdate.push(Spec.reset);

    return Spec;
  });
<|endoftext|>