<commit_before>'use strict'

console.log('Welcome #100DaysOfCode Twitter Bot')

// Dependencies
const schedule = require('node-schedule')
const twit = require('twit')
const config = require('./config')
const bot = new twit(config.twitterKeys)
const moment = require('moment')

// Import API functions
const retweet = require('./api/retweet')
const like = require('./api/like')
const reply = require('./api/reply')
const projectOfTheDay = require('./api/project-of-day')
const refreshDb = require('./api/refresh-db')
const sentimentBot = require('./api/sentiment')
const promoteSlackChannel = require('./api/promoteSlackChannel')

// import helpers
const isReply = require('./helpers/isReply')

// Frequency in minutes
const frequency = 1000 * 60 * 30
const firstOrLastDayFrequency = 40

// counter for tweets limit
// detail here: https://support.twitter.com/articles/15364#
const tweetLimit = 2400

// load up keywords
const param = config.twitterConfig
const trackWords = param.queryString.split(',')

// use stream to track keywords
const trackStream = bot.stream('statuses/filter', {
  track: trackWords
})

// Retweet and like on keywords
trackStream.on('tweet', addTweetToQueue)

// tweets object
let tweets = []
let tweetCounter = tweetLimit / 24

// add tweet to object
// e is the tweet event
function addTweetToQueue(e) {
  if (isReply(e)) {
    console.log('====================')
    console.log(`=IS REPLY RETURNING=`)
    console.log('====================')
    return
  }
  tweets.push({
    tweet: e.text,
    tweetId: e.id_str,
    user: e.user.screen_name,
    timeIn: new Date(newTimeIn()),
    timeOut: new Date(newTimeOut()),
    event: e // EVERYTHING!!!
  })
  console.log(`Item added to queue, current length=${tweets.length}`)
  // console.log(tweets)
}

// function to rerurn random
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

// set timeIn
const newTimeIn = date => {
  return moment(date).toDate()
}

// set timeOut 🙃
const randomMin = param.tweetTimeOutMin
const randomMax = param.tweetTimeOutMax

const newTimeOut = date => {
  return moment(date)
  .add(getRandomInt(randomMin, randomMax), 'm')
  .toDate()
}

// loop through tweets object
// pop off tweets after timeOut is matched
const queueTime = param.tweetQueueTime

setInterval(() => {
  // check counter
  console.log('====================')
  console.log(`TWEET COUNTER=${tweetCounter}`)
  console.log('====================')
  if (tweetCounter === 0) return
  // new array from tweets, right?
  tweets = tweets.slice()
  // sort it
  tweets.sort((a, b) => a.timeOut - b.timeOut)
  // loop through the thing
  tweets.map(item => {
    // console.log(time.timeOut)
    const itemTimeOut = new Date(item.timeOut).getTime()
    const currentTime = new Date().getTime()
    console.log('====================')
    console.log(`ITEM TIME OUT==== ${timeConverter(itemTimeOut)}`)
    console.log(`ITEM TIME NOW==== ${timeConverter(currentTime)}`)
    console.log(`POP IT OFF?====== ${itemTimeOut <= currentTime}`)
    console.log('====================')
    if (itemTimeOut <= currentTime) {
      // item needs 'dispatching' so tweet it
      const itemEvent = item.event
      // console.log(itemEvent)
      // coin flip to like or retweet
      if (Math.floor(Math.random() * 2)===0) {
        retweet(itemEvent)
      } else {
        like(itemEvent)
      }
      // then remove it
      tweets.shift()
      console.log(`Item removed from queue, current length ${tweets.length}`)
      // count down to 0 from the max tweet number = 100
      tweetCounter--
    }
  })
  return tweets
}, queueTime)

function timeConverter(UNIX_timestamp) {
  return new Date(UNIX_timestamp).toISOString()
}

// Reply
const userStream = bot.stream('user')
userStream.on('follow', reply)

// Use cron-job to schedule Project of the day
const rule = new schedule.RecurrenceRule()
rule.dayOfWeek = [0, new schedule.Range(1,6)]
rule.hour = 11
rule.minute = 59

var job = schedule.scheduleJob(rule, () => {
  console.log('Cron Job runs successfully')
  projectOfTheDay()
})

// Use cron-job to schedule promote slack channel
const rule2 = new schedule.RecurrenceRule()
rule2.dayOfWeek = [0, new schedule.Range(1,6)]
rule2.hour = 6
rule2.minute = 41

var job2 = schedule.scheduleJob(rule2, () => {
  console.log('Promote Slack Channel Cron Job runs successfully')
  promoteSlackChannel()
})

// Refresh LevelDB every 24 hrs
setInterval(refreshDb, 1000 * 60 * 60 * 24)

sentimentBot()

// ABANDONED API(s)

// Congratulation Messages for Day 1 & Day 100 ========
// const hashtagStream = T.stream('statuses/filter', {
//   track: ['#100DaysOfCode']
// })
//
// // Function that checks if day 1 or day 100
// var checkIfFirstOrLastDay = function() {
//   hashtagStream.on('tweet', (tweet) => {
//     if (checkIfLastDay(tweet)) {
//       console.log(`Sending a congrats to @${tweet.user.screen_name}`)
//       tweetNow(`WOOT! You did it @${tweet.user.screen_name}! Party Time!`)
//     }
//     else if (checkIfFirstDay(tweet)) {
//       console.log('Sending a congrats to @${tweet.user.screen_name}')
//       tweetNow(`Congrats on your first day @${tweet.user.screen_name}! Keep it up!`)
//     }
//   })
// }
// checkIfFirstOrLastDay()
// setImmediate(checkIfFirstOrLastDay, 1000 * 60 * firstOrLastDayFrequency)
//
// // NOTE: String elements in firstDay & lastDay are case insensitive
//
// function checkIfFirstDay(tweet) {
//   const firstDay = ['first day', 'day one', 'day 1/100']
//   const firstdayRegex = /\bday\s?0?1\b/i
//   console.log(`Checking if first day`)
//   for (let i = 0; i < firstDay.length; i++) {
//     if (checkTweetForText(tweet.text, firstDay[i]) || tweet.text.match(firstdayRegex) != null) {
//       return true
//     }
//   }
// }
//
// function checkIfLastDay(tweet) {
//   const lastDay = ['#day100', 'final day', 'day 100', 'one hundred', '100/100']
//   const lastdayRegex = /\bday\s?100\b/i
//   console.log(`Checking if Last day`)
//   for (let i = 0; i < lastDay.length; i++) {
//     if (checkTweetForText(tweet.text, lastDay[i]) || tweet.text.match(lastdayRegex) != null) {
//       return true
//     }
//   }
// }
//
// function checkTweetForText(tweetText, value) {
//   return tweetText.toLowerCase().indexOf(value) > -1 && tweetText.toLowerCase().indexOf('100daysofcode') > -1
// }
<commit_msg>parseInt :upside_down_face:<commit_after>'use strict'

console.log('Welcome #100DaysOfCode Twitter Bot')

// Dependencies
const schedule = require('node-schedule')
const twit = require('twit')
const config = require('./config')
const bot = new twit(config.twitterKeys)
const moment = require('moment')

// Import API functions
const retweet = require('./api/retweet')
const like = require('./api/like')
const reply = require('./api/reply')
const projectOfTheDay = require('./api/project-of-day')
const refreshDb = require('./api/refresh-db')
const sentimentBot = require('./api/sentiment')
const promoteSlackChannel = require('./api/promoteSlackChannel')

// import helpers
const isReply = require('./helpers/isReply')

// Frequency in minutes
const frequency = 1000 * 60 * 30
const firstOrLastDayFrequency = 40

// counter for tweets limit
// detail here: https://support.twitter.com/articles/15364#
const tweetLimit = 2400

// load up keywords
const param = config.twitterConfig
const trackWords = param.queryString.split(',')

// use stream to track keywords
const trackStream = bot.stream('statuses/filter', {
  track: trackWords
})

// Retweet and like on keywords
trackStream.on('tweet', addTweetToQueue)

// tweets object
let tweets = []
let tweetCounter = tweetLimit / 24

// add tweet to object
// e is the tweet event
function addTweetToQueue(e) {
  if (isReply(e)) {
    console.log('====================')
    console.log(`=IS REPLY RETURNING=`)
    console.log('====================')
    return
  }
  tweets.push({
    tweet: e.text,
    tweetId: e.id_str,
    user: e.user.screen_name,
    timeIn: new Date(newTimeIn()),
    timeOut: new Date(newTimeOut()),
    event: e // EVERYTHING!!!
  })
  console.log(`Item added to queue, current length=${tweets.length}`)
  // console.log(tweets)
}

