<commit_before>'use strict';

var Fisher = require('./fisher').Fisher;
var Microworld = require('../models/microworld-model').Microworld;
var OceanLog = require('./ocean-log').OceanLog;
var Run = require('../models/run-model').Run;

var io;

exports.Ocean = function Ocean(mw, incomingIo) {
    io = incomingIo;

    this.time = new Date();
    this.id = this.time.getTime();
    this.status = 'setup';
    this.fishers = [];
    this.season = 0;
    this.seconds = 0;
    this.warnSeconds = 3;
    this.secondsSinceAllReturned = 0;
    this.certainFish = 0;
    this.mysteryFish = 0;
    this.microworld = mw;
    this.results = [];
    this.log = new OceanLog(this.id);

    for (var botIdx = 0; botIdx < mw.params.bots.length; botIdx++) {
        var bot = mw.params.bots[botIdx];
        this.fishers.push(new Fisher(bot.name, 'bot', bot, this));
        this.log.debug('Bot fisher ' + bot.name + ' joined.');
    }

    /////////////////////
    // Membership methods
    /////////////////////

    this.hasRoom = function () {
        return (this.fishers.length < this.microworld.params.numFishers);
    };

    this.allHumansIn = function () {
        return (this.fishers.length === this.microworld.params.numFishers);
    };

    this.addFisher = function (pId) {
        this.fishers.push(new Fisher(pId, 'human', null, this));
        this.log.info('Human fisher ' + pId + ' joined.');
        return;
    };

    this.removeFisher = function (pId) {
        for (var i in this.fishers) {
            var fisher = this.fishers[i];
            if (!fisher.isBot() && fisher.name === pId) {
                this.fishers.splice(i, 1);
            }
        }
        this.log.info('Human fisher ' + pId + ' left.');
    };

    this.findFisherIndex = function (pId) {
        for (var i in this.fishers) {
            if (this.fishers[i].name === pId) {
                return i;
            }
        }

        return null;
    };

    /////////////////
    // Status methods
    /////////////////

    this.getParams = function () {
        // TODO - need to send more than the initial params perhaps?
        return this.microworld.params;
    };

    this.isInSetup = function () {
        // At least one participant still needs to read instructions
        return (this.status === 'setup');
    };

    this.isEveryoneReady = function () {
        if (this.hasRoom()) return false;
        for (var i in this.fishers) {
            if (!this.fishers[i].ready) return false;
        }
        return true;
    };

    this.isInInitialDelay = function () {
        // Before first season
        return (this.status === 'initial delay');
    };

    this.isRunning = function () {
        return (this.status === 'running');
    };

    this.hasEveryoneReturned = function () {
        for (var i in this.fishers) {
            if (!this.fishers[i].hasReturned) {
                return false;
            }
        }
        return true;
    };

    this.isResting = function () {
        return (this.status === 'resting'); // between seasons
    };

    this.isPaused = function () {
        return (this.status === 'paused');
    };

    this.isNotOver = function () {
        return (this.status !== 'over');
    };

    this.isRemovable = function () {
        return (this.status === 'over');
    };

    this.canEndEarly = function () {
        return (this.microworld.params.enableEarlyEnd);
    };

    this.shouldEndSeason = function () {
        return (this.hasReachedSeasonDuration() ||
            (this.canEndEarly() && this.secondsSinceAllReturned >= 3));
    };

    this.pause = function () {

    };

    this.resume = function () {

    };

    this.getSimStatus = function () {
        var status = {
            season: this.season,
            status: this.status,
            certainFish: this.certainFish,
            mysteryFish: this.mysteryFish,
            fishers: []
        }

        for (var i in this.fishers) {
            status.fishers.push({
                name: this.fishers[i].name,
                seasonData: this.fishers[i].seasonData,
                money: this.fishers[i].money,
                totalFishCaught: this.fishers[i].totalFishCaught,
                status: this.fishers[i].status
            });
        }

        return status;
    };

    //////////////////////////
    // Time management methods
    //////////////////////////

    this.resetTimer = function () {
        this.seconds = 0;
    };

    this.tick = function () {
        this.seconds += 1;
        this.log.debug('Tick. Seconds: ' + this.seconds);
    };

    this.timeStep = function () {

    };

    this.hasReachedInitialDelay = function () {
        return (this.seconds >= this.microworld.params.initialDelay);
    };

    this.hasReachedSeasonDelay = function () {
        return (this.seconds >= this.microworld.params.seasonDelay);
    };

    this.hasReachedSeasonDuration = function () {
        return (this.seconds >= this.microworld.params.seasonDuration);
    };

    //////////////////////
    // Preparation methods
    //////////////////////

    this.readRules = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].ready = true;
        this.log.info('Fisher ' + pId + ' is ready to start.');
        return;
    };

    this.attemptToFish = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].tryToFish();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.goToSea = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].goToSea();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.returnToPort = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].goToPort();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.getOceanReady = function () {
        this.status = 'initial delay';
        this.log.info('All fishers ready to start.');
        io.sockets.in(this.id).emit('initial delay');
    };

    this.startNextSeason = function () {
        this.season += 1;
        this.log.debug('Preparing to begin season ' + this.season + '.');

        this.resetTimer();
        this.status = 'running';
        this.secondsSinceAllReturned = 0;
        this.setAvailableFish();

        // Record starting data
        this.results.push({
            season: this.season,
            fishStart: this.certainFish + this.mysteryFish,
            fishers: []
        });

        for (var i in this.fishers) {
            this.fishers[i].prepareFisherForSeason(this.season);

            this.results[this.season - 1].fishers.push({
                name: this.fishers[i].name,
                type: this.fishers[i].type,
            });
        }

        // TODO: Need to get proper numbers for certain and mystery fish on seasons after first!
        this.log.info('Beginning season ' + this.season + '.');
        io.sockets.in(this.id).emit('begin season', this.getSimStatus());
    };

    this.endCurrentSeason = function () {
        // Bring all fishers back to port
        for (var i in this.fishers) {
            this.fishers[i].goToPort();
        }

        // Record end data
        var seasonResults = this.results[this.season - 1];
        var preRunFish = this.microworld.params.certainFish + this.microworld.params.availableMysteryFish;
        var spawnFactor = this.microworld.params.spawnFactor;
        seasonResults.fishEnd = this.certainFish + this.mysteryFish;
        seasonResults.groupRestraint = this.groupRestraint(seasonResults);
        seasonResults.groupEfficiency = this.groupEfficiency(seasonResults, preRunFish, spawnFactor);

        for (i in this.fishers) {
            var fisherData = this.fishers[i].seasonData[this.season];
            var fisherResults = seasonResults.fishers[i];
            fisherResults.fishTaken = fisherData.fishCaught;
            fisherResults.profit = fisherData.endMoney - fisherData.startMoney;
            fisherResults.individualRestraint =
                this.individualRestraint(seasonResults, i);
            fisherResults.individualEfficiency =
                this.individualEfficiency(seasonResults, i, preRunFish, spawnFactor);
        }

        if (this.season < this.microworld.params.numSeasons) {
            this.status = 'resting';
            this.resetTimer();
            this.log.info('Ending season ' + this.season + '.');
            io.sockets.in(this.id).emit('end season', {
                season: this.season
            });
        } else {
            this.endOcean('time');
        }
    };

    this.runOcean = function () {
        // States: setup, initial delay, running, resting, paused, over
        var loop = true;
        var delay;
        if (this.isInSetup()) {
            if (!this.allHumansIn()) {
                this.log.debug('Ocean loop - setup: waiting for humans.');
            } else if (!this.isEveryoneReady()) {
                this.log.debug('Ocean loop - setup: reading instructions.')
            } else {
                // Everyone ready!
                this.getOceanReady();
            }
        } else if (this.isInInitialDelay()) {
            delay = this.microworld.params.initialDelay;
            this.log.debug('Ocean loop - initial delay: ' + this.seconds +
                ' of ' + delay + ' seconds.');

            if (this.seconds + this.warnSeconds >= delay) {
                io.sockets.in(this.id).emit('warn season start');
            }

            if (delay <= this.seconds) {
                this.log.debug('Ocean loop - initial delay: triggering season start.');
                this.startNextSeason();
            } else {
                this.tick();
            }
        } else if (this.isRunning()) {
            var duration = this.microworld.params.seasonDuration;
            this.log.debug('Ocean loop: running: ' + this.seconds +
                ' of ' + duration + ' seconds.');

            for (var i in this.fishers) {
                this.fishers[i].runBot();
            }

            io.sockets.in(this.id).emit('status', this.getSimStatus());

            if (this.seconds + this.warnSeconds >= duration) {
                io.sockets.in(this.id).emit('warn season end');
            }

            if (duration <= this.seconds) {
                this.log.debug('Ocean loop - running: triggering season end.');
                this.endCurrentSeason();
            } else {
                this.tick();
            }
        } else if (this.isResting()) {
            delay = this.microworld.params.seasonDelay;
            this.log.debug('Ocean loop - resting: ' + this.seconds +
                ' of ' + delay + ' seconds.');
            io.sockets.in(this.id).emit('status', this.getSimStatus());

            if (this.seconds + this.warnSeconds >= delay) {
                io.sockets.in(this.id).emit('warn season start');
            }

            if (delay <= this.seconds) {
                this.log.debug('Ocean loop - resting: triggering season start.');
                this.startNextSeason();
            } else {
                this.tick();
            }
        } else if (this.isPaused()) {
            this.log.debug('Ocean loop - paused.');
        } else { // over
            this.log.debug('Ocean loop - over: Stopping.');
            loop = false;
        }

        if (loop) {
            setTimeout(this.runOcean.bind(this), 1000);
        }
    };

    this.setAvailableFish = function () {
        if (this.season === 1) {
            this.certainFish = this.microworld.params.certainFish;
            this.mysteryFish = this.microworld.params.availableMysteryFish;
        } else {
            var spawnFactor = this.microworld.params.spawnFactor;
            var spawnedFish = this.certainFish * spawnFactor;
            var maxFish = this.microworld.params.maxFish;
            this.certainFish = Math.round(Math.min(spawnedFish, maxFish));

            var spawnedMystery = this.mysteryFish * spawnFactor;
            var maxMystery = this.microworld.params.availableMysteryFish;
            this.mysteryFish = Math.round(Math.min(spawnedMystery, maxMystery));
        }
    };

    this.areThereFish = function () {
        return (this.certainFish + this.mysteryFish) > 0;
    };

    this.endOcean = function (reason) {
        this.status = 'over';
        io.sockets.in(this.id).emit('end run', reason);

        if (this.microworld.status !== 'active') {
            this.log.info('Simulation run not saved: in ' +
                this.microworld.status + ' status');
            return;
        }

        var run = {
            time: this.time,
            results: this.results,
            log: this.log.entries,
            microworld: this.microworld
        };
        var _this = this;

        Run.create(run, function onCreate(err, doc) {
            if (err) {
                _this.log.error('Simulation run could not be saved: ' + err);
            }

            if (doc) {
                _this.log.info('Simulation run saved with _id ' + doc._id);
                console.log(_this.microworld._id);
                Microworld.update({_id: _this.microworld._id},
                    {$inc: {numCompleted: 1}}, function (err, num) {
                        if (err) console.log(err);
                        if (num !== 1) console.log(num);
                    });
            }
        });
    };

    this.isSuccessfulCastAttempt = function () {
        return ((this.certainFish + this.mysteryFish > 0) &&
            Math.random() <= this.microworld.params.chanceCatch);
    };

    this.takeOneFish = function () {
        if (Math.floor(Math.random() *
                (this.certainFish + this.mysteryFish)) < this.certainFish) {
            this.certainFish -= 1;
        } else {
            this.mysteryFish -= 1;
        }

        if (!this.areThereFish()) this.endOcean('depletion');
    };

    // Metric calculations
    this.individualRestraint = function (seasonData, fisherIndex) {
        var fishStart = seasonData.fishStart;
        var numFishers = seasonData.fishers.length;
        var fishTaken = seasonData.fishers[fisherIndex].fishTaken;

        if (fishStart === 0) return undefined;
        return (fishStart - numFishers * fishTaken) / fishStart;
    };

    this.groupRestraint = function (seasonData) {
        if (seasonData.fishStart === 0) return undefined;
        return seasonData.fishEnd / seasonData.fishStart;
    };

    this.individualEfficiency = function (seasonData, fisherIndex, preRunFish, spawnFactor) {
        var fishStart = seasonData.fishStart;
        var numFishers = seasonData.fishers.length;
        var fishTaken = seasonData.fishers[fisherIndex].fishTaken;

        if (preRunFish <= spawnFactor * fishStart) {
            // Not endangered
            return (fishStart - fishTaken * numFishers) * spawnFactor / preRunFish;
        } else {
            // Endangered
            return (fishStart - fishTaken * numFishers) / fishStart;
        }
    };

    this.groupEfficiency = function (seasonData, preRunFish, spawnFactor) {
        var fishStart = seasonData.fishStart;
        var fishEnd = seasonData.fishEnd;

        if (preRunFish <= spawnFactor * fishStart) {
            // Not endangered
            return fishEnd * spawnFactor / preRunFish;
        } else {
            // Endangered
            return fishEnd / fishStart;
        }
    };
};
<commit_msg>Tweak logs<commit_after>'use strict';