// function to rerurn random
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

// set timeIn
const newTimeIn = date => {
  return moment(date).toDate()
}

// set timeOut 🙃
const randomMin = parseInt(param.tweetTimeOutMin)
const randomMax = parseInt(param.tweetTimeOutMax)

const newTimeOut = date => {
  return moment(date)
  .add(getRandomInt(randomMin, randomMax), 'm')
  .toDate()
}

// loop through tweets object
// pop off tweets after timeOut is matched
const queueTime = param.tweetQueueTime

setInterval(() => {
  // check counter
  console.log('====================')
  console.log(`TWEET COUNTER=${tweetCounter}`)
  console.log('====================')
  if (tweetCounter === 0) return
  // new array from tweets, right?
  tweets = tweets.slice()
  // sort it
  tweets.sort((a, b) => a.timeOut - b.timeOut)
  // loop through the thing
  tweets.map(item => {
    // console.log(time.timeOut)
    const itemTimeOut = new Date(item.timeOut).getTime()
    const currentTime = new Date().getTime()
    console.log('====================')
    console.log(`ITEM TIME OUT==== ${timeConverter(itemTimeOut)}`)
    console.log(`ITEM TIME NOW==== ${timeConverter(currentTime)}`)
    console.log(`POP IT OFF?====== ${itemTimeOut <= currentTime}`)
    console.log('====================')
    if (itemTimeOut <= currentTime) {
      // item needs 'dispatching' so tweet it
      const itemEvent = item.event
      // console.log(itemEvent)
      // coin flip to like or retweet
      if (Math.floor(Math.random() * 2)===0) {
        retweet(itemEvent)
      } else {
        like(itemEvent)
      }
      // then remove it
      tweets.shift()
      console.log(`Item removed from queue, current length ${tweets.length}`)
      // count down to 0 from the max tweet number = 100
      tweetCounter--
    }
  })
  return tweets
}, queueTime)

function timeConverter(UNIX_timestamp) {
  return new Date(UNIX_timestamp).toISOString()
}

// Reply
const userStream = bot.stream('user')
userStream.on('follow', reply)

// Use cron-job to schedule Project of the day
const rule = new schedule.RecurrenceRule()
rule.dayOfWeek = [0, new schedule.Range(1,6)]
rule.hour = 11
rule.minute = 59

var job = schedule.scheduleJob(rule, () => {
  console.log('Cron Job runs successfully')
  projectOfTheDay()
})

// Use cron-job to schedule promote slack channel
const rule2 = new schedule.RecurrenceRule()
rule2.dayOfWeek = [0, new schedule.Range(1,6)]
rule2.hour = 6
rule2.minute = 41

var job2 = schedule.scheduleJob(rule2, () => {
  console.log('Promote Slack Channel Cron Job runs successfully')
  promoteSlackChannel()
})

// Refresh LevelDB every 24 hrs
setInterval(refreshDb, 1000 * 60 * 60 * 24)

sentimentBot()

// ABANDONED API(s)

// Congratulation Messages for Day 1 & Day 100 ========
// const hashtagStream = T.stream('statuses/filter', {
//   track: ['#100DaysOfCode']
// })
//
// // Function that checks if day 1 or day 100
// var checkIfFirstOrLastDay = function() {
//   hashtagStream.on('tweet', (tweet) => {
//     if (checkIfLastDay(tweet)) {
//       console.log(`Sending a congrats to @${tweet.user.screen_name}`)
//       tweetNow(`WOOT! You did it @${tweet.user.screen_name}! Party Time!`)
//     }
//     else if (checkIfFirstDay(tweet)) {
//       console.log('Sending a congrats to @${tweet.user.screen_name}')
//       tweetNow(`Congrats on your first day @${tweet.user.screen_name}! Keep it up!`)
//     }
//   })
// }
// checkIfFirstOrLastDay()
// setImmediate(checkIfFirstOrLastDay, 1000 * 60 * firstOrLastDayFrequency)
//
// // NOTE: String elements in firstDay & lastDay are case insensitive
//
// function checkIfFirstDay(tweet) {
//   const firstDay = ['first day', 'day one', 'day 1/100']
//   const firstdayRegex = /\bday\s?0?1\b/i
//   console.log(`Checking if first day`)
//   for (let i = 0; i < firstDay.length; i++) {
//     if (checkTweetForText(tweet.text, firstDay[i]) || tweet.text.match(firstdayRegex) != null) {
//       return true
//     }
//   }
// }
//
// function checkIfLastDay(tweet) {
//   const lastDay = ['#day100', 'final day', 'day 100', 'one hundred', '100/100']
//   const lastdayRegex = /\bday\s?100\b/i
//   console.log(`Checking if Last day`)
//   for (let i = 0; i < lastDay.length; i++) {
//     if (checkTweetForText(tweet.text, lastDay[i]) || tweet.text.match(lastdayRegex) != null) {
//       return true
//     }
//   }
// }
//
// function checkTweetForText(tweetText, value) {
//   return tweetText.toLowerCase().indexOf(value) > -1 && tweetText.toLowerCase().indexOf('100daysofcode') > -1
// }
<|endoftext|>


<commit_before>(function () {

    "use strict";

    /**
     A **Model** is a unit of content within a xeogl {{#crossLink "Scene"}}{{/crossLink}}.

     ## Overview

     * A Model is a container of {{#crossLink "Component"}}Components{{/crossLink}}.
     * Can be transformed within World-space by attaching it to a {{#crossLink "Transform"}}{{/crossLink}}.
     * Provides its World-space boundary as a {{#crossLink "Boundary3D"}}{{/crossLink}}.
     * Subclassed by {{#crossLink "GLTFModel"}}{{/crossLink}}, which loads glTF files.
     * Subclassed by {{#crossLink "BuildableModel"}}{{/crossLink}}, which provides a fluent API for building itself.

     @class Model
     @module xeogl
     @submodule models
     @constructor
     @param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this ModelModel in the default
     {{#crossLink "Scene"}}Scene{{/crossLink}} when omitted.
     @param [cfg] {*} Configs
     @param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
     generated automatically when omitted.
     @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this ModelModel.
     @param [cfg.transform] {Number|String|Transform} A Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} to attach to this Model.
     Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this Model. Internally, the given
     {{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most {{#crossLink "Transform"}}Transform{{/crossLink}}
     that the Model attaches to its {{#crossLink "Entity"}}Entities{{/crossLink}}.
     @extends Component
     */
    xeogl.Model = xeogl.Component.extend({

        /**
         JavaScript class name for this Component.

         @property type
         @type String
         @final
         */
        type: "xeogl.Model",

        _init: function (cfg) {

            /**
             * The {{#crossLink "Components"}}{{/crossLink}} within this Model, mapped to their IDs.
             *
             * Fires an {{#crossLink "Model/updated:event"}}{{/crossLink}} event on change.
             *
             * @property components
             * @type {{String:Component}}
             */
            this.components = {};

            /**
             * The number of {{#crossLink "Components"}}{{/crossLink}} within this Model.
             *
             * @property numComponents
             * @type Number
             */
            this.numComponents = 0;

            /**
             * A map of maps; for each {{#crossLink "Component"}}{{/crossLink}} type in this Model,
             * a map to IDs to {{#crossLink "Component"}}{{/crossLink}} instances, eg.
             *
             * ````
             * "xeogl.Geometry": {
             *   "alpha": <xeogl.Geometry>,
             *   "beta": <xeogl.Geometry>
             * },
             * "xeogl.Rotate": {
             *   "charlie": <xeogl.Rotate>,
             *   "delta": <xeogl.Rotate>,
             *   "echo": <xeogl.Rotate>,
             * },
             * //...
             * ````
             *
             * @property types
             * @type {String:{String:xeogl.Component}}
             */
            this.types = {};

            // Subscriptions to "destroyed" events from components
            this._onDestroyed = {};

            // Subscriptions to "updated" events from components' worldBoundaries
            this._onWorldBoundaryUpdated = {};

            this._aabbDirty = true;

            // Dummy transform to make it easy to graft user-supplied transforms above added entities
            this._dummyRootTransform = this.create({
                type: "xeogl.Transform",
                meta: "dummy"
            });

            this.transform = cfg.transform;

            if (cfg.components) {
                this.add(cfg.components);
            }
        },

        /**
         * Adds a {{#crossLink "Component"}}Component{{/crossLink}} or subtype to this Model.
         *
         * The {{#crossLink "Component"}}Component(s){{/crossLink}} may be specified by ID, instance, JSON definition or type.
         *
         * See class comment for usage examples.
         *
         * The {{#crossLink "Component"}}Components{{/crossLink}} must be in the same {{#crossLink "Scene"}}{{/crossLink}} as this Model.
         *
         * Fires an {{#crossLink "Model/added:event"}}{{/crossLink}} event.
         *
         * @method add
         * @param {Number|String|*|Component} component ID, definition or instance of a {{#crossLink "Component"}}Component{{/crossLink}} type or subtype.
         */
        add: function (component) {

            var componentId;
            var types;

            if (xeogl._isNumeric(component) || xeogl._isString(component)) {

                if (this.scene.types[component]) {

                    // Component type

                    type = component;

                    types = this.scene.types[type];

                    if (!types) {
                        this.warn("Component type not found: '" + type + "'");
                        return;
                    }

                    for (componentId in types) {
                        if (types.hasOwnProperty(componentId)) {
                            this._add(types[componentId]);
                        }
                    }

                    return;

                } else {

                    // Component ID

                    component = this.scene.components[component];

                    if (!component) {
                        this.warn("Component not found: " + xeogl._inQuotes(component));
                        return;
                    }
                }

            } else if (xeogl._isObject(component)) {

                // Component config given

                var type = component.type || "xeogl.Component";

                if (!xeogl._isComponentType(type)) {
                    this.error("Not a xeogl component type: " + type);
                    return;
                }

                component = new window[type](this.scene, component);

            } else {

                return;
            }

            if (component.scene !== this.scene) {

                // Component in wrong Scene

                this.warn("Attempted to add component from different xeogl.Scene: " + xeogl._inQuotes(component.id));
                return;
            }

            // Add component to this map

            if (this.components[component.id]) {

                // Component already in this Model
                return;
            }

            this.components[component.id] = component;

            // Register component for its type

            types = this.types[component.type];

            if (!types) {
                types = this.types[component.type] = {};
            }

            types[component.id] = component;

            this.numComponents++;

            // Remove component when it's destroyed

            var self = this;

            this._onDestroyed[component.id] = component.on("destroyed", function () {
                self._remove(component);
            });

            if (component.isType("xeogl.Entity")) {

                // Insert the dummy transform above
                // each entity we just loaded

                var rootTransform = component.transform;

                if (!rootTransform) {

                    component.transform = self._dummyRootTransform;

                } else {

                    while (rootTransform.parent) {

                        if (rootTransform.id === self._dummyRootTransform.id) {

                            // Since transform hierarchies may contain
                            // transforms that share the same parents, there is potential to find
                            // our dummy root transform while walking up an entity's transform
                            // path, when that path is joins a path that belongs to an Entity that
                            // we processed earlier

                            return;
                        }

                        rootTransform = rootTransform.parent;
                    }

                    if (rootTransform.id !== self._dummyRootTransform.id) {
                        rootTransform.parent = self._dummyRootTransform;
                    }
                }
            }

            if (component.worldBoundary) {
                this._onWorldBoundaryUpdated[component.id] = component.worldBoundary.on("updated", this._updated, this);
                if (!this._aabbDirty) {
                    this._setAABBDirty();
                }
            }

            /**
             * Fired whenever an individual {{#crossLink "Component"}}{{/crossLink}} is added to this {{#crossLink "Model"}}{{/crossLink}}.
             * @event added
             * @param value {Component} The {{#crossLink "Component"}}{{/crossLink}} that was added.
             */
            this.fire("added", component);

            if (!this._dirty) {
                this._scheduleUpdate();
            }

            return component;
        },

        _scheduleUpdate: function () {
            if (!this._dirty) {
                this._dirty = true;
                xeogl.scheduleTask(this._notifyUpdated, this);
            }
        },

        _notifyUpdated: function () {

            /* Fired on the next {{#crossLink "Scene/tick.animate:event"}}{{/crossLink}} whenever
             * {{#crossLink "Component"}}Components{{/crossLink}} were added or removed since the
             * last {{#crossLink "Scene/tick.animate:event"}}{{/crossLink}} event, to provide a batched change event
             * for subscribers who don't want to react to every individual addition or removal on this Model.
             *
             * @event updated
             */
            this.fire("updated");
            this._dirty = false;
        },

        /**
         * Destroys all {{#crossLink "Component"}}Components{{/crossLink}} in this Model.
         *
         * @method destroyAll
         */
        destroyAll: function () {

            this.iterate(function (component) {
                component.destroy();
            });
        },

        /**
         * Removes all {{#crossLink "Component"}}Components{{/crossLink}} from this Model.
         *
         * @method removeAll
         */
        removeAll: function () {

            // this.iterate(function (component) {
            //     component.destroy();
            // });
        },

        _remove: function (component) {

            var componentId = component.id;

            if (component.scene !== this.scene) {
                this.warn("Attempted to remove component that's not in same xeogl.Scene: '" + componentId + "'");
                return;
            }

            delete this.components[componentId];

            // Unsubscribe from component destruction

            component.off(this._onDestroyed[componentId]);
            delete this._onDestroyed[componentId];

            // Unregister component for its type

            var types = this.types[component.type];

            if (types) {
                delete types[component.id];
            }

            this.numComponents--;

            //

            if (component.worldBoundary) {
                component.worldBoundary.off(this._onWorldBoundaryUpdated[component.id]);
                delete this._onWorldBoundaryUpdated[component.id];
            }

            if (!this._aabbDirty) {
                this._setAABBDirty();
            }

            /**
             * Fired whenever an individual {{#crossLink "Component"}}{{/crossLink}} is removed from this {{#crossLink "Model"}}{{/crossLink}}.
             * @event removed
             * @param value {Component} The {{#crossLink "Component"}}{{/crossLink}} that was removed.
             */
            this.fire("removed", component);

            if (!this._dirty) {
                this._scheduleUpdate();
            }
        },

        /**
         * Iterates with a callback over the {{#crossLink "Component"}}Components{{/crossLink}} in this Model.
         *
         * @method iterate
         * @param {Function} callback Callback called for each {{#crossLink "Component"}}{{/crossLink}}.
         * @param {Object} [scope=this] Optional scope for the callback, defaults to this Model.
         */
        iterate: function (callback, scope) {
            scope = scope || this;
            var components = this.components;
            for (var componentId in components) {
                if (components.hasOwnProperty(componentId)) {
                    callback.call(scope, components[componentId]);
                }
            }
        },

        _props: {

            /**
             * The Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} attached to this Model.
             *
             * Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this Model.
             *
             * Internally, the given {{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most
             * {{#crossLink "Transform"}}Transform{{/crossLink}} that the Model attaches to
             * its {{#crossLink "Entity"}}Entities{{/crossLink}}.
             *
             * Fires an {{#crossLink "Model/transform:event"}}{{/crossLink}} event on change.
             *
             * @property transform
             * @type Transform
             */
            transform: {

                set: function (value) {

                    /**
                     * Fired whenever this Model's {{#crossLink "Model/transform:property"}}{{/crossLink}} property changes.
                     *
                     * @event transform
                     * @param value The property's new value
                     */
                    this._attach({
                        name: "transform",
                        type: "xeogl.Transform",
                        component: value,
                        sceneDefault: false,
                        onAttached: {
                            callback: this._transformUpdated,
                            scope: this
                        }
                    });
                },

                get: function () {
                    return this._attached.transform;
                }
            },

            /**
             * World-space 3D boundary enclosing all the components in this Model.
             *
             * If you call {{#crossLink "Component/destroy:method"}}{{/crossLink}} on this boundary, then
             * this property will be assigned to a fresh {{#crossLink "Boundary3D"}}{{/crossLink}} instance next
             * time you reference it.
             *
             * @property worldBoundary
             * @type Boundary3D
             * @final
             */
            worldBoundary: {

                get: function () {

                    if (!this._worldBoundary) {

                        var self = this;

                        this._worldBoundary = this.create({

                            type: "xeogl.Boundary3D",

                            getDirty: function () {
                                if (self._aabbDirty) {
                                    self._buildAABB();
                                    self._aabbDirty = false;
                                    return true;
                                }
                                return false;
                            },

                            getAABB: function () {
                                return self._aabb;
                            }
                        });

                        this._worldBoundary.on("destroyed",
                            function () {
                                self._worldBoundary = null;
                            });
                    }

                    return this._worldBoundary;
                }
            }
        },

        _transformUpdated: function (transform) {
            this._dummyRootTransform.parent = transform;
        },

        _updated: function () {
            if (!this._aabbDirty) {
                this._setAABBDirty();
            }
        },

        _setAABBDirty: function () {
            this._aabbDirty = true;
            if (this._worldBoundary) {
                this._worldBoundary.fire("updated", true);
            }
        },

        _buildAABB: function () {

            if (!this._aabb) {
                this._aabb = xeogl.math.AABB3();
            }

            var xmin = 100000;
            var ymin = 100000;
            var zmin = 100000;
            var xmax = -100000;
            var ymax = -100000;
            var zmax = -100000;

            var component;
            var worldBoundary;
            var aabb;

            var components = this.components;

            for (var componentId in components) {
                if (components.hasOwnProperty(componentId)) {

                    component = components[componentId];

                    worldBoundary = component.worldBoundary;

                    if (worldBoundary) {

                        aabb = worldBoundary.aabb;

                        if (aabb[0] < xmin) {
                            xmin = aabb[0];
                        }

                        if (aabb[1] < ymin) {
                            ymin = aabb[1];
                        }

                        if (aabb[2] < zmin) {
                            zmin = aabb[2];
                        }

                        if (aabb[3] > xmax) {
                            xmax = aabb[3];
                        }

                        if (aabb[4] > ymax) {
                            ymax = aabb[4];
                        }

                        if (aabb[5] > zmax) {
                            zmax = aabb[5];
                        }
                    }
                }
            }

            this._aabb[0] = xmin;
            this._aabb[1] = ymin;
            this._aabb[2] = zmin;
            this._aabb[3] = xmax;
            this._aabb[4] = ymax;
            this._aabb[5] = zmax;
        },

        _destroy: function () {
            this.removeAll();
        }
    });

})();<commit_msg>Fix xeogl.Model#transform https://github.com/xeolabs/xeogl/issues/155<commit_after>(function () {

    "use strict";

    /**
     A **Model** is a unit of content within a xeogl {{#crossLink "Scene"}}{{/crossLink}}.

     ## Overview

     * A Model is a container of {{#crossLink "Component"}}Components{{/crossLink}}.
     * Can be transformed within World-space by attaching it to a {{#crossLink "Transform"}}{{/crossLink}}.
     * Provides its World-space boundary as a {{#crossLink "Boundary3D"}}{{/crossLink}}.
     * Subclassed by {{#crossLink "GLTFModel"}}{{/crossLink}}, which loads glTF files.
     * Subclassed by {{#crossLink "BuildableModel"}}{{/crossLink}}, which provides a fluent API for building itself.

     ## Usage

     #### Adding components

     When adding components to a Model, it's usually easiest to add their configuration objects
     and let the Model internally create component instances from those. Then the Model owns those
     components, and will destroy them when you destroy the Model or call its
     {{#crossLink "Model/destroyAll: method"}}{{/crossLink}}.

     ````javascript
     var model1 = new xeogl.Model();

     var geometry = model1.add({
            type: "xeogl.TorusGeometry"
        });

     var material = model1.add({
            type: "xeogl.PhongMaterial"
            diffuse: [0.4, 0.4, 9.0]
        });

     model1.add({
            type: "xeogl.Entity",
            geometry: geometry,
            material: material
        });
     ````

     Of course, we can also add component instances, but bear in mind that they will get destroyed if you destroy the Model
     or call its {{#crossLink "Model/destroyAll: method"}}{{/crossLink}} method:

     ````javascript
     var model2 = new xeogl.Model();

     var material2 = new xeogl.PhongMaterial({
            diffuse: [0.4, 1.0, 9.0]
        });

     var geometry2 = model.add({
            type: "xeogl.SphereGeometry"
        });

     var entity2 = new xeogl.Entity({
            geometry: geometry2,
            material: material2
        });

     model.add(material2);
     model.add(geometry2);
     model.add(entity2);
     ````

     #### Transforming

     One of the benefits of keeping components in Models is that we can transform them as a group.

     ````javascript

     ````

     @class Model
     @module xeogl
     @submodule models
     @constructor
     @param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this ModelModel in the default
     {{#crossLink "Scene"}}Scene{{/crossLink}} when omitted.
     @param [cfg] {*} Configs
     @param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
     generated automatically when omitted.
     @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this ModelModel.
     @param [cfg.transform] {Number|String|Transform} A Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} to attach to this Model.
     Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this Model. Internally, the given
     {{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most {{#crossLink "Transform"}}Transform{{/crossLink}}
     that the Model attaches to its {{#crossLink "Entity"}}Entities{{/crossLink}}.
     @extends Component
     */
    xeogl.Model = xeogl.Component.extend({

        /**
         JavaScript class name for this Component.

         @property type
         @type String
         @final
         */
        type: "xeogl.Model",

        _init: function (cfg) {

            /**
             * The {{#crossLink "Components"}}{{/crossLink}} within this Model, mapped to their IDs.
             *
             * Fires an {{#crossLink "Model/updated:event"}}{{/crossLink}} event on change.
             *
             * @property components
             * @type {{String:Component}}
             */
            this.components = {};

            /**
             * The number of {{#crossLink "Components"}}{{/crossLink}} within this Model.
             *
             * @property numComponents
             * @type Number
             */
            this.numComponents = 0;

            /**
             * A map of maps; for each {{#crossLink "Component"}}{{/crossLink}} type in this Model,
             * a map to IDs to {{#crossLink "Component"}}{{/crossLink}} instances, eg.
             *
             * ````
             * "xeogl.Geometry": {
             *   "alpha": <xeogl.Geometry>,
             *   "beta": <xeogl.Geometry>
             * },
             * "xeogl.Rotate": {
             *   "charlie": <xeogl.Rotate>,
             *   "delta": <xeogl.Rotate>,
             *   "echo": <xeogl.Rotate>,
             * },
             * //...
             * ````
             *
             * @property types
             * @type {String:{String:xeogl.Component}}
             */
            this.types = {};

            // Subscriptions to "destroyed" events from components
            this._onDestroyed = {};

            // Subscriptions to "updated" events from components' worldBoundaries
            this._onWorldBoundaryUpdated = {};

            this._aabbDirty = true;

            // Dummy transform to make it easy to graft user-supplied transforms above added entities
            this._dummyRootTransform = this.create({
                type: "xeogl.Transform",
                meta: "dummy"
            });

            this.transform = cfg.transform;

            if (cfg.components) {
                this.add(cfg.components);
            }
        },

        /**
         * Adds a {{#crossLink "Component"}}Component{{/crossLink}} or subtype to this Model.
         *
         * The {{#crossLink "Component"}}Component(s){{/crossLink}} may be specified by ID, instance, JSON definition or type.
         *
         * See class comment for usage examples.
         *
         * The {{#crossLink "Component"}}Components{{/crossLink}} must be in the same {{#crossLink "Scene"}}{{/crossLink}} as this Model.
         *
         * Fires an {{#crossLink "Model/added:event"}}{{/crossLink}} event.
         *
         * @method add
         * @param {Number|String|*|Component} component ID, definition or instance of a {{#crossLink "Component"}}Component{{/crossLink}} type or subtype.
         */
        add: function (component) {

            var componentId;
            var types;

            if (xeogl._isNumeric(component) || xeogl._isString(component)) {

                if (this.scene.types[component]) {

                    // Component type

                    type = component;

                    types = this.scene.types[type];

                    if (!types) {
                        this.warn("Component type not found: '" + type + "'");
                        return;
                    }

                    for (componentId in types) {
                        if (types.hasOwnProperty(componentId)) {
                            this._add(types[componentId]);
                        }
                    }

                    return;

                } else {

                    // Component ID

                    component = this.scene.components[component];

                    if (!component) {
                        this.warn("Component not found: " + xeogl._inQuotes(component));
                        return;
                    }
                }

            } else if (xeogl._isObject(component)) {

                // Component config given

                var type = component.type || "xeogl.Component";

                if (!xeogl._isComponentType(type)) {
                    this.error("Not a xeogl component type: " + type);
                    return;
                }

                component = new window[type](this.scene, component);

            }

            if (component.scene !== this.scene) {

                // Component in wrong Scene

                this.warn("Attempted to add component from different xeogl.Scene: " + xeogl._inQuotes(component.id));
                return;
            }

            // Add component to this map

            if (this.components[component.id]) {

                // Component already in this Model
                return;
            }

            this.components[component.id] = component;

            // Register component for its type

            types = this.types[component.type];

            if (!types) {
                types = this.types[component.type] = {};
            }

            types[component.id] = component;

            this.numComponents++;

            // Remove component when it's destroyed

            var self = this;

            this._onDestroyed[component.id] = component.on("destroyed", function () {
                self._remove(component);
            });

            if (component.isType("xeogl.Entity")) {

                // Insert the dummy transform above
                // each entity we just loaded

                var rootTransform = component.transform;

                if (!rootTransform) {

                    component.transform = self._dummyRootTransform;

                } else {

                    while (rootTransform.parent) {

                        if (rootTransform.id === self._dummyRootTransform.id) {

                            // Since transform hierarchies may contain
                            // transforms that share the same parents, there is potential to find
                            // our dummy root transform while walking up an entity's transform
                            // path, when that path is joins a path that belongs to an Entity that
                            // we processed earlier

                            return;
                        }

                        rootTransform = rootTransform.parent;
                    }

                    if (rootTransform.id !== self._dummyRootTransform.id) {
                        rootTransform.parent = self._dummyRootTransform;
                    }
                }
            }

            if (component.worldBoundary) {
                this._onWorldBoundaryUpdated[component.id] = component.worldBoundary.on("updated", this._updated, this);
                if (!this._aabbDirty) {
                    this._setAABBDirty();
                }
            }

            /**
             * Fired whenever an individual {{#crossLink "Component"}}{{/crossLink}} is added to this {{#crossLink "Model"}}{{/crossLink}}.
             * @event added
             * @param value {Component} The {{#crossLink "Component"}}{{/crossLink}} that was added.
             */
            this.fire("added", component);

            if (!this._dirty) {
                this._scheduleUpdate();
            }

            return component;
        },

        _scheduleUpdate: function () {
            if (!this._dirty) {
                this._dirty = true;
                xeogl.scheduleTask(this._notifyUpdated, this);
            }
        },

        _notifyUpdated: function () {

            /* Fired on the next {{#crossLink "Scene/tick.animate:event"}}{{/crossLink}} whenever
             * {{#crossLink "Component"}}Components{{/crossLink}} were added or removed since the
             * last {{#crossLink "Scene/tick.animate:event"}}{{/crossLink}} event, to provide a batched change event
             * for subscribers who don't want to react to every individual addition or removal on this Model.
             *
             * @event updated
             */
            this.fire("updated");
            this._dirty = false;
        },

        /**
         * Destroys all {{#crossLink "Component"}}Components{{/crossLink}} in this Model.
         *
         * @method destroyAll
         */
        destroyAll: function () {

            this.iterate(function (component) {
                component.destroy();
            });
        },

        /**
         * Removes all {{#crossLink "Component"}}Components{{/crossLink}} from this Model.
         *
         * @method removeAll
         */
        removeAll: function () {

            // this.iterate(function (component) {
            //     component.destroy();
            // });
        },

        _remove: function (component) {

            var componentId = component.id;

            if (component.scene !== this.scene) {
                this.warn("Attempted to remove component that's not in same xeogl.Scene: '" + componentId + "'");
                return;
            }

            delete this.components[componentId];

            // Unsubscribe from component destruction

            component.off(this._onDestroyed[componentId]);
            delete this._onDestroyed[componentId];

            // Unregister component for its type

            var types = this.types[component.type];

            if (types) {
                delete types[component.id];
            }

            this.numComponents--;

            //

            if (component.worldBoundary) {
                component.worldBoundary.off(this._onWorldBoundaryUpdated[component.id]);
                delete this._onWorldBoundaryUpdated[component.id];
            }

            if (!this._aabbDirty) {
                this._setAABBDirty();
            }

            /**
             * Fired whenever an individual {{#crossLink "Component"}}{{/crossLink}} is removed from this {{#crossLink "Model"}}{{/crossLink}}.
             * @event removed
             * @param value {Component} The {{#crossLink "Component"}}{{/crossLink}} that was removed.
             */
            this.fire("removed", component);

            if (!this._dirty) {
                this._scheduleUpdate();
            }
        },

        /**
         * Iterates with a callback over the {{#crossLink "Component"}}Components{{/crossLink}} in this Model.
         *
         * @method iterate
         * @param {Function} callback Callback called for each {{#crossLink "Component"}}{{/crossLink}}.
         * @param {Object} [scope=this] Optional scope for the callback, defaults to this Model.
         */
        iterate: function (callback, scope) {
            scope = scope || this;
            var components = this.components;
            for (var componentId in components) {
                if (components.hasOwnProperty(componentId)) {
                    callback.call(scope, components[componentId]);
                }
            }
        },

        _props: {

            /**
             * The Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} attached to this Model.
             *
             * Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this Model.
             *
             * Internally, the given {{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most
             * {{#crossLink "Transform"}}Transform{{/crossLink}} that the Model attaches to
             * its {{#crossLink "Entity"}}Entities{{/crossLink}}.
             *
             * Fires an {{#crossLink "Model/transform:event"}}{{/crossLink}} event on change.
             *
             * @property transform
             * @type Transform
             */
            transform: {

                set: function (value) {

                    /**
                     * Fired whenever this Model's {{#crossLink "Model/transform:property"}}{{/crossLink}} property changes.
                     *
                     * @event transform
                     * @param value The property's new value
                     */
                    this._attach({
                        name: "transform",
                        type: "xeogl.Transform",
                        component: value,
                        sceneDefault: false,
                        onAttached: {
                            callback: this._transformUpdated,
                            scope: this
                        }
                    });
                },

                get: function () {
                    return this._attached.transform;
                }
            },

            /**
             * World-space 3D boundary enclosing all the components in this Model.
             *
             * If you call {{#crossLink "Component/destroy:method"}}{{/crossLink}} on this boundary, then
             * this property will be assigned to a fresh {{#crossLink "Boundary3D"}}{{/crossLink}} instance next
             * time you reference it.
             *
             * @property worldBoundary
             * @type Boundary3D
             * @final
             */
            worldBoundary: {

                get: function () {

                    if (!this._worldBoundary) {

                        var self = this;

                        this._worldBoundary = this.create({

                            type: "xeogl.Boundary3D",

                            getDirty: function () {
                                if (self._aabbDirty) {
                                    self._buildAABB();
                                    self._aabbDirty = false;
                                    return true;
                                }
                                return false;
                            },

                            getAABB: function () {
                                return self._aabb;
                            }
                        });

                        this._worldBoundary.on("destroyed",
                            function () {
                                self._worldBoundary = null;
                            });
                    }

                    return this._worldBoundary;
                }
            }
        },

        _transformUpdated: function (transform) {
            this._dummyRootTransform.parent = transform;
        },

        _updated: function () {
            if (!this._aabbDirty) {
                this._setAABBDirty();
            }
        },

        _setAABBDirty: function () {
            this._aabbDirty = true;
            if (this._worldBoundary) {
                this._worldBoundary.fire("updated", true);
            }
        },

        _buildAABB: function () {

            if (!this._aabb) {
                this._aabb = xeogl.math.AABB3();
            }

            var xmin = 100000;
            var ymin = 100000;
            var zmin = 100000;
            var xmax = -100000;
            var ymax = -100000;
            var zmax = -100000;

            var component;
            var worldBoundary;
            var aabb;

            var components = this.components;

            for (var componentId in components) {
                if (components.hasOwnProperty(componentId)) {

                    component = components[componentId];

                    worldBoundary = component.worldBoundary;

                    if (worldBoundary) {

                        aabb = worldBoundary.aabb;

                        if (aabb[0] < xmin) {
                            xmin = aabb[0];
                        }

                        if (aabb[1] < ymin) {
                            ymin = aabb[1];
                        }

                        if (aabb[2] < zmin) {
                            zmin = aabb[2];
                        }

                        if (aabb[3] > xmax) {
                            xmax = aabb[3];
                        }

                        if (aabb[4] > ymax) {
                            ymax = aabb[4];
                        }

                        if (aabb[5] > zmax) {
                            zmax = aabb[5];
                        }
                    }
                }
            }

            this._aabb[0] = xmin;
            this._aabb[1] = ymin;
            this._aabb[2] = zmin;
            this._aabb[3] = xmax;
            this._aabb[4] = ymax;
            this._aabb[5] = zmax;
        },

        _destroy: function () {
            this.removeAll();
        }
    });

})();<|endoftext|>