var Fisher = require('./fisher').Fisher;
var Microworld = require('../models/microworld-model').Microworld;
var OceanLog = require('./ocean-log').OceanLog;
var Run = require('../models/run-model').Run;

var io;

exports.Ocean = function Ocean(mw, incomingIo) {
    io = incomingIo;

    this.time = new Date();
    this.id = this.time.getTime();
    this.status = 'setup';
    this.fishers = [];
    this.season = 0;
    this.seconds = 0;
    this.warnSeconds = 3;
    this.secondsSinceAllReturned = 0;
    this.certainFish = 0;
    this.mysteryFish = 0;
    this.microworld = mw;
    this.results = [];
    this.log = new OceanLog(this.microworld.name +
        '(' + this.microworld.experimenter.username + ')');

    for (var botIdx = 0; botIdx < mw.params.bots.length; botIdx++) {
        var bot = mw.params.bots[botIdx];
        this.fishers.push(new Fisher(bot.name, 'bot', bot, this));
        this.log.debug('Bot fisher ' + bot.name + ' joined.');
    }

    /////////////////////
    // Membership methods
    /////////////////////

    this.hasRoom = function () {
        return (this.fishers.length < this.microworld.params.numFishers);
    };

    this.allHumansIn = function () {
        return (this.fishers.length === this.microworld.params.numFishers);
    };

    this.addFisher = function (pId) {
        this.fishers.push(new Fisher(pId, 'human', null, this));
        this.log.info('Human fisher ' + pId + ' joined.');
        return;
    };

    this.removeFisher = function (pId) {
        for (var i in this.fishers) {
            var fisher = this.fishers[i];
            if (!fisher.isBot() && fisher.name === pId) {
                this.fishers.splice(i, 1);
            }
        }
        this.log.info('Human fisher ' + pId + ' left.');
    };

    this.findFisherIndex = function (pId) {
        for (var i in this.fishers) {
            if (this.fishers[i].name === pId) {
                return i;
            }
        }

        return null;
    };

    /////////////////
    // Status methods
    /////////////////

    this.getParams = function () {
        // TODO - need to send more than the initial params perhaps?
        return this.microworld.params;
    };

    this.isInSetup = function () {
        // At least one participant still needs to read instructions
        return (this.status === 'setup');
    };

    this.isEveryoneReady = function () {
        if (this.hasRoom()) return false;
        for (var i in this.fishers) {
            if (!this.fishers[i].ready) return false;
        }
        return true;
    };

    this.isInInitialDelay = function () {
        // Before first season
        return (this.status === 'initial delay');
    };

    this.isRunning = function () {
        return (this.status === 'running');
    };

    this.hasEveryoneReturned = function () {
        for (var i in this.fishers) {
            if (!this.fishers[i].hasReturned) {
                return false;
            }
        }
        return true;
    };

    this.isResting = function () {
        return (this.status === 'resting'); // between seasons
    };

    this.isPaused = function () {
        return (this.status === 'paused');
    };

    this.isNotOver = function () {
        return (this.status !== 'over');
    };

    this.isRemovable = function () {
        return (this.status === 'over');
    };

    this.canEndEarly = function () {
        return (this.microworld.params.enableEarlyEnd);
    };

    this.shouldEndSeason = function () {
        return (this.hasReachedSeasonDuration() ||
            (this.canEndEarly() && this.secondsSinceAllReturned >= 3));
    };

    this.pause = function () {

    };

    this.resume = function () {

    };

    this.getSimStatus = function () {
        var status = {
            season: this.season,
            status: this.status,
            certainFish: this.certainFish,
            mysteryFish: this.mysteryFish,
            fishers: []
        }

        for (var i in this.fishers) {
            status.fishers.push({
                name: this.fishers[i].name,
                seasonData: this.fishers[i].seasonData,
                money: this.fishers[i].money,
                totalFishCaught: this.fishers[i].totalFishCaught,
                status: this.fishers[i].status
            });
        }

        return status;
    };

    //////////////////////////
    // Time management methods
    //////////////////////////

    this.resetTimer = function () {
        this.seconds = 0;
    };

    this.tick = function () {
        this.seconds += 1;
        this.log.debug('Tick. Seconds: ' + this.seconds);
    };

    this.timeStep = function () {

    };

    this.hasReachedInitialDelay = function () {
        return (this.seconds >= this.microworld.params.initialDelay);
    };

    this.hasReachedSeasonDelay = function () {
        return (this.seconds >= this.microworld.params.seasonDelay);
    };

    this.hasReachedSeasonDuration = function () {
        return (this.seconds >= this.microworld.params.seasonDuration);
    };

    //////////////////////
    // Preparation methods
    //////////////////////

    this.readRules = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].ready = true;
        this.log.info('Fisher ' + pId + ' is ready to start.');
        return;
    };

    this.attemptToFish = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].tryToFish();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.goToSea = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].goToSea();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.returnToPort = function (pId) {
        var idx = this.findFisherIndex(pId);
        if (idx) this.fishers[idx].goToPort();
        io.sockets.in(this.id).emit('status', this.getSimStatus());
        return;
    };

    this.getOceanReady = function () {
        this.status = 'initial delay';
        this.log.info('All fishers ready to start.');
        io.sockets.in(this.id).emit('initial delay');
    };

    this.startNextSeason = function () {
        this.season += 1;
        this.log.debug('Preparing to begin season ' + this.season + '.');

        this.resetTimer();
        this.status = 'running';
        this.secondsSinceAllReturned = 0;
        this.setAvailableFish();

        // Record starting data
        this.results.push({
            season: this.season,
            fishStart: this.certainFish + this.mysteryFish,
            fishers: []
        });

        for (var i in this.fishers) {
            this.fishers[i].prepareFisherForSeason(this.season);

            this.results[this.season - 1].fishers.push({
                name: this.fishers[i].name,
                type: this.fishers[i].type,
            });
        }

        // TODO: Need to get proper numbers for certain and mystery fish on seasons after first!
        this.log.info('Beginning season ' + this.season + '.');
        io.sockets.in(this.id).emit('begin season', this.getSimStatus());
    };

    this.endCurrentSeason = function () {
        // Bring all fishers back to port
        for (var i in this.fishers) {
            this.fishers[i].goToPort();
        }

        // Record end data
        var seasonResults = this.results[this.season - 1];
        var preRunFish = this.microworld.params.certainFish + this.microworld.params.availableMysteryFish;
        var spawnFactor = this.microworld.params.spawnFactor;
        seasonResults.fishEnd = this.certainFish + this.mysteryFish;
        seasonResults.groupRestraint = this.groupRestraint(seasonResults);
        seasonResults.groupEfficiency = this.groupEfficiency(seasonResults, preRunFish, spawnFactor);

        for (i in this.fishers) {
            var fisherData = this.fishers[i].seasonData[this.season];
            var fisherResults = seasonResults.fishers[i];
            fisherResults.fishTaken = fisherData.fishCaught;
            fisherResults.profit = fisherData.endMoney - fisherData.startMoney;
            fisherResults.individualRestraint =
                this.individualRestraint(seasonResults, i);
            fisherResults.individualEfficiency =
                this.individualEfficiency(seasonResults, i, preRunFish, spawnFactor);
        }

        if (this.season < this.microworld.params.numSeasons) {
            this.status = 'resting';
            this.resetTimer();
            this.log.info('Ending season ' + this.season + '.');
            io.sockets.in(this.id).emit('end season', {
                season: this.season
            });
        } else {
            this.endOcean('time');
        }
    };

    this.runOcean = function () {
        // States: setup, initial delay, running, resting, paused, over
        var loop = true;
        var delay;
        if (this.isInSetup()) {
            if (!this.allHumansIn()) {
                this.log.debug('Ocean loop - setup: waiting for humans.');
            } else if (!this.isEveryoneReady()) {
                this.log.debug('Ocean loop - setup: reading instructions.')
            } else {
                // Everyone ready!
                this.getOceanReady();
            }
        } else if (this.isInInitialDelay()) {
            delay = this.microworld.params.initialDelay;
            this.log.debug('Ocean loop - initial delay: ' + this.seconds +
                ' of ' + delay + ' seconds.');

            if (this.seconds + this.warnSeconds >= delay) {
                io.sockets.in(this.id).emit('warn season start');
            }

            if (delay <= this.seconds) {
                this.log.debug('Ocean loop - initial delay: triggering season start.');
                this.startNextSeason();
            } else {
                this.tick();
            }
        } else if (this.isRunning()) {
            var duration = this.microworld.params.seasonDuration;
            this.log.debug('Ocean loop: running: ' + this.seconds +
                ' of ' + duration + ' seconds.');

            for (var i in this.fishers) {
                this.fishers[i].runBot();
            }

            io.sockets.in(this.id).emit('status', this.getSimStatus());

            if (this.seconds + this.warnSeconds >= duration) {
                io.sockets.in(this.id).emit('warn season end');
            }

            if (duration <= this.seconds) {
                this.log.debug('Ocean loop - running: triggering season end.');
                this.endCurrentSeason();
            } else {
                this.tick();
            }
        } else if (this.isResting()) {
            delay = this.microworld.params.seasonDelay;
            this.log.debug('Ocean loop - resting: ' + this.seconds +
                ' of ' + delay + ' seconds.');
            io.sockets.in(this.id).emit('status', this.getSimStatus());

            if (this.seconds + this.warnSeconds >= delay) {
                io.sockets.in(this.id).emit('warn season start');
            }

            if (delay <= this.seconds) {
                this.log.debug('Ocean loop - resting: triggering season start.');
                this.startNextSeason();
            } else {
                this.tick();
            }
        } else if (this.isPaused()) {
            this.log.debug('Ocean loop - paused.');
        } else { // over
            this.log.debug('Ocean loop - over: Stopping.');
            loop = false;
        }

        if (loop) {
            setTimeout(this.runOcean.bind(this), 1000);
        }
    };

    this.setAvailableFish = function () {
        if (this.season === 1) {
            this.certainFish = this.microworld.params.certainFish;
            this.mysteryFish = this.microworld.params.availableMysteryFish;
        } else {
            var spawnFactor = this.microworld.params.spawnFactor;
            var spawnedFish = this.certainFish * spawnFactor;
            var maxFish = this.microworld.params.maxFish;
            this.certainFish = Math.round(Math.min(spawnedFish, maxFish));

            var spawnedMystery = this.mysteryFish * spawnFactor;
            var maxMystery = this.microworld.params.availableMysteryFish;
            this.mysteryFish = Math.round(Math.min(spawnedMystery, maxMystery));
        }
    };

    this.areThereFish = function () {
        return (this.certainFish + this.mysteryFish) > 0;
    };

    this.endOcean = function (reason) {
        this.status = 'over';
        io.sockets.in(this.id).emit('end run', reason);

        if (this.microworld.status !== 'active') {
            this.log.info('Simulation run not saved: in ' +
                this.microworld.status + ' status');
            return;
        }

        var run = {
            time: this.time,
            results: this.results,
            log: this.log.entries,
            microworld: this.microworld
        };
        var _this = this;

        Run.create(run, function onCreate(err, doc) {
            if (err) {
                _this.log.error('Simulation run could not be saved: ' + err);
            }

            if (doc) {
                _this.log.info('Simulation run saved with _id ' + doc._id);
                console.log(_this.microworld._id);
                Microworld.update({_id: _this.microworld._id},
                    {$inc: {numCompleted: 1}}, function (err, num) {
                        if (err) {
                            _this.log.error('Simulation count could not be incremented: ', err);
                        }
                    });
            }
        });
    };

    this.isSuccessfulCastAttempt = function () {
        return ((this.certainFish + this.mysteryFish > 0) &&
            Math.random() <= this.microworld.params.chanceCatch);
    };

    this.takeOneFish = function () {
        if (Math.floor(Math.random() *
                (this.certainFish + this.mysteryFish)) < this.certainFish) {
            this.certainFish -= 1;
        } else {
            this.mysteryFish -= 1;
        }

        if (!this.areThereFish()) this.endOcean('depletion');
    };

    // Metric calculations
    this.individualRestraint = function (seasonData, fisherIndex) {
        var fishStart = seasonData.fishStart;
        var numFishers = seasonData.fishers.length;
        var fishTaken = seasonData.fishers[fisherIndex].fishTaken;

        if (fishStart === 0) return undefined;
        return (fishStart - numFishers * fishTaken) / fishStart;
    };

    this.groupRestraint = function (seasonData) {
        if (seasonData.fishStart === 0) return undefined;
        return seasonData.fishEnd / seasonData.fishStart;
    };

    this.individualEfficiency = function (seasonData, fisherIndex, preRunFish, spawnFactor) {
        var fishStart = seasonData.fishStart;
        var numFishers = seasonData.fishers.length;
        var fishTaken = seasonData.fishers[fisherIndex].fishTaken;

        if (preRunFish <= spawnFactor * fishStart) {
            // Not endangered
            return (fishStart - fishTaken * numFishers) * spawnFactor / preRunFish;
        } else {
            // Endangered
            return (fishStart - fishTaken * numFishers) / fishStart;
        }
    };

    this.groupEfficiency = function (seasonData, preRunFish, spawnFactor) {
        var fishStart = seasonData.fishStart;
        var fishEnd = seasonData.fishEnd;

        if (preRunFish <= spawnFactor * fishStart) {
            // Not endangered
            return fishEnd * spawnFactor / preRunFish;
        } else {
            // Endangered
            return fishEnd / fishStart;
        }
    };
};
<|endoftext|>


<commit_before>/** @flow */
/**
 * Default implementation of cellRangeRenderer used by Grid.
 * This renderer supports cell-caching while the user is scrolling.
 */
export default function defaultCellRangeRenderer ({
  cellCache,
  cellRenderer,
  columnSizeAndPositionManager,
  columnStartIndex,
  columnStopIndex,
  horizontalOffsetAdjustment,
  isScrolling,
  rowSizeAndPositionManager,
  rowStartIndex,
  rowStopIndex,
  scrollLeft,
  scrollTop,
  verticalOffsetAdjustment,
  visibleColumnIndices,
  visibleRowIndices
}: DefaultCellRangeRendererParams) {
  const renderedCells = []

  for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
    let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex)

    for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
      let columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex)
      let isVisible = columnIndex >= visibleColumnIndices.start
        && columnIndex <= visibleColumnIndices.stop
        && rowIndex >= visibleRowIndices.start
        && rowIndex <= visibleRowIndices.stop
      let key = `${rowIndex}-${columnIndex}`
      let style = {
        height: rowDatum.size,
        left: columnDatum.offset + horizontalOffsetAdjustment,
        position: 'absolute',
        top: rowDatum.offset + verticalOffsetAdjustment,
        width: columnDatum.size
      }

      let cellRendererParams = {
        columnIndex,
        isScrolling,
        key,
        rowIndex,
        style,
        isVisible
      }

      let renderedCell

      // Avoid re-creating cells while scrolling.
      // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells.
      // If a scroll is in progress- cache and reuse cells.
      // This cache will be thrown away once scrolling completes.
      // However if we are scaling scroll positions and sizes, we should also avoid caching.
      // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
      // For more info refer to issue #395
      if (
        isScrolling &&
        !horizontalOffsetAdjustment &&
        !verticalOffsetAdjustment
      ) {
        if (!cellCache[key]) {
          cellCache[key] = cellRenderer(cellRendererParams)
        }
        renderedCell = cellCache[key]
      // If the user is no longer scrolling, don't cache cells.
      // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
      } else {
        renderedCell = cellRenderer(cellRendererParams)
      }

      if (renderedCell == null || renderedCell === false) {
        continue
      }

      renderedCells.push(renderedCell)
    }
  }

  return renderedCells
}

type DefaultCellRangeRendererParams = {
  cellCache: Object,
  cellRenderer: Function,
  columnSizeAndPositionManager: Object,
  columnStartIndex: number,
  columnStopIndex: number,
  horizontalOffsetAdjustment: number,
  isScrolling: boolean,
  rowSizeAndPositionManager: Object,
  rowStartIndex: number,
  rowStopIndex: number,
  scrollLeft: number,
  scrollTop: number,
  verticalOffsetAdjustment: number
};
<commit_msg>Fix lint<commit_after>/** @flow */
/**
 * Default implementation of cellRangeRenderer used by Grid.
 * This renderer supports cell-caching while the user is scrolling.
 */
export default function defaultCellRangeRenderer ({
  cellCache,
  cellRenderer,
  columnSizeAndPositionManager,
  columnStartIndex,
  columnStopIndex,
  horizontalOffsetAdjustment,
  isScrolling,
  rowSizeAndPositionManager,
  rowStartIndex,
  rowStopIndex,
  scrollLeft,
  scrollTop,
  verticalOffsetAdjustment,
  visibleColumnIndices,
  visibleRowIndices
}: DefaultCellRangeRendererParams) {
  const renderedCells = []

  for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
    let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex)

    for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
      let columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex)
      let isVisible = columnIndex >= visibleColumnIndices.start &&
        columnIndex <= visibleColumnIndices.stop &&
        rowIndex >= visibleRowIndices.start &&
        rowIndex <= visibleRowIndices.stop
      let key = `${rowIndex}-${columnIndex}`
      let style = {
        height: rowDatum.size,
        left: columnDatum.offset + horizontalOffsetAdjustment,
        position: 'absolute',
        top: rowDatum.offset + verticalOffsetAdjustment,
        width: columnDatum.size
      }

      let cellRendererParams = {
        columnIndex,
        isScrolling,
        key,
        rowIndex,
        style,
        isVisible
      }

      let renderedCell

      // Avoid re-creating cells while scrolling.
      // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells.
      // If a scroll is in progress- cache and reuse cells.
      // This cache will be thrown away once scrolling completes.
      // However if we are scaling scroll positions and sizes, we should also avoid caching.
      // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
      // For more info refer to issue #395
      if (
        isScrolling &&
        !horizontalOffsetAdjustment &&
        !verticalOffsetAdjustment
      ) {
        if (!cellCache[key]) {
          cellCache[key] = cellRenderer(cellRendererParams)
        }
        renderedCell = cellCache[key]
      // If the user is no longer scrolling, don't cache cells.
      // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
      } else {
        renderedCell = cellRenderer(cellRendererParams)
      }

      if (renderedCell == null || renderedCell === false) {
        continue
      }

      renderedCells.push(renderedCell)
    }
  }

  return renderedCells
}