<commit_before>/* TODO list
isValid("") --> false
isValid(null) --> false
isValid() --> false
isValid("1Ag_") --> false
isValid("123456Je") --> false
isValid("123456y_") --> false
isValid("_JYhgyQ") --> false
isValid("_JYKR23Q") --> false
isValid("123456yQ_") --> true
*/

describe("Entering a password", function(){
  it("should be false when empty string", function(){
    expect(isValid("")).toBeFalsy();
  });
  it("should be false when Null", function(){
    expect(isValid(null)).toBeFalsy();
  });
  it("should be false when nothing passed", function(){
    expect(isValid()).toBeFalsy();
  });
  it("should be false if password is too short", function(){
    expect(isValid("1Ag_")).toBeFalsy();
  });
});<commit_msg>fail underscore<commit_after>/* TODO list
isValid("") --> false
isValid(null) --> false
isValid() --> false
isValid("1Ag_") --> false
isValid("123456Je") --> false
isValid("123456y_") --> false
isValid("_JYhgyQ") --> false
isValid("_JYKR23Q") --> false
isValid("123456yQ_") --> true
*/

describe("Entering a password", function(){
  it("should be false when empty string", function(){
    expect(isValid("")).toBeFalsy();
  });
  it("should be false when Null", function(){
    expect(isValid(null)).toBeFalsy();
  });
  it("should be false when nothing passed", function(){
    expect(isValid()).toBeFalsy();
  });
  it("should be false if password is too short", function(){
    expect(isValid("1Ag_")).toBeFalsy();
  });it("should be false if no underscore", function(){
    expect(isValid("123456Je")).toBeFalsy();
  });
});<|endoftext|>