type DefaultCellRangeRendererParams = {
  cellCache: Object,
  cellRenderer: Function,
  columnSizeAndPositionManager: Object,
  columnStartIndex: number,
  columnStopIndex: number,
  horizontalOffsetAdjustment: number,
  isScrolling: boolean,
  rowSizeAndPositionManager: Object,
  rowStartIndex: number,
  rowStopIndex: number,
  scrollLeft: number,
  scrollTop: number,
  verticalOffsetAdjustment: number,
  visibleColumnIndices: object,
  visibleRowIndices: object
};
<|endoftext|>


<commit_before>/** @flow */
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import shallowCompare from 'react-addons-shallow-compare'
import raf from 'raf'

export default class WindowScroller extends Component {
  static propTypes = {
    /**
     * Function respondible for rendering children.
     * This function should implement the following signature:
     *
     */
    children: PropTypes.func.isRequired

  }

  constructor (props) {
    super(props)
    this.state = {
      scrollTop: 0,
      height: 0
    }

    this._onScroll = this._onScroll.bind(this)
  }

  componentDidMount () {
    this._positionFromTop = ReactDOM.findDOMNode(this).getBoundingClientRect().top
    this.setState({ height: window.innerHeight })
    window.addEventListener('scroll', this._onScroll.bind(this))
  }

  componentWillUnmount () {
    window.removeEventListener('scroll', this._onScroll)
  }

  /**
   * Updates the state during the next animation frame.
   * Use this method to avoid multiple renders in a small span of time.
   * This helps performance for bursty events (like onScroll).
   */
  _setNextState (state) {
    if (this._setNextStateAnimationFrameId) {
      raf.cancel(this._setNextStateAnimationFrameId)
    }

    this._setNextStateAnimationFrameId = raf(() => {
      this._setNextStateAnimationFrameId = null
      this.setState(state)
    })
  }

  render () {
    const { children } = this.props
    const { scrollTop, height } = this.state

    return (

        {children({
          height,
          scrollTop
        })}

    )
  }

  shouldComponentUpdate (nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState)
  }

  _onScroll (event) {
    this.setState({ scrollTop: Math.max(0, window.scrollY - Math.max(0, this._positionFromTop)) })
  }
}
<commit_msg>use raf to improve performance onscroll<commit_after>/** @flow */
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import shallowCompare from 'react-addons-shallow-compare'
import raf from 'raf'

export default class WindowScroller extends Component {
  static propTypes = {
    /**
     * Function respondible for rendering children.
     * This function should implement the following signature:
     *
     */
    children: PropTypes.func.isRequired

  }

  constructor (props) {
    super(props)
    this.state = {
      scrollTop: 0,
      height: 0
    }

    this._onScroll = this._onScroll.bind(this)
  }

  componentDidMount () {
    this._positionFromTop = ReactDOM.findDOMNode(this).getBoundingClientRect().top
    this.setState({ height: window.innerHeight })
    window.addEventListener('scroll', this._onScroll.bind(this))
  }

  componentWillUnmount () {
    window.removeEventListener('scroll', this._onScroll)
  }

  /**
   * Updates the state during the next animation frame.
   * Use this method to avoid multiple renders in a small span of time.
   * This helps performance for bursty events (like onScroll).
   */
  _setNextState (state) {
    if (this._setNextStateAnimationFrameId) {
      raf.cancel(this._setNextStateAnimationFrameId)
    }

    this._setNextStateAnimationFrameId = raf(() => {
      this._setNextStateAnimationFrameId = null
      this.setState(state)
    })
  }

  render () {
    const { children } = this.props
    const { scrollTop, height } = this.state

    return (

        {children({
          height,
          scrollTop
        })}

    )
  }

  shouldComponentUpdate (nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState)
  }

  _onScroll (event) {
    this._setNextState({
      scrollTop: Math.max(0, window.scrollY - Math.max(0, this._positionFromTop))
    })
  }
}
<|endoftext|>


<commit_before>import React, {Component, PropTypes} from 'react'
import Button from 'components/Button'
import Textarea from 'react-textarea-autosize'
import {stringIsEmpty} from 'helpers/stringHelpers'

export default class EditTextArea extends Component {
  static propTypes = {
    text: PropTypes.string,
    autoFocus: PropTypes.bool,
    placeholder: PropTypes.string,
    onCancel: PropTypes.func,
    onEditDone: PropTypes.func,
    rows: PropTypes.number
  }

  constructor(props) {
    super()
    this.state = {
      editedText: props.text
    }
    this.onSubmit = this.onSubmit.bind(this)
    this.onChange = this.onChange.bind(this)
  }

  render() {
    const {editedText} = this.state
    const {autoFocus = false, placeholder = 'Enter text', rows = 4} = this.props
    return (

        <Textarea
          placeholder={placeholder}
          autoFocus={autoFocus}
          className="form-control"
          style={{
            marginTop: '1em'
          }}
          rows={rows}
          value={editedText}
          onChange={this.onChange}
        />

          <Button
            className="btn btn-default btn-sm"
            onClick={this.onSubmit}
            disabled={stringIsEmpty(editedText)}
          >
            Done
          </Button>
          <Button
            className="btn btn-default btn-sm"
            style={{
              marginLeft: '0.5em'
            }}
            onClick={() => this.props.onCancel()}
          >
            Cancel
          </Button>

    )
  }

  onChange(event) {
    this.setState({editedText: event.target.value})
  }

  onSubmit() {
    const {editedText} = this.state
    this.props.onEditDone(editedText)
  }
}
<commit_msg>emojis supported while editing texts<commit_after>import React, {Component, PropTypes} from 'react'
import Button from 'components/Button'
import Textarea from 'react-textarea-autosize'
import {stringIsEmpty, addEmoji} from 'helpers/stringHelpers'

export default class EditTextArea extends Component {
  static propTypes = {
    text: PropTypes.string,
    autoFocus: PropTypes.bool,
    placeholder: PropTypes.string,
    onCancel: PropTypes.func,
    onEditDone: PropTypes.func,
    rows: PropTypes.number
  }

  constructor(props) {
    super()
    this.state = {
      editedText: props.text
    }
    this.onSubmit = this.onSubmit.bind(this)
    this.onChange = this.onChange.bind(this)
    this.handleKeyPress = this.handleKeyPress.bind(this)
  }

  render() {
    const {editedText} = this.state
    const {autoFocus = false, placeholder = 'Enter text', rows = 4} = this.props
    return (

        <Textarea
          placeholder={placeholder}
          autoFocus={autoFocus}
          className="form-control"
          style={{
            marginTop: '1em'
          }}
          rows={rows}
          value={editedText}
          onChange={this.onChange}
          onKeyPress={this.handleKeyPress}
        />

          <Button
            className="btn btn-default btn-sm"
            onClick={this.onSubmit}
            disabled={stringIsEmpty(editedText)}
          >
            Done
          </Button>
          <Button
            className="btn btn-default btn-sm"
            style={{
              marginLeft: '0.5em'
            }}
            onClick={() => this.props.onCancel()}
          >
            Cancel
          </Button>

    )
  }

  onChange(event) {
    this.setState({editedText: event.target.value})
  }

  handleKeyPress(event) {
    if (event.key === ' ') this.setState({editedText: addEmoji(event.target.value)})
  }

  onSubmit() {
    const {editedText} = this.state
    this.props.onEditDone(addEmoji(editedText))
  }
}
<|endoftext|>


<commit_before>/*
	Project View
	--------------
	Renders View of projects (initializes galleries)
*/
var	ScrollController = require('../controllers/scroll-controller');

var ProjectView = function () {

	var _galleries = new Array();

	var init = function () {
		//init Scroll controller
		ScrollController.init("related-posts");

		//init Pagination Menu
		_paginationMenu();

		//init the galleries
		var galleries = document.getElementsByClassName("gallery");

		for (var i = 0; i < galleries.length; i++) {
			_initGallery(galleries[i]);
		}

		//add functionality to related posts
		var p = document.getElementsByClassName("project");

		for (var i = 0; i < p.length; i++) {
			p[i].addEventListener("click", function(e){
				e.stopPropagation();
				emitter.emit("requestNewPage", e.target.parentNode.getAttribute("data-href"));
			});
		}
	};

	var _initGallery = function(gallery){
		var gallery = new Gallery(gallery);
		gallery.init();
	};

	function Gallery(gallery){

		this.photos = gallery.getElementsByClassName("gallery-item");
		this.currentPhoto = this.photos.length-1;;
		this.DOMObject = gallery;

		this.init = function(){
			_galleries.push(this);
			var thisGallery = this;
			this._nIntervId = setInterval(function(){
				thisGallery._fadePhoto();
			}, 3000);
		};

		this._fadePhoto = function(){
			if (this.currentPhoto!=0){
				this.photos[this.currentPhoto].className = this.photos[this.currentPhoto].className + " fade-out";
			} else {
				//STEF MEJORAR: se puede mejorar haciendo fade-in primero de la primera foto y luego del resto
				for (var i = 0; i < this.photos.length; i++) {
					this.photos[i].className = this.photos[i].className.replace(" fade-out", "");
				};
			}
			this.currentPhoto == 0 ? this.currentPhoto = this.photos.length-1 : this.currentPhoto--;
		}
	}

	var clearTheInterval = function(){
		for (var i = 0; i < _galleries.length; i++) {
			clearInterval(_galleries[i]._nIntervId);
		}
	}

	//STEF: move to functions
	var _findAncestror = function(el, cls){
		while ((el = el.parentElement) && !el.classList.contains(cls));
		return el;
	}

	var _paginationMenu = function(){

		var target;

		document.getElementById("square-previous").addEventListener("click", function(e){
			clearTheInterval();
			target = _findAncestror(e.target, "clickable-square");
			emitter.emit("requestNewPage", target.getAttribute("data-href"));
		}, false);

		document.getElementById("square-next").addEventListener("click", function(e){
			clearTheInterval();
			target = _findAncestror(e.target, "clickable-square");
			emitter.emit("requestNewPage", target.getAttribute("data-href"));
		}, false);

		document.getElementById("scroll-up").addEventListener("click", function(e){
			//STEF move to functions
			(function smoothscroll(){
			    var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
			    if (currentScroll > 0) {
			         window.requestAnimationFrame(smoothscroll);
			         window.scrollTo (0,currentScroll - (currentScroll/25));
			    }
			})();
		}, false);

	}

	return {
		init: init,
		clearTheInterval : clearTheInterval
	};
};

module.exports = new ProjectView();<commit_msg>Fix navigation menu<commit_after>/*
	Project View
	--------------
	Renders View of projects (initializes galleries)
*/
var	ScrollController = require('../controllers/scroll-controller');