<commit_before>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/mixIn',
    'mout/array/find',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, mixIn, find, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String} name     The state name
     * @param {Object} [params] The state params
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params) {
        var state;

        // Resolve the state
        state = this._resolveFullState(name);
        mixIn(state.params, params);

        return stateRegistry.generateUrl(state.fullName, state.params);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var stateMeta,
            state;

        // Resolve the state
        stateMeta = this._resolveFullState(name);
        mixIn(stateMeta.params, params);

        // If the state is absolute, simply set it on the state registry
        if (stateMeta.name == null) {
            stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options);
            return this;
        }

        // Check if the state is globally registered
        if (stateRegistry.isRegistered(stateMeta.fullName)) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(stateMeta.name);
        } else {
            state = stateRegistry._createStateInstance(stateMeta.name, stateMeta.params);
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name;

        // Assume app state if not passed
        if (!state) {
            state = stateRegistry.getCurrent();
        }

        state = state && (state.$info ? state.$info.newState : state);
        name = state && state.getName() || this._defaultState;

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        if (stateRegistry.getCurrent() === state) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    // --------------------------------------------

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states;

        this._states = {};
        this._nrStates = size(this._states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = {};

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = {};
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }

        // Process the default state
        if (has('debug') && this._defaultState && !this._states[this._defaultState]) {
            throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
        }
    };

    /**
     * Resolves a full state name.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * Otherwise the full state name will be resolved from the local name.
     *
     * @param {String} [name] The state name
     *
     * @return {Object} The full state name and params
     */
    Controller.prototype._resolveFullState = function (name) {
        var state,
            ancestor,
            ancestorState;

        name = name || '';

        // Absolute
        if (name.charAt(0) === '/') {
            return {
                fullName: name.substr(1),
                params: {}
            };
        }

        // Relative
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '".');
            }

            state = this._uplink._resolveFullState(name.substr(3));
            delete state.name;  // Remove name because state is not local

            return state;
        }

        state = {
            name: name,
            fullName: name,
            params: {}
        };

        // Local
        ancestor = this._uplink;
        while (ancestor && ancestor instanceof Controller) {
            ancestorState = ancestor.getState();
            if (!ancestorState) {
                // Break here, the ancestor is not in any state
                break;
            }

            // Concatenate name & mix in relevant params
            state.fullName = ancestorState.getName() + (state.fullName ? '.' + state.fullName : '');
            mixIn(state.params, ancestor._currentStateParams);

            ancestor = ancestor._uplink;
        }

        // Ensure names
        state.name = state.name || this._defaultState || '';
        state.fullName = state.fullName || this._defaultState || '';

        return state;
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state The state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state) {
        var stateMeta;

        if (!this._currentState) {
            return false;
        }

        // Translate to default state if name is empty
        if (!state.getName()) {
            state = state.clone();
            state.setFullName(state.getFullName() + '.' + this._defaultState);
        }

        stateMeta = this._states[state.getName()];

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return this._currentState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var name,
            fullName,
            stateMeta;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();

        // Resolve to default state always
        if (!state.getName() && this._defaultState) {
            fullName = state.getFullName() ? state.getFullName() + '.' + this._defaultState : this._defaultState;
            this._currentState.setFullName(fullName);

            // Update also the state registry one
            if (state === stateRegistry.getCurrent()) {
                state.setFullName(fullName);
            }
        }

        name = this._currentState.getName();
        stateMeta = this._states[name];

        // Update state params being used by this controller
        this._currentStateParams = pick(this._currentState.getParams(), stateMeta.params);
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        length = this._downlinks.length;

        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (curr instanceof Controller) {
                if (curr._states[name] || (!name && curr._defaultState)) {
                    curr.delegateState(state);
                    return;
                }
            }
        }

        if (name && has('debug')) {
            console.warn('No child controller of "' + this.$name + '" declared the "' + name + '" state.');
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    // --------------------------------------------

    // Instruct the extend to merge states
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    return Controller;
});
<commit_msg>Fix number of states not being correctly counted.<commit_after>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/mixIn',
    'mout/array/find',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, mixIn, find, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String} name     The state name
     * @param {Object} [params] The state params
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params) {
        var state;

        // Resolve the state
        state = this._resolveFullState(name);
        mixIn(state.params, params);

        return stateRegistry.generateUrl(state.fullName, state.params);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var stateMeta,
            state;

        // Resolve the state
        stateMeta = this._resolveFullState(name);
        mixIn(stateMeta.params, params);

        // If the state is absolute, simply set it on the state registry
        if (stateMeta.name == null) {
            stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options);
            return this;
        }

        // Check if the state is globally registered
        if (stateRegistry.isRegistered(stateMeta.fullName)) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(stateMeta.name);
        } else {
            state = stateRegistry._createStateInstance(stateMeta.name, stateMeta.params);
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name;

        // Assume app state if not passed
        if (!state) {
            state = stateRegistry.getCurrent();
        }

        state = state && (state.$info ? state.$info.newState : state);
        name = state && state.getName() || this._defaultState;

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        if (stateRegistry.getCurrent() === state) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    // --------------------------------------------

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states;

        this._states = {};
        this._nrStates = size(states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = {};

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = {};
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }

        // Process the default state
        if (has('debug') && this._defaultState && !this._states[this._defaultState]) {
            throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
        }
    };

    /**
     * Resolves a full state name.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * Otherwise the full state name will be resolved from the local name.
     *
     * @param {String} [name] The state name
     *
     * @return {Object} The full state name and params
     */
    Controller.prototype._resolveFullState = function (name) {
        var state,
            ancestor,
            ancestorState;

        name = name || '';

        // Absolute
        if (name.charAt(0) === '/') {
            return {
                fullName: name.substr(1),
                params: {}
            };
        }

        // Relative
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '".');
            }

            state = this._uplink._resolveFullState(name.substr(3));
            delete state.name;  // Remove name because state is not local

            return state;
        }

        state = {
            name: name,
            fullName: name,
            params: {}
        };

        // Local
        ancestor = this._uplink;
        while (ancestor && ancestor instanceof Controller) {
            ancestorState = ancestor.getState();
            if (!ancestorState) {
                // Break here, the ancestor is not in any state
                break;
            }

            // Concatenate name & mix in relevant params
            state.fullName = ancestorState.getName() + (state.fullName ? '.' + state.fullName : '');
            mixIn(state.params, ancestor._currentStateParams);

            ancestor = ancestor._uplink;
        }

        // Ensure names
        state.name = state.name || this._defaultState || '';
        state.fullName = state.fullName || this._defaultState || '';

        return state;
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state The state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state) {
        var stateMeta;

        if (!this._currentState) {
            return false;
        }

        // Translate to default state if name is empty
        if (!state.getName()) {
            state = state.clone();
            state.setFullName(state.getFullName() + '.' + this._defaultState);
        }

        stateMeta = this._states[state.getName()];

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return this._currentState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var name,
            fullName,
            stateMeta;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();

        // Resolve to default state always
        if (!state.getName() && this._defaultState) {
            fullName = state.getFullName() ? state.getFullName() + '.' + this._defaultState : this._defaultState;
            this._currentState.setFullName(fullName);

            // Update also the state registry one
            if (state === stateRegistry.getCurrent()) {
                state.setFullName(fullName);
            }
        }

        name = this._currentState.getName();
        stateMeta = this._states[name];

        // Update state params being used by this controller
        this._currentStateParams = pick(this._currentState.getParams(), stateMeta.params);
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        length = this._downlinks.length;

        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (curr instanceof Controller) {
                if (curr._states[name] || (!name && curr._defaultState)) {
                    curr.delegateState(state);
                    return;
                }
            }
        }

        if (name && has('debug')) {
            console.warn('No child controller of "' + this.$name + '" declared the "' + name + '" state.');
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    // --------------------------------------------

    // Instruct the extend to merge states
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    return Controller;
});
<|endoftext|>