var ProjectView = function () {

	var _galleries = new Array();

	var init = function () {
		//init Scroll controller
		ScrollController.init("related-posts");

		//init Pagination Menu
		_paginationMenu();

		//init the galleries
		var galleries = document.getElementsByClassName("gallery");

		for (var i = 0; i < galleries.length; i++) {
			_initGallery(galleries[i]);
		}

		//add functionality to related posts
		var p = document.getElementsByClassName("project");

		for (var i = 0; i < p.length; i++) {
			p[i].addEventListener("click", function(e){
				e.stopPropagation();
				emitter.emit("requestNewPage", e.target.parentNode.getAttribute("data-href"));
			});
		}
	};

	var _initGallery = function(gallery){
		var gallery = new Gallery(gallery);
		gallery.init();
	};

	function Gallery(gallery){

		this.photos = gallery.getElementsByClassName("gallery-item");
		this.currentPhoto = this.photos.length-1;;
		this.DOMObject = gallery;

		this.init = function(){
			_galleries.push(this);
			var thisGallery = this;
			this._nIntervId = setInterval(function(){
				thisGallery._fadePhoto();
			}, 3000);
		};

		this._fadePhoto = function(){
			if (this.currentPhoto!=0){
				this.photos[this.currentPhoto].className = this.photos[this.currentPhoto].className + " fade-out";
			} else {
				//STEF MEJORAR: se puede mejorar haciendo fade-in primero de la primera foto y luego del resto
				for (var i = 0; i < this.photos.length; i++) {
					this.photos[i].className = this.photos[i].className.replace(" fade-out", "");
				};
			}
			this.currentPhoto == 0 ? this.currentPhoto = this.photos.length-1 : this.currentPhoto--;
		}
	}

	var clearTheInterval = function(){
		for (var i = 0; i < _galleries.length; i++) {
			clearInterval(_galleries[i]._nIntervId);
		}
	}

	//STEF: move to functions
	var _findAncestror = function(el, cls){
		while ((el = el.parentElement) && !el.classList.contains(cls));
		return el;
	}

	var _paginationMenu = function(){

		var target;

		if (document.contains(document.getElementById("square-previous"))) {

			document.getElementById("square-previous").addEventListener("click", function(e){
				clearTheInterval();
				target = _findAncestror(e.target, "clickable-square");
				emitter.emit("requestNewPage", target.getAttribute("data-href"));
			}, false);

		}

		if (document.contains(document.getElementById("square-next"))) {

			document.getElementById("square-next").addEventListener("click", function(e){
				clearTheInterval();
				target = _findAncestror(e.target, "clickable-square");
				emitter.emit("requestNewPage", target.getAttribute("data-href"));
			}, false);

		}

		document.getElementById("scroll-up").addEventListener("click", function(e){
			//STEF move to functions
			(function smoothscroll(){
			    var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
			    if (currentScroll > 0) {
			         window.requestAnimationFrame(smoothscroll);
			         window.scrollTo (0,currentScroll - (currentScroll/25));
			    }
			})();
		}, false);

	}

	return {
		init: init,
		clearTheInterval : clearTheInterval
	};
};

module.exports = new ProjectView();<|endoftext|>


<commit_before>(function () {
  var salaryContainer = '.salary-container';
  var compensationAmount = salaryContainer + ' .compensation .amount';
  var defaultValue = '--';

  var CONTRACT_TYPE_EMPLOYEE = 'employee_factor';
  var CONTRACT_TYPE_CONTRACTOR = 'contractor_factor';

  // Dropdown Core functionality

  var setDropdown = function(event) {
    var $selected = $(event.currentTarget);
    var key = $selected.find('.key').text();
    var value = $selected.find('.value').text();
    var displayValue = $selected.find('.display-value').text();
    var $title = $selected.parents('.dropdown').find('.title');
    var $subtitle = $selected.parents('.dropdown').find('.subtitle');

    // Get range values if available
    var min = $selected.find('.value').data('min');
    var max = $selected.find('.value').data('max');

    $title.text(key);
    $title.data('selected', value || key);

    if (min && max) {
      $title.data('min', min);
      $title.data('max', max);
    }

    if (value) {
      $subtitle.text(value);
    } else if (displayValue) {
      $subtitle.text(displayValue);
    }
  }

  this.SalaryCalculator = (function() {
    function SalaryCalculator() {
      this.bindEvents();
    }

    SalaryCalculator.prototype.bindEvents = function() {
      var $countryDropdown = $(salaryContainer + ' .country li');
      var $cityDropdown = $(salaryContainer + ' .city li');
      var $levelDropdown = $(salaryContainer + ' .level li');
      var $experienceDropdown = $(salaryContainer + ' .experience li');

      // Set selected dropdown value
      $countryDropdown.on('click', setDropdown);
      $cityDropdown.on('click', setDropdown);
      $levelDropdown.on('click', setDropdown);
      $experienceDropdown.on('click', setDropdown);

      // Render compensation
      $countryDropdown.on('click', this.render.bind(this));
      $cityDropdown.on('click', this.render.bind(this));
      $levelDropdown.on('click', this.render.bind(this));
      $experienceDropdown.on('click', this.render.bind(this));

      // Unlock and filter city dropdown
      $countryDropdown.on('click', this.filterCityDropdown.bind(this));
      $countryDropdown.on('click', this.resetCityDropdown.bind(this));

      $countryDropdown.on('click', this.renderContractType.bind(this));

      // Render Formula
      $levelDropdown.on('click', this.renderFormula.bind(this));
      $experienceDropdown.on('click', this.renderFormula.bind(this));
    }

    // Custom dropdown functionality

    SalaryCalculator.prototype.filterCityDropdown = function() {
      var selectedCountry = this.getElementValues().country;
      var $cities = $(salaryContainer + ' .city li');

      $cities.each(function(index, element) {
        var $element = $(element);
        if ($element.data('country') === selectedCountry) {
          $element.removeClass('hidden');
        } else {
          $element.addClass('hidden');
        }
      });
    }

    SalaryCalculator.prototype.resetCityDropdown = function() {
      var $cityDropdownBtn = $(salaryContainer + ' .city .btn');
      var $cityDropdownTitle = $cityDropdownBtn.find('.title');
      $cityDropdownBtn.removeClass('disabled');
      $cityDropdownTitle.text('--');
      $cityDropdownTitle.data('selected', '');
      $cityDropdownBtn.find('.subtitle').text('');
      this.renderInvalidCompensation();
      this.renderFormula();
    }

    // Render functions

    SalaryCalculator.prototype.getData = function() {
      var deferred = jQuery.Deferred();

      $.get('/salary/data.json').then(function(data) {
        // Cache the data
        this.data = data;
        deferred.resolve();
      }.bind(this));

      return deferred.promise();
    }

    SalaryCalculator.prototype.render = function() {
      var input = this.getElementValues();

      function renderData() {
        if (input.country && input.city && input.level && input.experience.min && input.experience.max) {
          this.renderCompensation(input);
        } else {
          this.renderInvalidCompensation();
        }
        this.renderFormula();
      }

      if (this.data) {
        renderData.call(this);
      } else {
        $.when(this.getData()).then(function() {
            renderData.call(this);
          }.bind(this),
          this.renderInvalidCompensation.bind(this));
      }
    }

    SalaryCalculator.prototype.renderCompensation = function(input) {
      var numbeo = this.data.numbeo;
      var contracts = this.data.contractTypes;

      var levelIndex = parseFloat(input.level);
      var benchmark = input.salary;

      var rentIndex = this.calculateRentIndex(input.city, input.country);
      var contract = this.calculateContractFactor(input.country);

      if (!rentIndex) {
        //When the city and country combination selected do not exist
        return this.renderInvalidCompensation();
      }

      var min = this.calculateCompensation(benchmark, rentIndex, levelIndex, contract.factor, input.experience.min);
      var max = this.calculateCompensation(benchmark, rentIndex, levelIndex, contract.factor, input.experience.max);
      $(compensationAmount).text(this.formatAmount(min) + ' - ' + this.formatAmount(max) + ' USD');
    }

    SalaryCalculator.prototype.renderContractType = function() {
      var country = this.getElementValues().country;
      var contract = this.calculateContractFactor(country);
      var $container = $('.contract-type-container');

      if (contract.type === CONTRACT_TYPE_EMPLOYEE) {
        $container.find('.grammer').text('an');
        $container.find('.company-type').text('Inc.');
        $container.find('.contract-type').text('employee');
      } else {
        $container.find('.grammer').text('a');
        $container.find('.company-type').text('BV.');
        $container.find('.contract-type').text('contractor');
      }

      $container.removeClass('hidden');
    }

    SalaryCalculator.prototype.renderFormula = function() {
      var values = this.getElementValues();
      var rentIndex = this.calculateRentIndex(values.city, values.country);
      var contract = this.calculateContractFactor(values.country);
      var experience = values.experience;

      $('.formula .level .value').text(values.level ? values.level : defaultValue);
      $('.formula .experience .value').text(experience.min && experience.max ? experience.min + ' to ' + experience.max : defaultValue);

      $('.formula .rentIndex .value').text(rentIndex ? rentIndex : defaultValue);
      $('.formula .contractType .value').text(contract.hasOwnProperty('factor') ? contract.factor.toFixed(2) : defaultValue);
    }

    SalaryCalculator.prototype.renderInvalidCompensation = function() {
      $(compensationAmount).text(defaultValue);
    }

    // Pure functions

    SalaryCalculator.prototype.getElementValues = function() {
      return {
        salary: $(salaryContainer).data('salary') || '',
        country: $(salaryContainer + ' .country .title').data('selected') || '',
        city: $(salaryContainer + ' .city .title').data('selected') || '',
        level: $(salaryContainer + ' .level .title').data('selected') || '',
        experience: {
          min: $(salaryContainer + ' .experience .title').data('min') || '',
          max: $(salaryContainer + ' .experience .title').data('max') || ''
        }
      };
    }

    SalaryCalculator.prototype.calculateRentIndex = function(city, country) {
      if (this.data && this.data.numbeo) {
        var locationData = this.data.numbeo.find(function(o) {
          return o.country === country && o.city === city;
        });

        return locationData ? parseFloat((locationData.rentIndex * 0.01).toFixed(2)) : 0;
      }

      // Rent Index will never be zero, safe to return as error value
      return 0;
    }

    SalaryCalculator.prototype.calculateContractFactor = function(country) {
      var contract = {};

      if (this.data && this.data.contractTypes) {
        var defaultContractorFactor = this.data.contractTypes.find(function (o) {
          return o.country === '*';
        }).contractor_factor;

        var contractType = this.data.contractTypes.find(function(o) {
          return o.country === country
        });

        if (contractType && contractType.hasOwnProperty(CONTRACT_TYPE_EMPLOYEE)) {
          contract.type = CONTRACT_TYPE_EMPLOYEE;
          contract.factor = contractType[CONTRACT_TYPE_EMPLOYEE];
        } else if (contractType) {
          contract.type = CONTRACT_TYPE_CONTRACTOR;
          contract.factor = contractType[CONTRACT_TYPE_CONTRACTOR];
        } else {
          contract.type = CONTRACT_TYPE_CONTRACTOR;
          contract.factor = defaultContractorFactor;
        }
      }

      return contract;
    }

    SalaryCalculator.prototype.calculateCompensation = function(benchmark, rentIndex, levelIndex, contractType, experienceFactor) {
      return Math.round(benchmark * (rentIndex + 0.25) * levelIndex * contractType * experienceFactor);
    };

    SalaryCalculator.prototype.formatAmount = function(amount) {
      return '$' + amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    }

    return SalaryCalculator;
  })();

  new SalaryCalculator();
})();
<commit_msg>Fix calculator contract type first render<commit_after>(function () {
  var salaryContainer = '.salary-container';
  var compensationAmount = salaryContainer + ' .compensation .amount';
  var defaultValue = '--';

  var CONTRACT_TYPE_EMPLOYEE = 'employee_factor';
  var CONTRACT_TYPE_CONTRACTOR = 'contractor_factor';

  // Dropdown Core functionality

  var setDropdown = function(event) {
    var $selected = $(event.currentTarget);
    var key = $selected.find('.key').text();
    var value = $selected.find('.value').text();
    var displayValue = $selected.find('.display-value').text();
    var $title = $selected.parents('.dropdown').find('.title');
    var $subtitle = $selected.parents('.dropdown').find('.subtitle');

    // Get range values if available
    var min = $selected.find('.value').data('min');
    var max = $selected.find('.value').data('max');

    $title.text(key);
    $title.data('selected', value || key);

    if (min && max) {
      $title.data('min', min);
      $title.data('max', max);
    }

    if (value) {
      $subtitle.text(value);
    } else if (displayValue) {
      $subtitle.text(displayValue);
    }
  }

  this.SalaryCalculator = (function() {
    function SalaryCalculator() {
      this.bindEvents();
    }

    SalaryCalculator.prototype.bindEvents = function() {
      var $countryDropdown = $(salaryContainer + ' .country li');
      var $cityDropdown = $(salaryContainer + ' .city li');
      var $levelDropdown = $(salaryContainer + ' .level li');
      var $experienceDropdown = $(salaryContainer + ' .experience li');

      // Set selected dropdown value
      $countryDropdown.on('click', setDropdown);
      $cityDropdown.on('click', setDropdown);
      $levelDropdown.on('click', setDropdown);
      $experienceDropdown.on('click', setDropdown);

      // Render compensation
      $countryDropdown.on('click', this.render.bind(this));
      $cityDropdown.on('click', this.render.bind(this));
      $levelDropdown.on('click', this.render.bind(this));
      $experienceDropdown.on('click', this.render.bind(this));

      // Unlock and filter city dropdown
      $countryDropdown.on('click', this.filterCityDropdown.bind(this));
      $countryDropdown.on('click', this.resetCityDropdown.bind(this));

      // Render Formula
      $levelDropdown.on('click', this.renderFormula.bind(this));
      $experienceDropdown.on('click', this.renderFormula.bind(this));
    }

    // Custom dropdown functionality

    SalaryCalculator.prototype.filterCityDropdown = function() {
      var selectedCountry = this.getElementValues().country;
      var $cities = $(salaryContainer + ' .city li');

      $cities.each(function(index, element) {
        var $element = $(element);
        if ($element.data('country') === selectedCountry) {
          $element.removeClass('hidden');
        } else {
          $element.addClass('hidden');
        }
      });
    }

    SalaryCalculator.prototype.resetCityDropdown = function() {
      var $cityDropdownBtn = $(salaryContainer + ' .city .btn');
      var $cityDropdownTitle = $cityDropdownBtn.find('.title');
      $cityDropdownBtn.removeClass('disabled');
      $cityDropdownTitle.text('--');
      $cityDropdownTitle.data('selected', '');
      $cityDropdownBtn.find('.subtitle').text('');
      this.renderInvalidCompensation();
      this.renderFormula();
    }

    // Render functions

    SalaryCalculator.prototype.getData = function() {
      var deferred = jQuery.Deferred();

      $.get('/salary/data.json').then(function(data) {
        // Cache the data
        this.data = data;
        deferred.resolve();
      }.bind(this));

      return deferred.promise();
    }

    SalaryCalculator.prototype.render = function() {
      var input = this.getElementValues();

      function renderData() {
        if (input.country && input.city && input.level && input.experience.min && input.experience.max) {
          this.renderCompensation(input);
        } else {
          this.renderInvalidCompensation();
        }
        this.renderFormula();
        this.renderContractType();
      }

      if (this.data) {
        renderData.call(this);
      } else {
        $.when(this.getData()).then(function() {
            renderData.call(this);
          }.bind(this),
          this.renderInvalidCompensation.bind(this));
      }
    }

    SalaryCalculator.prototype.renderCompensation = function(input) {
      var numbeo = this.data.numbeo;
      var contracts = this.data.contractTypes;

      var levelIndex = parseFloat(input.level);
      var benchmark = input.salary;

      var rentIndex = this.calculateRentIndex(input.city, input.country);
      var contract = this.calculateContractFactor(input.country);

      if (!rentIndex) {
        //When the city and country combination selected do not exist
        return this.renderInvalidCompensation();
      }

      var min = this.calculateCompensation(benchmark, rentIndex, levelIndex, contract.factor, input.experience.min);
      var max = this.calculateCompensation(benchmark, rentIndex, levelIndex, contract.factor, input.experience.max);
      $(compensationAmount).text(this.formatAmount(min) + ' - ' + this.formatAmount(max) + ' USD');
    }

    SalaryCalculator.prototype.renderContractType = function() {
      var country = this.getElementValues().country;
      var contract = this.calculateContractFactor(country);
      var $container = $('.contract-type-container');

      if (contract.type === CONTRACT_TYPE_EMPLOYEE) {
        $container.find('.grammer').text('an');
        $container.find('.company-type').text('Inc.');
        $container.find('.contract-type').text('employee');
      } else {
        $container.find('.grammer').text('a');
        $container.find('.company-type').text('BV.');
        $container.find('.contract-type').text('contractor');
      }

      $container.removeClass('hidden');
    }

    SalaryCalculator.prototype.renderFormula = function() {
      var values = this.getElementValues();
      var rentIndex = this.calculateRentIndex(values.city, values.country);
      var contract = this.calculateContractFactor(values.country);
      var experience = values.experience;

      $('.formula .level .value').text(values.level ? values.level : defaultValue);
      $('.formula .experience .value').text(experience.min && experience.max ? experience.min + ' to ' + experience.max : defaultValue);

      $('.formula .rentIndex .value').text(rentIndex ? rentIndex : defaultValue);
      $('.formula .contractType .value').text(contract.hasOwnProperty('factor') ? contract.factor.toFixed(2) : defaultValue);
    }

    SalaryCalculator.prototype.renderInvalidCompensation = function() {
      $(compensationAmount).text(defaultValue);
    }

    // Pure functions

    SalaryCalculator.prototype.getElementValues = function() {
      return {
        salary: $(salaryContainer).data('salary') || '',
        country: $(salaryContainer + ' .country .title').data('selected') || '',
        city: $(salaryContainer + ' .city .title').data('selected') || '',
        level: $(salaryContainer + ' .level .title').data('selected') || '',
        experience: {
          min: $(salaryContainer + ' .experience .title').data('min') || '',
          max: $(salaryContainer + ' .experience .title').data('max') || ''
        }
      };
    }

    SalaryCalculator.prototype.calculateRentIndex = function(city, country) {
      if (this.data && this.data.numbeo) {
        var locationData = this.data.numbeo.find(function(o) {
          return o.country === country && o.city === city;
        });

        return locationData ? parseFloat((locationData.rentIndex * 0.01).toFixed(2)) : 0;
      }

      // Rent Index will never be zero, safe to return as error value
      return 0;
    }

    SalaryCalculator.prototype.calculateContractFactor = function(country) {
      var contract = {};

      if (this.data && this.data.contractTypes) {
        var defaultContractorFactor = this.data.contractTypes.find(function (o) {
          return o.country === '*';
        }).contractor_factor;

        var contractType = this.data.contractTypes.find(function(o) {
          return o.country === country
        });

        if (contractType && contractType.hasOwnProperty(CONTRACT_TYPE_EMPLOYEE)) {
          contract.type = CONTRACT_TYPE_EMPLOYEE;
          contract.factor = contractType[CONTRACT_TYPE_EMPLOYEE];
        } else if (contractType) {
          contract.type = CONTRACT_TYPE_CONTRACTOR;
          contract.factor = contractType[CONTRACT_TYPE_CONTRACTOR];
        } else {
          contract.type = CONTRACT_TYPE_CONTRACTOR;
          contract.factor = defaultContractorFactor;
        }
      }

      return contract;
    }

    SalaryCalculator.prototype.calculateCompensation = function(benchmark, rentIndex, levelIndex, contractType, experienceFactor) {
      return Math.round(benchmark * (rentIndex + 0.25) * levelIndex * contractType * experienceFactor);
    };

    SalaryCalculator.prototype.formatAmount = function(amount) {
      return '$' + amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    }

    return SalaryCalculator;
  })();

  new SalaryCalculator();
})();
<|endoftext|>


<commit_before>/*
 * Copyright 2014 François de Campredon
 *
 * 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.
 */