<commit_before>class CalendarTiler {
	static isString(value) {
            return typeof value === 'string' || value instanceof String;
        }

        static sortAppointments(a, b) {
            return a.start - b.start || b.end - a.end;
        }

        static initializeCollisionSubArray(initialValue, length) {
            let i,
                array = [];

            for (i = length - 1; i >= 0; --i) {
                array.push(typeof initialValue !== 'undefined' ? initialValue : []);
            }

            return array;
        }

        static getTail(head, array) {
            let i;

            for (i = head + 1; i < array.length; ++i) {
                if (array[i] !== -1) {
                    return i;
                }
            }

            return array.length;
        }

        static getFirstIndexOf(val, array) {
            let i;

            for (i = 0; i < array.length; ++i) {
                if (array[i] === val) {
                    return i;
                }
            }

            return -1;
        }

        constructor(appointments, tileParameters) {
            if (!Array.isArray(appointments)) {
                throw 'CalendarTiler constructor, 1st argument - appointments: expects array.';
            }

            if (tileParameters !== Object(tileParameters)) {
                throw 'CalendarTiler constructor, 2nd argument - tileParameters: expects object.';
            }

            this.tileParameters = this.mapTileParameters(tileParameters);
            this.appointments = this.copyRelevantAppointmentData(appointments);
            this.collisions = this.initializeCollisions();

            this.initialize();
        }

        mapTileParameters(tileParameters) {
            let usesDuration = this.isString(tileParameters.duration);

            return {
                usesDuration: usesDuration,
                id: this.isString(tileParameters.id) ? tileParameters.id : 'id',
                start: this.isString(tileParameters.start) ? tileParameters.start : 'start',
                delineator: usesDuration ? tileParameters.duration : (this.isString(tileParameters.end) ? tileParameters.end : 'end')
            };
        }

        getAppointmentEnd(appointment) {
            if (this.tileParameters.usesDuration) {
                return appointment[this.tileParameters.start] + appointment[this.tileParameters.duration];
            }

            return appointment[this.tileParameters.end];
        }

        copyRelevantAppointmentData(appointments) {
            let i,
                values = [];

            for (i = appointments.length - 1; i >= 0; --i) {
                values.push({
                    id: appointments[i][this.tileParameters.id],
                    start: appointments[i][this.tileParameters.start],
                    end: this.getAppointmentEnd(appointments[i])
                });
            }

            return values.sort(this.sortAppointments);
        }

        initializeCollisions() {
            let appointmentsLength = this.appointments.length;

            return {
                front: this.initializeCollisionSubArray(appointmentsLength),
                back: this.initializeCollisionSubArray(appointmentsLength),
                tBack: this.initializeCollisionSubArray(appointmentsLength, -1),
                tFront: this.initializeCollisionSubArray(appointmentsLength),
                dx: this.initializeCollisionSubArray(appointmentsLength, 1),
                x: this.initializeCollisionSubArray(appointmentsLength, 0)
            };
        }

        initialize() {
            let j,
                k;

            if (this.appointments.length > 0) {
                for (j = 0; j < this.appointments.length; ++j) {
                    for (k = j + 1; k < this.appointments.length; ++k) {
                        if (this.appointments[k].start < this.appointments[j].end) {
                            this.collisions.front[j].push(k);
                        }
                    }

                    for (k = 0; k < j; ++k) {
                        if (this.appointments[k].end > this.appointments[j].start) {
                            this.collisions.back[j].push(k);
                        }
                    }
                }

                this.buildTBack();
            }

            return {
                dx: this.collisions.dx,
                x: this.collisions.x
            };
        }

        buildTBack() {
            let next,
                head,
                tail,
                path;

            while (this.getFirstIndexOf(-1, this.collisions.tBack) > -1) {
                head = this.getFirstIndexOf(-1, this.collisions.tBack);
                tail = this.getTail(head, this.collisions.tBack);
                path = (head > 0 ? this.collisions.tBack[head - 1].concat([head - 1]) : []);

                while (head < tail) {
                    this.collisions.tBack[head] = path;
                    next = this.collisions.front[head][this.collisions.front[head].length - 1];
                    head = (next ? (next >= tail ? tail : next + 1) : head + 1);
                }
            }

            this.buildTFront();
        }

        buildTFront() {
            let i,
                next;

            for (i = 0; i < this.collisions.tFront.length; ++i) {
                if (this.collisions.back[i].length === this.collisions.tBack[i].length) {
                    if (this.collisions.front[i].length > 0
                            && this.collisions.tBack[this.collisions.front[i][0]].length > this.collisions.tBack[i].length) {
                        this.collisions.tFront[i].push(this.collisions.front[i][0]);
                    }
                } else {
                    next = this.findNextTFront(i, this.collisions);

                    if (this.collisions.front[i].length > 0
                            && this.getFirstIndexOf(next, this.collisions.back[this.collisions.front[i][0]]) > -1) {
                        if (this.collisions.tBack[this.collisions.front[i][0]].length < this.collisions.tBack[i].length) {
                            this.collisions.tFront[i].push(next);
                        } else {
                            this.collisions.tFront[i].push(this.collisions.front[i][0]);
                        }
                    } else {
                        this.collisions.tFront[i].push(next);
                    }
                }
            }

            this.expandTFront();
        }

        findNextTFront(index) {
            let i,
                tBack = [],
                maxFront = -1,
                maxIndex = index;

            for (i = 0; i < this.collisions.back[index].length; ++i) {
                if (this.getFirstIndexOf(this.collisions.back[index][i], this.collisions.tBack[index]) === -1) {
                    tBack.push(this.collisions.back[index][i]);
                }
            }

            for (i = 0; i < tBack.length; ++i) {
                if (this.collisions.tFront[tBack[i]].length > maxFront) {
                    maxFront = this.collisions.tFront[tBack[i]].length;
                    maxIndex = tBack[i];
                }
            }

            return maxIndex;
        }

        expandTFront() {
            let i,
                next;

            for (i = 0; i < this.collisions.tFront.length; ++i) {
                if (this.collisions.tFront[i].length > 0) {
                    next = this.collisions.tFront[i].length > 0 ? this.collisions.tFront[this.collisions.tFront[i][0]][0] : null;

                    while (next) {
                        this.collisions.tFront[i].push(next);

                        if (this.collisions.tFront[next].length > 0) {
                            next = this.collisions.tFront[next][0];
                        } else {
                            next = null;
                        }
                    }
                }
            }

            this.calculateDxs();
        }

        calculateDxs() {
            let i,
                j,
                chain,
                width,
                unset;

            for (i = 0; i < this.collisions.tBack.length; ++i) {
                if (this.collisions.dx[i] === 1) {
                    if (this.collisions.tFront[i].length === 0) {
                        for (j = 0; j < this.collisions.tBack[i].length; ++j) {
                            this.collisions.dx[i] -= this.collisions.dx[this.collisions.tBack[i][j]];
                        }
                    } else {
                        chain = this.findLongestChainThroughNode(i);
                        unset = 0;
                        width = 0;

                        for (j = 0; j < chain.length; ++j) {
                            if (this.collisions.dx[chain[j]] < 1) {
                                width += this.collisions.dx[chain[j]];
                            } else {
                                ++unset;
                            }
                        }

                        this.collisions.dx[i] = (1 - width) / unset;
                    }
                }
            }

            this.calculateXs();
        }

        findLongestChainThroughNode(index) {
            let i,
                chain = this.collisions.tBack[index].concat([index]).concat(this.collisions.tFront[index]);

            for (i = index; i < this.collisions.front[index].length; ++i) {
                if ((this.getFirstIndexOf(index, this.collisions.tBack[i]) > -1 || this.getFirstIndexOf(index, this.collisions.tFront[i]) > -1)
                        && this.collisions.tBack[i].length + this.collisions.tFront[i].length + 1 > chain.length) {
                    chain = this.collisions.tBack[i].concat([i]).concat(this.collisions.tFront[i]);
                }
            }

            return chain;
        }

        calculateXs() {
            let i,
                last;

            for (i = 1; i < this.collisions.dx.length; ++i) {
                if (this.collisions.tBack[i].length > 0) {
                    last = this.collisions.tBack[i][this.collisions.tBack[i].length - 1];
                    this.collisions.x[i] = this.collisions.x[last] + this.collisions.dx[last];
                }
            }
        }
    }