/*jslint browser:true, devel:true, unused:true*/
/*global define, brackets, $ */

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

    var EditorManager    = brackets.getModule('editor/EditorManager'),
        _                 = brackets.getModule('thirdparty/lodash');

    var errorToolTipHTML  = require('text!errortoolip.html'),
        errorsTick        = require('./errorsTick'),
        divContainsMouse = require('./utils').divContainsMouse;

    var $errorToolTipContainer,    // error tooltip container
        $errorToolTipContent;      // errot tooltip content holder

    var TOOLTIP_BOUNDS_OFFSET          = 8;    // offset between tooltip and position of the cursor / or bounds of the editor

    /**
     * and error maps containing information about the last linting session
     */
    var errorsMap;

    function setErrorsMap(_errorsMap) {
        errorsMap = _errorsMap;
        hideErrorToolTip();
    }

    // Tooltip widget management ----------------------------------------------

    /**
     * hide the error tooltip
     */
    function hideErrorToolTip() {
        $errorToolTipContainer.hide();
        $errorToolTipContent.html('');
    }

    /**
     * if the errorsMap contains errors for the given position returns that error
     * else returns null
     */
    function getLineErrorForPos(pos) {
        var lineErrors;
        if (errorsMap && (lineErrors = errorsMap[pos.line])) {
            for (var i =0, l = lineErrors.length; i < l; i++) {
                var error = lineErrors[i];
                if (pos.ch >= error.pos.ch && pos.ch <= error.endpos.ch) {
                    return error;
                }
            }
        }
        return null;
    }

    /**
     * position the tooltip below the marked error, centered
     * but always in the bound of the editor
     */
    function positionToolTip(xpos, ypos, ybot) {
        $errorToolTipContainer.offset({
            left: 0,
            top: 0
        });
        $errorToolTipContainer.css('visibility', 'hidden');

        window.requestAnimationFrame(function () {
            var toolTipWidth  = $errorToolTipContainer.width(),
                toolTipHeight = $errorToolTipContainer.height(),
                top           = ybot + TOOLTIP_BOUNDS_OFFSET,
                left          = xpos - (toolTipWidth / 2 ),
                $editorHolder = $('#editor-holder'),
                editorOffset = $editorHolder.offset();

            left = Math.max(left, editorOffset.left + TOOLTIP_BOUNDS_OFFSET);
            left = Math.min(left, editorOffset.left + $editorHolder.width() - toolTipWidth - TOOLTIP_BOUNDS_OFFSET - 10);

            if (top < (editorOffset.top + $editorHolder.height() - toolTipHeight - TOOLTIP_BOUNDS_OFFSET)) {
                $errorToolTipContainer.removeClass('preview-bubble-above');
                $errorToolTipContainer.addClass('preview-bubble-below');
                $errorToolTipContainer.offset({
                    left: left,
                    top: top
                });
            } else {
                $errorToolTipContainer.removeClass('preview-bubble-below');
                $errorToolTipContainer.addClass('preview-bubble-above');
                top = ypos - TOOLTIP_BOUNDS_OFFSET - toolTipHeight;
                $errorToolTipContainer.offset({
                    left: left,
                    top: top
                });
            }

            $errorToolTipContainer.css('visibility', 'visible');
        });
    }

    /**
     * last position handled
     */
    var lastPos;
    function handleMouseMove(event) {
        if (event.which) {
            // Button is down - don't show popovers while dragging
            hideErrorToolTip();
            return;
        }

        var editor = EditorManager.getCurrentFullEditor();

        if (!editor || !divContainsMouse($(editor.getRootElement()), event, 10, 10)) {
            hideErrorToolTip();
            return;
        }
        // Find char mouse is over
        var cm = editor._codeMirror,
            pos = cm.coordsChar({left: event.clientX, top: event.clientY});

        var lineErrors, coord;
        // No preview if mouse is past last char on line
        if (pos.ch >= editor.document.getLine(pos.line).length) {
            lineErrors = errorsTick.getErrorsForMouse(event);
            if (!lineErrors.length) {
                hideErrorToolTip();
                return;
            }
            coord = {
                left: event.clientX,
                top: event.clientY,
                bottom: event.clientY
            };
        } else {
            // Bail if mouse is on same char as last event
            if (lastPos && lastPos.line === pos.line && lastPos.ch === pos.ch) {
                return;
            }
            lastPos = pos;
            var lineError = getLineErrorForPos(pos);
            if (!lineError) {
                hideErrorToolTip();
                return;
            }
            lineErrors = [lineError];
            coord = cm.charCoords(pos);
        }

        $errorToolTipContent.html(
            _.flatten(lineErrors.map(function (lineError) {
                return lineError.errors.map(function (error) {
                    return error.message;
                });
            })).join('
')
        );

        $errorToolTipContainer.show();
        positionToolTip(coord.left, coord.top, coord.bottom);
    }

    function handleMouseOut(event) {
        var $editorHolder = $('#editor-holder');
        if (!divContainsMouse($editorHolder, event, 10, 10)) {
            hideErrorToolTip();
        }
    }

    // Bootstraping ----------------------------------------------
    function init() {
        var editorHolder = $('#editor-holder')[0];
        // Note: listening to 'scroll' also catches text edits, which bubble a scroll event up from the hidden text area. This means
        // we auto-hide on text edit, which is probably actually a good thing.
        editorHolder.addEventListener('mousemove', handleMouseMove, true);
        editorHolder.addEventListener('scroll', hideErrorToolTip, true);
        editorHolder.addEventListener('mouseout', handleMouseOut, true);

        $errorToolTipContainer = $(errorToolTipHTML).appendTo($('body'));
        $errorToolTipContent = $errorToolTipContainer.find('.error-tooltip-content');

    }

    return {
        init: init,
        setErrorsMap: setErrorsMap
    };

});<commit_msg>fixes #4<commit_after>/*
 * Copyright 2014 François de Campredon
 *
 * 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.
 */

/*jslint browser:true, devel:true, unused:true*/
/*global define, brackets, $ */

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

    var EditorManager    = brackets.getModule('editor/EditorManager'),
        _                 = brackets.getModule('thirdparty/lodash');

    var errorToolTipHTML  = require('text!errortoolip.html'),
        errorsTick        = require('./errorsTick'),
        divContainsMouse = require('./utils').divContainsMouse;

    var $errorToolTipContainer,    // error tooltip container
        $errorToolTipContent;      // errot tooltip content holder

    var TOOLTIP_BOUNDS_OFFSET          = 8;    // offset between tooltip and position of the cursor / or bounds of the editor

    /**
     * and error maps containing information about the last linting session
     */
    var errorsMap;

    function setErrorsMap(_errorsMap) {
        errorsMap = _errorsMap;
        hideErrorToolTip();
    }

    // Tooltip widget management ----------------------------------------------

    /**
     * hide the error tooltip
     */
    function hideErrorToolTip() {
        $errorToolTipContainer.hide();
        $errorToolTipContent.html('');
    }

    function getEditorHolder() {
        var fullEditor = EditorManager.getCurrentFullEditor();
        return fullEditor ? $(fullEditor.getRootElement()) : $('#editor-holder');
    }

    /**
     * if the errorsMap contains errors for the given position returns that error
     * else returns null
     */
    function getLineErrorForPos(pos) {
        var lineErrors;
        if (errorsMap && (lineErrors = errorsMap[pos.line])) {
            for (var i =0, l = lineErrors.length; i < l; i++) {
                var error = lineErrors[i];
                if (pos.ch >= error.pos.ch && pos.ch <= error.endpos.ch) {
                    return error;
                }
            }
        }
        return null;
    }

    /**
     * position the tooltip below the marked error, centered
     * but always in the bound of the editor
     */
    function positionToolTip(xpos, ypos, ybot) {
        $errorToolTipContainer.offset({
            left: 0,
            top: 0
        });
        $errorToolTipContainer.css('visibility', 'hidden');

        window.requestAnimationFrame(function () {
            var toolTipWidth  = $errorToolTipContainer.width(),
                toolTipHeight = $errorToolTipContainer.height(),
                top           = ybot + TOOLTIP_BOUNDS_OFFSET,
                left          = xpos - (toolTipWidth / 2 ),

                $editorHolder = getEditorHolder(),
                editorOffset = $editorHolder.offset();

            left = Math.max(left, editorOffset.left + TOOLTIP_BOUNDS_OFFSET);
            left = Math.min(left, editorOffset.left + $editorHolder.width() - toolTipWidth - TOOLTIP_BOUNDS_OFFSET - 10);

            if (top < (editorOffset.top + $editorHolder.height() - toolTipHeight - TOOLTIP_BOUNDS_OFFSET)) {
                $errorToolTipContainer.removeClass('preview-bubble-above');
                $errorToolTipContainer.addClass('preview-bubble-below');
                $errorToolTipContainer.offset({
                    left: left,
                    top: top
                });
            } else {
                $errorToolTipContainer.removeClass('preview-bubble-below');
                $errorToolTipContainer.addClass('preview-bubble-above');
                top = ypos - TOOLTIP_BOUNDS_OFFSET - toolTipHeight;
                $errorToolTipContainer.offset({
                    left: left,
                    top: top
                });
            }

            $errorToolTipContainer.css('visibility', 'visible');
        });
    }

    /**
     * last position handled
     */
    var lastPos;
    function handleMouseMove(event) {
        if (event.which) {
            // Button is down - don't show popovers while dragging
            hideErrorToolTip();
            return;
        }

        var editor = EditorManager.getCurrentFullEditor();

        if (!editor || !divContainsMouse($(editor.getRootElement()), event, 10, 10)) {
            hideErrorToolTip();
            return;
        }
        // Find char mouse is over
        var cm = editor._codeMirror,
            pos = cm.coordsChar({left: event.clientX, top: event.clientY});

        var lineErrors, coord;
        // No preview if mouse is past last char on line
        if (pos.ch >= editor.document.getLine(pos.line).length) {
            lineErrors = errorsTick.getErrorsForMouse(event);
            if (!lineErrors.length) {
                hideErrorToolTip();
                return;
            }
            coord = {
                left: event.clientX,
                top: event.clientY,
                bottom: event.clientY
            };
        } else {
            // Bail if mouse is on same char as last event
            if (lastPos && lastPos.line === pos.line && lastPos.ch === pos.ch) {
                return;
            }
            lastPos = pos;
            var lineError = getLineErrorForPos(pos);
            if (!lineError) {
                hideErrorToolTip();
                return;
            }
            lineErrors = [lineError];
            coord = cm.charCoords(pos);
        }

        $errorToolTipContent.html(
            _.flatten(lineErrors.map(function (lineError) {
                return lineError.errors.map(function (error) {
                    return error.message;
                });
            })).join('
')
        );

        $errorToolTipContainer.show();
        positionToolTip(coord.left, coord.top, coord.bottom);
    }

    function handleMouseOut(event) {
        var $editorHolder = getEditorHolder();
        if (!divContainsMouse($editorHolder, event, 10, 10)) {
            hideErrorToolTip();
        }
    }

    // Bootstraping ----------------------------------------------
    function init() {
        var editorHolder = $('#editor-holder')[0];
        // Note: listening to 'scroll' also catches text edits, which bubble a scroll event up from the hidden text area. This means
        // we auto-hide on text edit, which is probably actually a good thing.
        editorHolder.addEventListener('mousemove', handleMouseMove, true);
        editorHolder.addEventListener('scroll', hideErrorToolTip, true);
        editorHolder.addEventListener('mouseout', handleMouseOut, true);

        $errorToolTipContainer = $(errorToolTipHTML).appendTo($('body'));
        $errorToolTipContent = $errorToolTipContainer.find('.error-tooltip-content');

    }

    return {
        init: init,
        setErrorsMap: setErrorsMap
    };

});<|endoftext|>