<commit_msg>Fixing something that I forgot to finish/didin't fully think through/did wrong<commit_after>class CalendarTiler {
    static isString(value) {
        return typeof value === 'string' || value instanceof String;
    }

    static sortAppointments(a, b) {
        return a.start - b.start || b.end - a.end;
    }

    static initializeCollisionSubArray(initialValue, length) {
        let i,
                array = [];

            for (i = length - 1; i >= 0; --i) {
                array.push(typeof initialValue !== 'undefined' ? initialValue : []);
            }

            return array;
        }

        static getTail(head, array) {
            let i;

            for (i = head + 1; i < array.length; ++i) {
                if (array[i] !== -1) {
                    return i;
                }
            }

            return array.length;
        }

        static getFirstIndexOf(val, array) {
            let i;

            for (i = 0; i < array.length; ++i) {
                if (array[i] === val) {
                    return i;
                }
            }

            return -1;
        }

        constructor(appointments, tileParameters) {
            if (!Array.isArray(appointments)) {
                throw 'CalendarTiler constructor, 1st argument - appointments: expects array.';
            }

            if (tileParameters !== Object(tileParameters)) {
                throw 'CalendarTiler constructor, 2nd argument - tileParameters: expects object.';
            }

            this.tileParameters = this.mapTileParameters(tileParameters);
            this.appointments = this.copyRelevantAppointmentData(appointments);
            this.collisions = this.initializeCollisions();

            this.initialize();
        }

        mapTileParameters(tileParameters) {
            let usesDuration = this.isString(tileParameters.duration);

            return {
                usesDuration: usesDuration,
                id: this.isString(tileParameters.id) ? tileParameters.id : 'id',
                start: this.isString(tileParameters.start) ? tileParameters.start : 'start',
                delineator: usesDuration ? tileParameters.duration : (this.isString(tileParameters.end) ? tileParameters.end : 'end')
            };
        }

        getAppointmentEnd(appointment) {
            if (this.tileParameters.usesDuration) {
                return appointment[this.tileParameters.start] + appointment[this.tileParameters.duration];
            }

            return appointment[this.tileParameters.end];
        }

        copyRelevantAppointmentData(appointments) {
            let i,
                values = [];

            for (i = appointments.length - 1; i >= 0; --i) {
                values.push({
                    id: appointments[i][this.tileParameters.id],
                    start: appointments[i][this.tileParameters.start],
                    end: this.getAppointmentEnd(appointments[i])
                });
            }

            return values.sort(this.sortAppointments);
        }

        initializeCollisions() {
            let appointmentsLength = this.appointments.length;

            return {
                front: this.initializeCollisionSubArray(appointmentsLength),
                back: this.initializeCollisionSubArray(appointmentsLength),
                tBack: this.initializeCollisionSubArray(appointmentsLength, -1),
                tFront: this.initializeCollisionSubArray(appointmentsLength),
                dx: this.initializeCollisionSubArray(appointmentsLength, 1),
                x: this.initializeCollisionSubArray(appointmentsLength, 0)
            };
        }

        initialize() {
            let j,
                k;

            if (this.appointments.length > 0) {
                for (j = 0; j < this.appointments.length; ++j) {
                    for (k = j + 1; k < this.appointments.length; ++k) {
                        if (this.appointments[k].start < this.appointments[j].end) {
                            this.collisions.front[j].push(k);
                        }
                    }

                    for (k = 0; k < j; ++k) {
                        if (this.appointments[k].end > this.appointments[j].start) {
                            this.collisions.back[j].push(k);
                        }
                    }
                }

                this.buildTBack();
            }

            return {
                dx: this.collisions.dx,
                x: this.collisions.x
            };
        }

        buildTBack() {
            let next,
                head,
                tail,
                path;

            while (this.getFirstIndexOf(-1, this.collisions.tBack) > -1) {
                head = this.getFirstIndexOf(-1, this.collisions.tBack);
                tail = this.getTail(head, this.collisions.tBack);
                path = (head > 0 ? this.collisions.tBack[head - 1].concat([head - 1]) : []);

                while (head < tail) {
                    this.collisions.tBack[head] = path;
                    next = this.collisions.front[head][this.collisions.front[head].length - 1];
                    head = (next ? (next >= tail ? tail : next + 1) : head + 1);
                }
            }

            this.buildTFront();
        }

        buildTFront() {
            let i,
                next;

            for (i = 0; i < this.collisions.tFront.length; ++i) {
                if (this.collisions.back[i].length === this.collisions.tBack[i].length) {
                    if (this.collisions.front[i].length > 0
                            && this.collisions.tBack[this.collisions.front[i][0]].length > this.collisions.tBack[i].length) {
                        this.collisions.tFront[i].push(this.collisions.front[i][0]);
                    }
                } else {
                    next = this.findNextTFront(i, this.collisions);

                    if (this.collisions.front[i].length > 0
                            && this.getFirstIndexOf(next, this.collisions.back[this.collisions.front[i][0]]) > -1) {
                        if (this.collisions.tBack[this.collisions.front[i][0]].length < this.collisions.tBack[i].length) {
                            this.collisions.tFront[i].push(next);
                        } else {
                            this.collisions.tFront[i].push(this.collisions.front[i][0]);
                        }
                    } else {
                        this.collisions.tFront[i].push(next);
                    }
                }
            }

            this.expandTFront();
        }

        findNextTFront(index) {
            let i,
                tFront = [],
                minFront,
                minIndex;

            for (i = 0; i < collisions.back[index].length; ++i) {
                if (getFirstIndexOf(collisions.back[index][i], collisions.tBack[index]) === -1) {
                    tFront.push(collisions.back[index][i]);
                }
            }

            minIndex = tFront[0];
            minFront = collisions.tFront[minIndex][0];

            for (i = tFront.length - 1; i > 0; --i) {
                if (collisions.tFront[tFront[i]] === minFront) {
                    minFront = collisions.tFront[tFront[i]];
                    minIndex = tFront[i];
                }
            }

            return minIndex;
        }

        expandTFront() {
            let i,
                next;

            for (i = 0; i < this.collisions.tFront.length; ++i) {
                if (this.collisions.tFront[i].length > 0) {
                    next = this.collisions.tFront[i].length > 0 ? this.collisions.tFront[this.collisions.tFront[i][0]][0] : null;

                    while (next) {
                        this.collisions.tFront[i].push(next);

                        if (this.collisions.tFront[next].length > 0) {
                            next = this.collisions.tFront[next][0];
                        } else {
                            next = null;
                        }
                    }
                }
            }

            this.calculateDxs();
        }

        calculateDxs() {
            let i,
                j,
                chain,
                width,
                unset;

            for (i = 0; i < this.collisions.tBack.length; ++i) {
                if (this.collisions.dx[i] === 1) {
                    if (this.collisions.tFront[i].length === 0) {
                        for (j = 0; j < this.collisions.tBack[i].length; ++j) {
                            this.collisions.dx[i] -= this.collisions.dx[this.collisions.tBack[i][j]];
                        }
                    } else {
                        chain = this.findLongestChainThroughNode(i);
                        unset = 0;
                        width = 0;

                        for (j = 0; j < chain.length; ++j) {
                            if (this.collisions.dx[chain[j]] < 1) {
                                width += this.collisions.dx[chain[j]];
                            } else {
                                ++unset;
                            }
                        }

                        this.collisions.dx[i] = (1 - width) / unset;
                    }
                }
            }

            this.calculateXs();
        }

        findLongestChainThroughNode(index) {
            let i,
                chain = this.collisions.tBack[index].concat([index]).concat(this.collisions.tFront[index]);

            for (i = index; i < this.collisions.front[index].length; ++i) {
                if ((this.getFirstIndexOf(index, this.collisions.tBack[i]) > -1 || this.getFirstIndexOf(index, this.collisions.tFront[i]) > -1)
                        && this.collisions.tBack[i].length + this.collisions.tFront[i].length + 1 > chain.length) {
                    chain = this.collisions.tBack[i].concat([i]).concat(this.collisions.tFront[i]);
                }
            }

            return chain;
        }

        calculateXs() {
            let i,
                last;

            for (i = 1; i < this.collisions.dx.length; ++i) {
                if (this.collisions.tBack[i].length > 0) {
                    last = this.collisions.tBack[i][this.collisions.tBack[i].length - 1];
                    this.collisions.x[i] = this.collisions.x[last] + this.collisions.dx[last];
                }
            }
        }
    }
<|endoftext|>