<commit_before>import { Class } from '../core/Core';
import Obj from '../foundation/Object';
import '../foundation/ObservableProps';  // For Function#observes
import '../foundation/ComputedProps';  // For Function#property, #nocache

const SelectionController = Class({

    Extends: Obj,

    content: null,
    visible: null,

    init: function (/* ...mixins */) {
        this._selectionId = 0;
        this._lastSelectedIndex = 0;
        this._selectedStoreKeys = {};

        this.isLoadingSelection = false;
        this.length = 0;

        SelectionController.parent.constructor.apply( this, arguments );

        const content = this.get( 'content' );
        if ( content ) {
            content.on( 'query:updated', this, 'contentWasUpdated' );
        }
    },

    contentDidChange: function ( _, __, oldContent, newContent ) {
        if ( oldContent ) {
            oldContent.off( 'query:updated', this, 'contentWasUpdated' );
        }
        if ( newContent ) {
            newContent.on( 'query:updated', this, 'contentWasUpdated' );
        }
        this.selectNone();
    }.observes( 'content' ),

    visibleDidChange: function () {
        this._lastSelectedIndex = 0;
    }.observes( 'visible' ),

    contentWasUpdated ( event ) {
        // If an id has been removed, it may no
        // longer belong to the selection
        const _selectedStoreKeys = this._selectedStoreKeys;
        let length = this.get( 'length' );
        const removed = event.removed;
        const added = event.added.reduce( ( set, storeKey ) => {
            set[ storeKey ] = true;
            return set;
        }, {} );
        let l = removed.length;
        let storeKey;

        while ( l-- ) {
            storeKey = removed[l];
            if ( _selectedStoreKeys[ storeKey ] && !added[ storeKey ] ) {
                length -= 1;
                delete _selectedStoreKeys[ storeKey ];
            }
        }

        this.set( 'length', length )
            .propertyDidChange( 'selectedStoreKeys' );
    },

    // ---

    selectedStoreKeys: function () {
        return Object.keys( this._selectedStoreKeys );
    }.property().nocache(),

    isStoreKeySelected ( storeKey ) {
        return !!this._selectedStoreKeys[ storeKey ];
    },

    getSelectedRecords ( store ) {
        return this.get( 'selectedStoreKeys' ).map(
            storeKey => store.getRecordFromStoreKey( storeKey )
        );
    },

    // ---

    selectStoreKeys ( storeKeys, isSelected, _selectionId ) {
        if ( _selectionId && _selectionId !== this._selectionId ) {
            return;
        }
        // Make sure we've got a boolean
        isSelected = !!isSelected;

        const _selectedStoreKeys = this._selectedStoreKeys;
        let howManyChanged = 0;
        let l = storeKeys.length;
        let storeKey, wasSelected;

        while ( l-- ) {
            storeKey = storeKeys[l];
            wasSelected = !!_selectedStoreKeys[ storeKey ];
            if ( isSelected !== wasSelected ) {
                if ( isSelected ) {
                    _selectedStoreKeys[ storeKey ] = true;
                } else {
                    delete _selectedStoreKeys[ storeKey ];
                }
                howManyChanged += 1;
            }
        }

        if ( howManyChanged ) {
            this.increment( 'length',
                    isSelected ? howManyChanged : -howManyChanged )
                .propertyDidChange( 'selectedStoreKeys' );
        }

        this.set( 'isLoadingSelection', false );
    },

    selectIndex ( index, isSelected, includeRangeFromLastSelected ) {
        const lastSelectedIndex = this._lastSelectedIndex;
        const start = includeRangeFromLastSelected ?
                Math.min( index, lastSelectedIndex ) : index;
        const end = ( includeRangeFromLastSelected ?
                Math.max( index, lastSelectedIndex ) : index ) + 1;
        this._lastSelectedIndex = index;
        return this.selectRange( start, end, isSelected );
    },

    selectRange ( start, end, isSelected ) {
        const query = this.get( 'visible' ) || this.get( 'content' );
        const selectionId = ( this._selectionId += 1 );
        const loading = query.getStoreKeysForObjectsInRange(
            start, Math.min( end, query.get( 'length' ) || 0 ),
            ( storeKeys, start, end ) => {
                this.selectStoreKeys( storeKeys,
                    isSelected, selectionId, start, end );
            }
        );

        if ( loading ) {
            this.set( 'isLoadingSelection', true );
        }

        return this;
    },

    selectAll () {
        const query = this.get( 'visible' ) || this.get( 'content' );
        const selectionId = ( this._selectionId += 1 );
        const loading = query.getStoreKeysForAllObjects(
            ( storeKeys, start, end ) => {
                this.selectStoreKeys( storeKeys,
                    true, selectionId, start, end );
            }
        );

        if ( loading ) {
            this.set( 'isLoadingSelection', true );
        }

        return this;
    },

    selectNone () {
        this._lastSelectedIndex = 0;
        this._selectedStoreKeys = {};
        this.set( 'length', 0 )
            .propertyDidChange( 'selectedStoreKeys' )
            .set( 'isLoadingSelection', false );

        return this;
    },
});

export default SelectionController;
<commit_msg>Fix memory leak when destroying SelectionController<commit_after>import { isDestroyed, Class } from '../core/Core';
import Obj from '../foundation/Object';
import '../foundation/ObservableProps';  // For Function#observes
import '../foundation/ComputedProps';  // For Function#property, #nocache

const SelectionController = Class({

    Extends: Obj,

    content: null,
    visible: null,

    init: function (/* ...mixins */) {
        this._selectionId = 0;
        this._lastSelectedIndex = 0;
        this._selectedStoreKeys = {};

        this.isLoadingSelection = false;
        this.length = 0;

        SelectionController.parent.constructor.apply( this, arguments );

        const content = this.get( 'content' );
        if ( content ) {
            content.on( 'query:updated', this, 'contentWasUpdated' );
        }
    },

    destroy () {
        const content = this.get( 'content' );
        if ( content ) {
            content.off( 'query:updated', this, 'contentWasUpdated' );
        }
        SelectionController.parent.destroy.call( this );
    },

    contentDidChange: function ( _, __, oldContent, newContent ) {
        if ( oldContent ) {
            oldContent.off( 'query:updated', this, 'contentWasUpdated' );
        }
        if ( newContent ) {
            newContent.on( 'query:updated', this, 'contentWasUpdated' );
        }
        this.selectNone();
    }.observes( 'content' ),

    visibleDidChange: function () {
        this._lastSelectedIndex = 0;
    }.observes( 'visible' ),

    contentWasUpdated ( event ) {
        // If an id has been removed, it may no
        // longer belong to the selection
        const _selectedStoreKeys = this._selectedStoreKeys;
        let length = this.get( 'length' );
        const removed = event.removed;
        const added = event.added.reduce( ( set, storeKey ) => {
            set[ storeKey ] = true;
            return set;
        }, {} );
        let l = removed.length;
        let storeKey;

        while ( l-- ) {
            storeKey = removed[l];
            if ( _selectedStoreKeys[ storeKey ] && !added[ storeKey ] ) {
                length -= 1;
                delete _selectedStoreKeys[ storeKey ];
            }
        }

        this.set( 'length', length )
            .propertyDidChange( 'selectedStoreKeys' );
    },

    // ---

    selectedStoreKeys: function () {
        return Object.keys( this._selectedStoreKeys );
    }.property().nocache(),

    isStoreKeySelected ( storeKey ) {
        return !!this._selectedStoreKeys[ storeKey ];
    },

    getSelectedRecords ( store ) {
        return this.get( 'selectedStoreKeys' ).map(
            storeKey => store.getRecordFromStoreKey( storeKey )
        );
    },

    // ---

    selectStoreKeys ( storeKeys, isSelected, _selectionId ) {
        if ( _selectionId && _selectionId !== this._selectionId ||
                isDestroyed( this ) ) {
            return;
        }
        // Make sure we've got a boolean
        isSelected = !!isSelected;

        const _selectedStoreKeys = this._selectedStoreKeys;
        let howManyChanged = 0;
        let l = storeKeys.length;
        let storeKey, wasSelected;

        while ( l-- ) {
            storeKey = storeKeys[l];
            wasSelected = !!_selectedStoreKeys[ storeKey ];
            if ( isSelected !== wasSelected ) {
                if ( isSelected ) {
                    _selectedStoreKeys[ storeKey ] = true;
                } else {
                    delete _selectedStoreKeys[ storeKey ];
                }
                howManyChanged += 1;
            }
        }

        if ( howManyChanged ) {
            this.increment( 'length',
                    isSelected ? howManyChanged : -howManyChanged )
                .propertyDidChange( 'selectedStoreKeys' );
        }

        this.set( 'isLoadingSelection', false );
    },

    selectIndex ( index, isSelected, includeRangeFromLastSelected ) {
        const lastSelectedIndex = this._lastSelectedIndex;
        const start = includeRangeFromLastSelected ?
                Math.min( index, lastSelectedIndex ) : index;
        const end = ( includeRangeFromLastSelected ?
                Math.max( index, lastSelectedIndex ) : index ) + 1;
        this._lastSelectedIndex = index;
        return this.selectRange( start, end, isSelected );
    },

    selectRange ( start, end, isSelected ) {
        const query = this.get( 'visible' ) || this.get( 'content' );
        const selectionId = ( this._selectionId += 1 );
        const loading = query.getStoreKeysForObjectsInRange(
            start, Math.min( end, query.get( 'length' ) || 0 ),
            ( storeKeys, start, end ) => {
                this.selectStoreKeys( storeKeys,
                    isSelected, selectionId, start, end );
            }
        );

        if ( loading ) {
            this.set( 'isLoadingSelection', true );
        }

        return this;
    },

    selectAll () {
        const query = this.get( 'visible' ) || this.get( 'content' );
        const selectionId = ( this._selectionId += 1 );
        const loading = query.getStoreKeysForAllObjects(
            ( storeKeys, start, end ) => {
                this.selectStoreKeys( storeKeys,
                    true, selectionId, start, end );
            }
        );

        if ( loading ) {
            this.set( 'isLoadingSelection', true );
        }

        return this;
    },

    selectNone () {
        this._lastSelectedIndex = 0;
        this._selectedStoreKeys = {};
        this.set( 'length', 0 )
            .propertyDidChange( 'selectedStoreKeys' )
            .set( 'isLoadingSelection', false );

        return this;
    },
});

export default SelectionController;
<|endoftext|>