<commit_before>const _ = require('lodash');
const Promise = require('bluebird');
const {i18n} = require('../../lib/common');
const logging = require('../../../shared/logging');
const db = require('../db');
const schema = require('./schema');
const clients = require('./clients');

function addTableColumn(tableName, table, columnName, columnSpec = schema[tableName][columnName]) {
    let column;

    // creation distinguishes between text with fieldtype, string with maxlength and all others
    if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
        column = table[columnSpec.type](columnName, columnSpec.fieldtype);
    } else if (columnSpec.type === 'string') {
        if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
            column = table[columnSpec.type](columnName, columnSpec.maxlength);
        } else {
            column = table[columnSpec.type](columnName, 191);
        }
    } else {
        column = table[columnSpec.type](columnName);
    }

    if (Object.prototype.hasOwnProperty.call(columnSpec, 'nullable') && columnSpec.nullable === true) {
        column.nullable();
    } else {
        column.nullable(false);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'primary') && columnSpec.primary === true) {
        column.primary();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unique') && columnSpec.unique) {
        column.unique();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unsigned') && columnSpec.unsigned) {
        column.unsigned();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
        // check if table exists?
        column.references(columnSpec.references);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'cascadeDelete') && columnSpec.cascadeDelete === true) {
        column.onDelete('CASCADE');
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'defaultTo')) {
        column.defaultTo(columnSpec.defaultTo);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'index') && columnSpec.index === true) {
        column.index();
    }
}

function addColumn(tableName, column, transaction, columnSpec) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        addTableColumn(tableName, table, column, columnSpec);
    });
}

function dropColumn(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropColumn(column);
    });
}

/**
 * Checks if unique index exists in a table over the given columns.
 *
 * @param {string} tableName - name of the table to add unique constraint to
 * @param {string|[string]} columns - column(s) to form unique constraint with
 * @param {Object} transaction - connnection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function hasUnique(tableName, columns, transaction) {
    const knex = (transaction || db.knex);
    const client = knex.client.config.client;
    const columnNames = _.isArray(columns) ? columns.join('_') : columns;
    const constraintName = `${tableName}_${columnNames}_unique`;

    if (client === 'mysql') {
        const dbName = knex.client.config.connection.database;
        const [rawConstraints] = await knex.raw(`
                SELECT CONSTRAINT_NAME
                FROM information_schema.TABLE_CONSTRAINTS
                WHERE 1=1
                AND CONSTRAINT_SCHEMA=:dbName
                AND TABLE_NAME=:tableName
                AND CONSTRAINT_TYPE='UNIQUE'`, {dbName, tableName});
        const dbConstraints = rawConstraints.map(c => c.CONSTRAINT_NAME);

        if (dbConstraints.includes(constraintName)) {
            return true;
        }
    } else {
        const rawConstraints = await knex.raw(`PRAGMA index_list('${tableName}');`);
        const dbConstraints = rawConstraints.map(c => c.name);

        if (dbConstraints.includes(constraintName)) {
            return true;
        }
    }

    return false;
}

/**
 * Adds an unique index to a table over the given columns.
 *
 * @param {string} tableName - name of the table to add unique constraint to
 * @param {string|[string]} columns - column(s) to form unique constraint with
 * @param {Object} transaction - connnection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function addUnique(tableName, columns, transaction) {
    const hasUniqueConstraint = await hasUnique(tableName, columns, transaction);

    if (!hasUniqueConstraint) {
        logging.info(`Adding unique constraint for: ${columns} in table ${tableName}`);
        return (transaction || db.knex).schema.table(tableName, function (table) {
            table.unique(columns);
        });
    } else {
        logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
    }
}

/**
 * Drops a unique key constraint from a table.
 *
 * @param {string} tableName - name of the table to drop unique constraint from
 * @param {string|[string]} columns - column(s) unique constraint was formed
 * @param {Object} transaction - connnection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function dropUnique(tableName, columns, transaction) {
    const hasUniqueConstraint = await hasUnique(tableName, columns, transaction);

    if (hasUniqueConstraint) {
        logging.info(`Dropping unique constraint for: ${columns} in table: ${tableName}`);
        return (transaction || db.knex).schema.table(tableName, function (table) {
            table.dropUnique(columns);
        });
    } else {
        logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
    }
}

/**
 * https://github.com/tgriesser/knex/issues/1303
 * createTableIfNotExists can throw error if indexes are already in place
 */
function createTable(table, transaction, tableSpec = schema[table]) {
    return (transaction || db.knex).schema.hasTable(table)
        .then(function (exists) {
            if (exists) {
                return;
            }

            return (transaction || db.knex).schema.createTable(table, function (t) {
                Object.keys(tableSpec)
                    .filter(column => !(column.startsWith('@@')))
                    .forEach(column => addTableColumn(table, t, column, tableSpec[column]));

                if (tableSpec['@@INDEXES@@']) {
                    tableSpec['@@INDEXES@@'].forEach(index => t.index(index));
                }
                if (tableSpec['@@UNIQUE_CONSTRAINTS@@']) {
                    tableSpec['@@UNIQUE_CONSTRAINTS@@'].forEach(unique => t.unique(unique));
                }
            });
        });
}

function deleteTable(table, transaction) {
    return (transaction || db.knex).schema.dropTableIfExists(table);
}

function getTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getTables(transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getIndexes(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getIndexes(table, transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getColumns(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getColumns(table);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function checkTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (client === 'mysql') {
        return clients[client].checkPostTable();
    }
}

function createColumnMigration(...migrations) {
    async function runColumnMigration(conn, migration) {
        const {
            table,
            column,
            dbIsInCorrectState,
            operation,
            operationVerb,
            columnDefinition
        } = migration;

        const hasColumn = await conn.schema.hasColumn(table, column);
        const isInCorrectState = dbIsInCorrectState(hasColumn);

        if (isInCorrectState) {
            logging.warn(`${operationVerb} ${table}.${column} column - skipping as table is correct`);
        } else {
            logging.info(`${operationVerb} ${table}.${column} column`);
            await operation(table, column, conn, columnDefinition);
        }
    }

    return async function columnMigration(conn) {
        for (const migration of migrations) {
            await runColumnMigration(conn, migration);
        }
    };
}

module.exports = {
    checkTables: checkTables,
    createTable: createTable,
    deleteTable: deleteTable,
    getTables: getTables,
    getIndexes: getIndexes,
    addUnique: addUnique,
    dropUnique: dropUnique,
    addColumn: addColumn,
    dropColumn: dropColumn,
    getColumns: getColumns,
    createColumnMigration
};
<commit_msg>Fixed typos connnection -> connection<commit_after>const _ = require('lodash');
const Promise = require('bluebird');
const {i18n} = require('../../lib/common');
const logging = require('../../../shared/logging');
const db = require('../db');
const schema = require('./schema');
const clients = require('./clients');

function addTableColumn(tableName, table, columnName, columnSpec = schema[tableName][columnName]) {
    let column;

    // creation distinguishes between text with fieldtype, string with maxlength and all others
    if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
        column = table[columnSpec.type](columnName, columnSpec.fieldtype);
    } else if (columnSpec.type === 'string') {
        if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
            column = table[columnSpec.type](columnName, columnSpec.maxlength);
        } else {
            column = table[columnSpec.type](columnName, 191);
        }
    } else {
        column = table[columnSpec.type](columnName);
    }

    if (Object.prototype.hasOwnProperty.call(columnSpec, 'nullable') && columnSpec.nullable === true) {
        column.nullable();
    } else {
        column.nullable(false);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'primary') && columnSpec.primary === true) {
        column.primary();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unique') && columnSpec.unique) {
        column.unique();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unsigned') && columnSpec.unsigned) {
        column.unsigned();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
        // check if table exists?
        column.references(columnSpec.references);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'cascadeDelete') && columnSpec.cascadeDelete === true) {
        column.onDelete('CASCADE');
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'defaultTo')) {
        column.defaultTo(columnSpec.defaultTo);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'index') && columnSpec.index === true) {
        column.index();
    }
}

function addColumn(tableName, column, transaction, columnSpec) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        addTableColumn(tableName, table, column, columnSpec);
    });
}

function dropColumn(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropColumn(column);
    });
}

/**
 * Checks if unique index exists in a table over the given columns.
 *
 * @param {string} tableName - name of the table to add unique constraint to
 * @param {string|[string]} columns - column(s) to form unique constraint with
 * @param {Object} transaction - connection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function hasUnique(tableName, columns, transaction) {
    const knex = (transaction || db.knex);
    const client = knex.client.config.client;
    const columnNames = _.isArray(columns) ? columns.join('_') : columns;
    const constraintName = `${tableName}_${columnNames}_unique`;

    if (client === 'mysql') {
        const dbName = knex.client.config.connection.database;
        const [rawConstraints] = await knex.raw(`
                SELECT CONSTRAINT_NAME
                FROM information_schema.TABLE_CONSTRAINTS
                WHERE 1=1
                AND CONSTRAINT_SCHEMA=:dbName
                AND TABLE_NAME=:tableName
                AND CONSTRAINT_TYPE='UNIQUE'`, {dbName, tableName});
        const dbConstraints = rawConstraints.map(c => c.CONSTRAINT_NAME);

        if (dbConstraints.includes(constraintName)) {
            return true;
        }
    } else {
        const rawConstraints = await knex.raw(`PRAGMA index_list('${tableName}');`);
        const dbConstraints = rawConstraints.map(c => c.name);

        if (dbConstraints.includes(constraintName)) {
            return true;
        }
    }

    return false;
}

/**
 * Adds an unique index to a table over the given columns.
 *
 * @param {string} tableName - name of the table to add unique constraint to
 * @param {string|[string]} columns - column(s) to form unique constraint with
 * @param {Object} transaction - connection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function addUnique(tableName, columns, transaction) {
    const hasUniqueConstraint = await hasUnique(tableName, columns, transaction);

    if (!hasUniqueConstraint) {
        logging.info(`Adding unique constraint for: ${columns} in table ${tableName}`);
        return (transaction || db.knex).schema.table(tableName, function (table) {
            table.unique(columns);
        });
    } else {
        logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
    }
}

/**
 * Drops a unique key constraint from a table.
 *
 * @param {string} tableName - name of the table to drop unique constraint from
 * @param {string|[string]} columns - column(s) unique constraint was formed
 * @param {Object} transaction - connection object containing knex reference
 * @param {Object} transaction.knex - knex instance
 */
async function dropUnique(tableName, columns, transaction) {
    const hasUniqueConstraint = await hasUnique(tableName, columns, transaction);

    if (hasUniqueConstraint) {
        logging.info(`Dropping unique constraint for: ${columns} in table: ${tableName}`);
        return (transaction || db.knex).schema.table(tableName, function (table) {
            table.dropUnique(columns);
        });
    } else {
        logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
    }
}

/**
 * https://github.com/tgriesser/knex/issues/1303
 * createTableIfNotExists can throw error if indexes are already in place
 */
function createTable(table, transaction, tableSpec = schema[table]) {
    return (transaction || db.knex).schema.hasTable(table)
        .then(function (exists) {
            if (exists) {
                return;
            }

            return (transaction || db.knex).schema.createTable(table, function (t) {
                Object.keys(tableSpec)
                    .filter(column => !(column.startsWith('@@')))
                    .forEach(column => addTableColumn(table, t, column, tableSpec[column]));

                if (tableSpec['@@INDEXES@@']) {
                    tableSpec['@@INDEXES@@'].forEach(index => t.index(index));
                }
                if (tableSpec['@@UNIQUE_CONSTRAINTS@@']) {
                    tableSpec['@@UNIQUE_CONSTRAINTS@@'].forEach(unique => t.unique(unique));
                }
            });
        });
}

function deleteTable(table, transaction) {
    return (transaction || db.knex).schema.dropTableIfExists(table);
}

function getTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getTables(transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getIndexes(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getIndexes(table, transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getColumns(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getColumns(table);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function checkTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (client === 'mysql') {
        return clients[client].checkPostTable();
    }
}

function createColumnMigration(...migrations) {
    async function runColumnMigration(conn, migration) {
        const {
            table,
            column,
            dbIsInCorrectState,
            operation,
            operationVerb,
            columnDefinition
        } = migration;

        const hasColumn = await conn.schema.hasColumn(table, column);
        const isInCorrectState = dbIsInCorrectState(hasColumn);

        if (isInCorrectState) {
            logging.warn(`${operationVerb} ${table}.${column} column - skipping as table is correct`);
        } else {
            logging.info(`${operationVerb} ${table}.${column} column`);
            await operation(table, column, conn, columnDefinition);
        }
    }

    return async function columnMigration(conn) {
        for (const migration of migrations) {
            await runColumnMigration(conn, migration);
        }
    };
}

module.exports = {
    checkTables: checkTables,
    createTable: createTable,
    deleteTable: deleteTable,
    getTables: getTables,
    getIndexes: getIndexes,
    addUnique: addUnique,
    dropUnique: dropUnique,
    addColumn: addColumn,
    dropColumn: dropColumn,
    getColumns: getColumns,
    createColumnMigration
};
<|endoftext|>


<commit_before>const _ = require('lodash');
const Promise = require('bluebird');
const {i18n} = require('../../lib/common');
const logging = require('../../../shared/logging');
const db = require('../db');
const schema = require('./schema');
const clients = require('./clients');

function addTableColumn(tableName, table, columnName, columnSpec = schema[tableName][columnName]) {
    let column;

    // creation distinguishes between text with fieldtype, string with maxlength and all others
    if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
        column = table[columnSpec.type](columnName, columnSpec.fieldtype);
    } else if (columnSpec.type === 'string') {
        if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
            column = table[columnSpec.type](columnName, columnSpec.maxlength);
        } else {
            column = table[columnSpec.type](columnName, 191);
        }
    } else {
        column = table[columnSpec.type](columnName);
    }

    if (Object.prototype.hasOwnProperty.call(columnSpec, 'nullable') && columnSpec.nullable === true) {
        column.nullable();
    } else {
        column.nullable(false);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'primary') && columnSpec.primary === true) {
        column.primary();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unique') && columnSpec.unique) {
        column.unique();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unsigned') && columnSpec.unsigned) {
        column.unsigned();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
        // check if table exists?
        column.references(columnSpec.references);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'cascadeDelete') && columnSpec.cascadeDelete === true) {
        column.onDelete('CASCADE');
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'defaultTo')) {
        column.defaultTo(columnSpec.defaultTo);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'index') && columnSpec.index === true) {
        column.index();
    }
}

function addColumn(tableName, column, transaction, columnSpec) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        addTableColumn(tableName, table, column, columnSpec);
    });
}

function dropColumn(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropColumn(column);
    });
}

function addUnique(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.unique(column);
    });
}

function dropUnique(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropUnique(column);
    });
}

/**
 * https://github.com/tgriesser/knex/issues/1303
 * createTableIfNotExists can throw error if indexes are already in place
 */
function createTable(table, transaction) {
    return (transaction || db.knex).schema.hasTable(table)
        .then(function (exists) {
            if (exists) {
                return;
            }

            return (transaction || db.knex).schema.createTable(table, function (t) {
                let tableIndexes = [];

                const columnKeys = _.keys(schema[table]);
                _.each(columnKeys, function (column) {
                    if (column === '@@INDEXES@@') {
                        tableIndexes = schema[table]['@@INDEXES@@'];
                        return;
                    }

                    return addTableColumn(table, t, column);
                });

                _.each(tableIndexes, function (index) {
                    t.index(index);
                });
            });
        });
}

function deleteTable(table, transaction) {
    return (transaction || db.knex).schema.dropTableIfExists(table);
}

function getTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getTables(transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getIndexes(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getIndexes(table, transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getColumns(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getColumns(table);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function checkTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (client === 'mysql') {
        return clients[client].checkPostTable();
    }
}

const createLog = type => msg => logging[type](msg);

function createColumnMigration(...migrations) {
    async function runColumnMigration(conn, migration) {
        const {
            table,
            column,
            dbIsInCorrectState,
            operation,
            operationVerb,
            columnDefinition
        } = migration;

        const hasColumn = await conn.schema.hasColumn(table, column);
        const isInCorrectState = dbIsInCorrectState(hasColumn);

        const log = createLog(isInCorrectState ? 'warn' : 'info');

        log(`${operationVerb} ${table}.${column} column`);

        if (!isInCorrectState) {
            await operation(table, column, conn, columnDefinition);
        }
    }

    return async function columnMigration(conn) {
        for (const migration of migrations) {
            await runColumnMigration(conn, migration);
        }
    };
}

module.exports = {
    checkTables: checkTables,
    createTable: createTable,
    deleteTable: deleteTable,
    getTables: getTables,
    getIndexes: getIndexes,
    addUnique: addUnique,
    dropUnique: dropUnique,
    addColumn: addColumn,
    dropColumn: dropColumn,
    getColumns: getColumns,
    createColumnMigration
};
<commit_msg>Added ability to specific table definition to createTable<commit_after>const _ = require('lodash');
const Promise = require('bluebird');
const {i18n} = require('../../lib/common');
const logging = require('../../../shared/logging');
const db = require('../db');
const schema = require('./schema');
const clients = require('./clients');

function addTableColumn(tableName, table, columnName, columnSpec = schema[tableName][columnName]) {
    let column;

    // creation distinguishes between text with fieldtype, string with maxlength and all others
    if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
        column = table[columnSpec.type](columnName, columnSpec.fieldtype);
    } else if (columnSpec.type === 'string') {
        if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
            column = table[columnSpec.type](columnName, columnSpec.maxlength);
        } else {
            column = table[columnSpec.type](columnName, 191);
        }
    } else {
        column = table[columnSpec.type](columnName);
    }

    if (Object.prototype.hasOwnProperty.call(columnSpec, 'nullable') && columnSpec.nullable === true) {
        column.nullable();
    } else {
        column.nullable(false);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'primary') && columnSpec.primary === true) {
        column.primary();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unique') && columnSpec.unique) {
        column.unique();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'unsigned') && columnSpec.unsigned) {
        column.unsigned();
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
        // check if table exists?
        column.references(columnSpec.references);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'cascadeDelete') && columnSpec.cascadeDelete === true) {
        column.onDelete('CASCADE');
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'defaultTo')) {
        column.defaultTo(columnSpec.defaultTo);
    }
    if (Object.prototype.hasOwnProperty.call(columnSpec, 'index') && columnSpec.index === true) {
        column.index();
    }
}

function addColumn(tableName, column, transaction, columnSpec) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        addTableColumn(tableName, table, column, columnSpec);
    });
}

function dropColumn(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropColumn(column);
    });
}

function addUnique(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.unique(column);
    });
}

function dropUnique(tableName, column, transaction) {
    return (transaction || db.knex).schema.table(tableName, function (table) {
        table.dropUnique(column);
    });
}

/**
 * https://github.com/tgriesser/knex/issues/1303
 * createTableIfNotExists can throw error if indexes are already in place
 */
function createTable(table, transaction, tableSpec = schema[table]) {
    return (transaction || db.knex).schema.hasTable(table)
        .then(function (exists) {
            if (exists) {
                return;
            }

            return (transaction || db.knex).schema.createTable(table, function (t) {
                let tableIndexes = [];

                const columnKeys = _.keys(tableSpec);
                _.each(columnKeys, function (column) {
                    if (column === '@@INDEXES@@') {
                        tableIndexes = tableSpec['@@INDEXES@@'];
                        return;
                    }

                    return addTableColumn(table, t, column, tableSpec[column]);
                });

                _.each(tableIndexes, function (index) {
                    t.index(index);
                });
            });
        });
}

function deleteTable(table, transaction) {
    return (transaction || db.knex).schema.dropTableIfExists(table);
}

function getTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getTables(transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getIndexes(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getIndexes(table, transaction);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function getColumns(table, transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (_.includes(_.keys(clients), client)) {
        return clients[client].getColumns(table);
    }

    return Promise.reject(i18n.t('notices.data.utils.index.noSupportForDatabase', {client: client}));
}

function checkTables(transaction) {
    const client = (transaction || db.knex).client.config.client;

    if (client === 'mysql') {
        return clients[client].checkPostTable();
    }
}

const createLog = type => msg => logging[type](msg);

function createColumnMigration(...migrations) {
    async function runColumnMigration(conn, migration) {
        const {
            table,
            column,
            dbIsInCorrectState,
            operation,
            operationVerb,
            columnDefinition
        } = migration;

        const hasColumn = await conn.schema.hasColumn(table, column);
        const isInCorrectState = dbIsInCorrectState(hasColumn);

        const log = createLog(isInCorrectState ? 'warn' : 'info');

        log(`${operationVerb} ${table}.${column} column`);

        if (!isInCorrectState) {
            await operation(table, column, conn, columnDefinition);
        }
    }

    return async function columnMigration(conn) {
        for (const migration of migrations) {
            await runColumnMigration(conn, migration);
        }
    };
}

module.exports = {
    checkTables: checkTables,
    createTable: createTable,
    deleteTable: deleteTable,
    getTables: getTables,
    getIndexes: getIndexes,
    addUnique: addUnique,
    dropUnique: dropUnique,
    addColumn: addColumn,
    dropColumn: dropColumn,
    getColumns: getColumns,
    createColumnMigration
};
<|endoftext|>


<commit_before>/**
 * Created by Bryan Henderson on 4/16/2014.
 */
var Editors = angular.module("Editors",
    [
        'ngResource',
        'ui.router',
        'ui.bootstrap',
        'ngTable',
        'xeditable',
        'ui.tree',
        'ui.sortable',
        'ngAnimate',
        'textAngular',
        'restangular',
        'angularUtils.directives.uiBreadcrumbs',
        'EscreeningDashboardApp.services.battery',
        'EscreeningDashboardApp.services.survey',
        'EscreeningDashboardApp.services.surveypage',
        'EscreeningDashboardApp.services.surveysection',
        'EscreeningDashboardApp.services.question',
        'EscreeningDashboardApp.services.template',
        'EscreeningDashboardApp.services.templateType',
        'EscreeningDashboardApp.services.assessmentVariable',
        'EscreeningDashboardApp.services.question',
	    'EscreeningDashboardApp.services.MeasureService',
        'EscreeningDashboardApp.services.templateBlock',
        'EscreeningDashboardApp.filters.messages',
        'EscreeningDashboardApp.filters.freemarkerWhiteSpace',
        'EscreeningDashboardApp.filters.limitToWithEllipsis'
    ]);

/**
 * A generic confirmation for risky actions.
 * Usage: Add attributes: ng-really-message="Are you sure"? ng-really-click="takeAction()" function
 */
Editors.directive('ngReallyClick', [function() {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            element.bind('click', function() {
                var message = attrs.ngReallyMessage;
                var shouldSkip = Object.isDefined(attrs.ngReallySkipWhen)
                    && attrs.ngReallySkipWhen.length > 0
                    && scope.$apply(attrs.ngReallySkipWhen);

                if (shouldSkip || (message && confirm(message))) {
                    scope.$apply(attrs.ngReallyClick);
                }
            });
        }
    };
}]);

Editors.config(function(RestangularProvider, $provide) {

    $provide.decorator('taOptions', ['taRegisterTool', 'taCustomRenderers', 'taSelectableElements', 'textAngularManager', '$delegate', '$modal', 'TemplateBlockService',
                                     function(taRegisterTool, taCustomRenderers, taSelectableElements, textAngularManager, $delegate, $modal, TemplateBlockService){

	    $delegate.setup.textEditorSetup = function($element){
	        $element.attr('template-block-text-editor', '');
	    }

		// Register the custom addVariable tool with textAngular
	    // $delegate is the taOptions we are decorating
		taRegisterTool('insertVariable', {
			display: '<button title="Add Variable" class="btn btn-default"> Add Variable</button>',
			tooltiptext: 'Insert Assessment Variable',
			action: function(deferred, restoreSelection) {

				var addVariableTool = this;

		        deferred.promise.then(function(result){
		            addVariableTool.$editor().updateTaBindtaTextElement();

		            return addVariableTool.$editor().updateTaBindtaHtmlElement();
		        });

				var modalInstance = $modal.open({
					templateUrl: 'resources/editors/views/templates/assessmentvariablemodal.html',
					controller: ['$scope', '$modalInstance', 'AssessmentVariableService', function($scope, $modalInstance, AssessmentVariableService) {
                        $scope.assessmentVariables = AssessmentVariableService.getLastCachedResults();

						$scope.assessmentVariable = {};

						$scope.$watch('assessmentVariable.id', function(newValue, oldValue) {

							if (newValue !== oldValue && $scope.assessmentVariable && $scope.assessmentVariable.id) {
							    var embed = TemplateBlockService.createAVElement($scope.assessmentVariable.id, $scope.assessmentVariable.getName());

								$modalInstance.close(embed);
	                        }

                        }, true);

						$scope.cancel = function() {
						    $modalInstance.close("");
						};

					}]
				});

				modalInstance.result.then(function(embed) {
					var $taEl = $("div[id^='taTextElement']");

					$taEl.focus();
					$taEl.find(".rangySelectionBoundary").first().before($(embed));

					restoreSelection();

					deferred.resolve();

				});

				return false;

			}
		});
		// DO NOT add the button to the default toolbar definition, but if you did, this is how you would:
		//$delegate.toolbar[$delegate.toolbar.length] = ['insertVariable'];
		return $delegate;
	}]);
});

Editors.run(function(editableOptions) {
    editableOptions.theme = 'bs3';
});

Editors.run(['$rootScope', '$state', '$stateParams', '$modal', 'Restangular', function ($rootScope,   $state,   $stateParams,  $modal, Restangular) {
    Restangular.setErrorInterceptor(function(response, deferred, responseHandler) {

        if(Object.isDefined(response) && Object.isDefined(response.data)){
            if(Object.isArray(response.data.errorMessages)){
                response.data.errorMessages.forEach(function (errorMessage) {
                    $rootScope.addMessage(new BytePushers.models.Message({type: BytePushers.models.Message.ERROR, value: errorMessage.description}));
                });
            }
        }

        return true; // error not handled
    });
    // It's very handy to add references to $state and $stateParams to the $rootScope
    // so that you can access them from any scope within your applications.For example,
    //
 will set the

    // to active whenever 'assessments.list' or one of its descendents is active.
    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;

}]);
Editors.value('MessageHandler', new BytePushers.models.MessageHandler());
Editors.value('Template', new EScreeningDashboardApp.models.Template());
Editors.value('TemplateType', new EScreeningDashboardApp.models.TemplateType());
Editors.value('TemplateVariableContent', new EScreeningDashboardApp.models.TemplateVariableContent());
<commit_msg>#660 small change in white space.<commit_after>/**
 * Created by Bryan Henderson on 4/16/2014.
 */
var Editors = angular.module("Editors",
    [
        'ngResource',
        'ui.router',
        'ui.bootstrap',
        'ngTable',
        'xeditable',
        'ui.tree',
        'ui.sortable',
        'ngAnimate',
        'textAngular',
        'restangular',
        'angularUtils.directives.uiBreadcrumbs',
        'EscreeningDashboardApp.services.battery',
        'EscreeningDashboardApp.services.survey',
        'EscreeningDashboardApp.services.surveypage',
        'EscreeningDashboardApp.services.surveysection',
        'EscreeningDashboardApp.services.question',
        'EscreeningDashboardApp.services.template',
        'EscreeningDashboardApp.services.templateType',
        'EscreeningDashboardApp.services.assessmentVariable',
        'EscreeningDashboardApp.services.question',
        'EscreeningDashboardApp.services.MeasureService',
        'EscreeningDashboardApp.services.templateBlock',
        'EscreeningDashboardApp.filters.messages',
        'EscreeningDashboardApp.filters.freemarkerWhiteSpace',
        'EscreeningDashboardApp.filters.limitToWithEllipsis'
    ]);

/**
 * A generic confirmation for risky actions.
 * Usage: Add attributes: ng-really-message="Are you sure"? ng-really-click="takeAction()" function
 */
Editors.directive('ngReallyClick', [function() {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            element.bind('click', function() {
                var message = attrs.ngReallyMessage;
                var shouldSkip = Object.isDefined(attrs.ngReallySkipWhen)
                    && attrs.ngReallySkipWhen.length > 0
                    && scope.$apply(attrs.ngReallySkipWhen);

                if (shouldSkip || (message && confirm(message))) {
                    scope.$apply(attrs.ngReallyClick);
                }
            });
        }
    };
}]);

Editors.config(function(RestangularProvider, $provide) {

    $provide.decorator('taOptions', ['taRegisterTool', 'taCustomRenderers', 'taSelectableElements', 'textAngularManager', '$delegate', '$modal', 'TemplateBlockService',
                                     function(taRegisterTool, taCustomRenderers, taSelectableElements, textAngularManager, $delegate, $modal, TemplateBlockService){

	    $delegate.setup.textEditorSetup = function($element){
	        $element.attr('template-block-text-editor', '');
	    }

		// Register the custom addVariable tool with textAngular
	    // $delegate is the taOptions we are decorating
		taRegisterTool('insertVariable', {
			display: '<button title="Add Variable" class="btn btn-default"> Add Variable</button>',
			tooltiptext: 'Insert Assessment Variable',
			action: function(deferred, restoreSelection) {

				var addVariableTool = this;

		        deferred.promise.then(function(result){
		            addVariableTool.$editor().updateTaBindtaTextElement();

		            return addVariableTool.$editor().updateTaBindtaHtmlElement();
		        });

				var modalInstance = $modal.open({
					templateUrl: 'resources/editors/views/templates/assessmentvariablemodal.html',
					controller: ['$scope', '$modalInstance', 'AssessmentVariableService', function($scope, $modalInstance, AssessmentVariableService) {
                        $scope.assessmentVariables = AssessmentVariableService.getLastCachedResults();

						$scope.assessmentVariable = {};

						$scope.$watch('assessmentVariable.id', function(newValue, oldValue) {

							if (newValue !== oldValue && $scope.assessmentVariable && $scope.assessmentVariable.id) {
							    var embed = TemplateBlockService.createAVElement($scope.assessmentVariable.id, $scope.assessmentVariable.getName());

								$modalInstance.close(embed);
	                        }

                        }, true);

						$scope.cancel = function() {
						    $modalInstance.close("");
						};

					}]
				});

				modalInstance.result.then(function(embed) {
					var $taEl = $("div[id^='taTextElement']");

					$taEl.focus();
					$taEl.find(".rangySelectionBoundary").first().before($(embed));

					restoreSelection();

					deferred.resolve();

				});

				return false;

			}
		});
		// DO NOT add the button to the default toolbar definition, but if you did, this is how you would:
		//$delegate.toolbar[$delegate.toolbar.length] = ['insertVariable'];
		return $delegate;
	}]);
});

Editors.run(function(editableOptions) {
    editableOptions.theme = 'bs3';
});

Editors.run(['$rootScope', '$state', '$stateParams', '$modal', 'Restangular', function ($rootScope,   $state,   $stateParams,  $modal, Restangular) {
    Restangular.setErrorInterceptor(function(response, deferred, responseHandler) {

        if(Object.isDefined(response) && Object.isDefined(response.data)){
            if(Object.isArray(response.data.errorMessages)){
                response.data.errorMessages.forEach(function (errorMessage) {
                    $rootScope.addMessage(new BytePushers.models.Message({type: BytePushers.models.Message.ERROR, value: errorMessage.description}));
                });
            }
        }

        return true; // error not handled
    });
    // It's very handy to add references to $state and $stateParams to the $rootScope
    // so that you can access them from any scope within your applications.For example,
    //
 will set the

    // to active whenever 'assessments.list' or one of its descendents is active.
    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;

}]);
Editors.value('MessageHandler', new BytePushers.models.MessageHandler());
Editors.value('Template', new EScreeningDashboardApp.models.Template());
Editors.value('TemplateType', new EScreeningDashboardApp.models.TemplateType());
Editors.value('TemplateVariableContent', new EScreeningDashboardApp.models.TemplateVariableContent());
<|endoftext|>


<commit_before>"use strict";

var AnimationTimer = function(terrain) {
  var FRAMES_PER_SECONDS = 60;
  var MILLISECONDS_PER_TICK = 1000.0 / FRAMES_PER_SECONDS;

  var masterStartTime;
  var previousTickStartTime;
  var totalTicks = 0;

  var animator = new Animator(terrain);

  var tick = function(timestamp) {
    var tickStartTime = performance.now();

    if (!masterStartTime) {
      masterStartTime = tickStartTime;
    }

    var ticksToProcess = 1;
    var expectedFramesSoFar = Math.floor((tickStartTime - masterStartTime) / MILLISECONDS_PER_TICK);
    if (expectedFramesSoFar > totalTicks) {
      ticksToProcess += (expectedFramesSoFar - totalTicks);
    }

    for (var i = 0; i < ticksToProcess; i++) {
      animator.tick();
      totalTicks += 1;
    }

    renderer.render(scene, camera);

    previousTickStartTime = tickStartTime;

    requestAnimFrame(tick);
  };

  var animationTimer = {};

  animationTimer.start = function() {
    requestAnimFrame(tick);
  };

  return animationTimer;
};

var Animator = function(terrain) {
  var animator = {};

  var xPositionGenerator = new MotionGenerator();
  var zPositionGenerator = new LinearMotionGenerator(2, CityConfig.HALF_SCENE_DEPTH);

  animator.tick = function() {
    camera.position.x += xPositionGenerator.nextValue();
    camera.position.z += -zPositionGenerator.nextValue();

    var mapX = Coordinates.sceneXToMapX(camera.position.x);
    var mapZ = Coordinates.sceneZToMapZ(camera.position.z);

    var y = terrain.heightAtCoordinates(mapX, mapZ);
    camera.position.y = y + 0.5;
  };

  return animator;
};

var MotionGenerator = function() {
  var motionGenerator = {};

  motionGenerator.nextValue = function() {
    return 0.0;
  };

  motionGenerator.isComplete = function() {
    return false;
  };

  motionGenerator.onComplete = function() { };

  return motionGenerator;
};

var LinearMotionGenerator = function(delta, target) {
  var accumulator = 0.0;
  var isComplete = false;

  var linearMotionGenerator = new MotionGenerator();

  linearMotionGenerator.nextValue = function() {
    if (isComplete) {
      return 0.0;
    }

    accumulator += delta;

    if (accumulator <= target) {
      return delta;
    }
    else {
      isComplete = true;
      return (delta - (accumulator - target));
    }
  };

  linearMotionGenerator.isComplete = function() {
    return isComplete;
  };

  linearMotionGenerator.onComplete = function() { };

  return linearMotionGenerator;
};

var AnimationManager = function() {
  var TARGET_FRAME_WINDOW = 1000 / 60;   // 60 frames per second

  var animationManager = {};
  var animators = [];
  var previousFrameTimestamp;

  var deltaX = 0.0;
  var deltaZ = 0.2;
  var targetX;
  var targetGridX;
  var targetZ;
  var targetGridZ;
  var targetAngle = 0;
  var deltaAngle;

  var init = function() {
    var START_X = 0;
    var START_Y = 40;
    var START_Z = CityConfig.HALF_SCENE_DEPTH;

    camera.position.x = START_X;
    camera.position.y = START_Y;
    camera.position.z = START_Z;

    targetX = 0;
    targetZ = 0;
    deltaX = 0.0;
    deltaZ = -0.2;

    var framesUntilCityEdge = Math.abs(((CityConfig.HALF_TERRAIN_ROWS - CityConfig.HALF_BLOCK_ROWS) * CityConfig.BLOCK_AND_STREET_DEPTH) / deltaZ);
    var swoopDescentDelta = (START_Y - 0.5) / framesUntilCityEdge;

    var ramp = new rampAnimation(framesUntilCityEdge, -swoopDescentDelta, 0.5, 1000000);
    var forward = new forwardAnimation(targetX, deltaX, targetZ, deltaZ);
    animators = [ramp, forward];
  };

  var determineNextTargetPoint = function() {
    if (targetGridX == undefined || targetGridZ == undefined) {
      targetGridX = 0;
      targetGridZ = -1;
    }
    else {
      var oldTargetGridX = targetGridX;
      var oldTargetGridZ = targetGridZ;

      while (oldTargetGridX == targetGridX && oldTargetGridZ == targetGridZ) {
        if (deltaX == 0) {
          targetGridX = Math.floor(Math.random() * CityConfig.BLOCK_ROWS) - (CityConfig.BLOCK_ROWS / 2);
        }
        else if (deltaZ == 0) {
          targetGridZ = Math.floor(Math.random() * CityConfig.BLOCK_COLUMNS) - (CityConfig.BLOCK_COLUMNS / 2);
        }
      }
    }

    targetX = (targetGridX * CityConfig.BLOCK_WIDTH) + (targetGridX * CityConfig.STREET_WIDTH);
    targetZ = (targetGridZ * CityConfig.BLOCK_WIDTH) + (targetGridZ * CityConfig.STREET_WIDTH);

    deltaX = (camera.position.x == targetX) ? 0 : 0.2;
    deltaZ = (camera.position.z == targetZ) ? 0 : 0.2;
    deltaX *= (camera.position.x > targetX) ? -1 : 1;
    deltaZ *= (camera.position.z > targetZ) ? -1 : 1;
  };

  var determineRotationAngle = function() {
    var RIGHT_ANGLE = Math.PI / 2;
    var ROTATION_DELTA = 0.03;

    var oldTargetAngle = targetAngle;
    if (deltaX != 0 && oldTargetAngle == 0) {  // NORTH
      targetAngle = (deltaX < 0) ? targetAngle + RIGHT_ANGLE : targetAngle - RIGHT_ANGLE;
    }
    else if (deltaZ != 0 && (oldTargetAngle == RIGHT_ANGLE || oldTargetAngle == ((-3 * Math.PI) / 2))) {  // EAST
      targetAngle = (deltaZ < 0) ? targetAngle - RIGHT_ANGLE : targetAngle + RIGHT_ANGLE;
    }
    else if (deltaX != 0 && (oldTargetAngle == Math.PI || oldTargetAngle == Math.PI * -1)) {  // SOUTH
      targetAngle = (deltaX < 0) ? targetAngle - RIGHT_ANGLE : targetAngle + RIGHT_ANGLE;
    }
    else if (deltaZ != 0 && (oldTargetAngle == Math.PI / -2 || oldTargetAngle == (Math.PI * 3) / 2)) {  // WEST
      targetAngle = (deltaZ < 0) ? targetAngle + RIGHT_ANGLE : targetAngle - RIGHT_ANGLE;
    }

    deltaAngle = ROTATION_DELTA;
    deltaAngle *= (targetAngle > oldTargetAngle) ? 1 : -1;
  };

  animationManager.animate = function() {
    var currentTimestamp = new Date().getTime();
    var frameCount;

    if (animators.length === 0) {
      init();
    }

    if (previousFrameTimestamp === undefined) {
      frameCount = 1;
    }
    else {
      frameCount = Math.floor((currentTimestamp - previousFrameTimestamp) / TARGET_FRAME_WINDOW);
      if (frameCount < 1) {
        frameCount = 1;
      }
    }
    previousFrameTimestamp = currentTimestamp;

    var newAnimators = [];
    for (var i = 0; i < animators.length; i++) {
      animators[i].animate(frameCount);

      if (animators[i].finished === true) {
        if (animators[i] instanceof rampAnimation) {
          newAnimators.push(new hoverAnimation());
        }
        else if (animators[i] instanceof forwardAnimation) {
          determineNextTargetPoint();
          determineRotationAngle();
          newAnimators.push(new rotationAnimation(targetAngle, deltaAngle));
        }
        else if (animators[i] instanceof rotationAnimation) {
          newAnimators.push(new forwardAnimation(targetX, deltaX, targetZ, deltaZ));
        }
        else if (animators[i] instanceof hoverAnimation) {
          newAnimators.push(new hoverAnimation());
        }
      }
      else {
        newAnimators.push(animators[i]);
      }
    }
    animators = newAnimators;

    var mapX = Coordinates.sceneXToMapX(camera.position.x);
    var mapZ = Coordinates.sceneZToMapZ(camera.position.z);

    var y = terrain.heightAtCoordinates(mapX, mapZ);
    camera.position.y = Math.max(camera.position.y, y + 0.5);

    renderer.render(scene, camera);
    requestAnimFrame(animationManager.animate);
  };

  return animationManager;
};

function forwardAnimation(targetX, deltaX, targetZ, deltaZ) {
  this.targetX = targetX;
  this.deltaX = deltaX;
  this.targetZ = targetZ;
  this.deltaZ = deltaZ;
  this.finished = false;
}

forwardAnimation.prototype.animate = function(frameCount) {
  camera.position.x += this.deltaX * frameCount;
  camera.position.z += this.deltaZ * frameCount;

  if ((this.deltaX < 0 && camera.position.x < this.targetX) || (this.deltaX > 0 && camera.position.x > this.targetX) ||
      (this.deltaZ < 0 && camera.position.z < this.targetZ) || (this.deltaZ > 0 && camera.position.z > this.targetZ)) {
    camera.position.x = this.targetX;
    camera.position.z = this.targetZ;

    this.finished = true;
  }
}

function rotationAnimation(targetAngle, deltaAngle) {
  this.targetAngle = targetAngle;
  this.deltaAngle = deltaAngle;
  this.finished = false;
}

rotationAnimation.prototype.animate = function(frameCount) {
  camera.rotation.y += this.deltaAngle * frameCount;

  if ((this.deltaAngle < 0 && camera.rotation.y <= this.targetAngle) || (this.deltaAngle > 0 && camera.rotation.y >= this.targetAngle)) {
    if (this.targetAngle >= Math.PI * 2 || this.targetAngle <= Math.PI * -2) {
      this.targetAngle = 0;
    }

    camera.rotation.y = this.targetAngle;
    this.finished = true;
  }
}

function rampAnimation(frameDistance, deltaY, minHeight, maxHeight) {
  this.ticks = 0;
  this.frameDistance = frameDistance;
  this.deltaY = deltaY;
  this.minHeight = minHeight;
  this.maxHeight = maxHeight;
  this.finished = false;
}

rampAnimation.prototype.animate = function(frameCount) {
  if (camera.position.y >= this.minHeight && camera.position.y <= this.maxHeight) {
    camera.position.y += this.deltaY * frameCount;
  }
  if (camera.position.y < this.minHeight) {
    camera.position.y = this.minHeight;
  }
  if (camera.position.y > this.maxHeight) {
    camera.position.y = this.maxHeight;
  }

  this.ticks += frameCount;
  if (this.ticks > this.frameDistance) {
    this.finished = true;
  }
}

function hoverAnimation() {
  var frameDistance = (Math.random() * 300) + 300;
  var deltaY = (camera.position.y > 0.5) ? -0.05 : 0.05;

  this.rampAnimation = new rampAnimation(frameDistance, deltaY, 0.5, 15);
  this.finished = false;
}

hoverAnimation.prototype.animate = function(frameCount) {
  this.rampAnimation.animate(frameCount);
  this.finished = this.rampAnimation.finished;
}
<commit_msg>Removing confusing variable initialization<commit_after>"use strict";

var AnimationTimer = function(terrain) {
  var FRAMES_PER_SECONDS = 60;
  var MILLISECONDS_PER_TICK = 1000.0 / FRAMES_PER_SECONDS;

  var masterStartTime;
  var previousTickStartTime;
  var totalTicks = 0;

  var animator = new Animator(terrain);

  var tick = function(timestamp) {
    var tickStartTime = performance.now();

    if (!masterStartTime) {
      masterStartTime = tickStartTime;
    }

    var ticksToProcess = 1;
    var expectedFramesSoFar = Math.floor((tickStartTime - masterStartTime) / MILLISECONDS_PER_TICK);
    if (expectedFramesSoFar > totalTicks) {
      ticksToProcess += (expectedFramesSoFar - totalTicks);
    }

    for (var i = 0; i < ticksToProcess; i++) {
      animator.tick();
      totalTicks += 1;
    }

    renderer.render(scene, camera);

    previousTickStartTime = tickStartTime;

    requestAnimFrame(tick);
  };

  var animationTimer = {};

  animationTimer.start = function() {
    requestAnimFrame(tick);
  };

  return animationTimer;
};

var Animator = function(terrain) {
  var animator = {};

  var xPositionGenerator = new MotionGenerator();
  var zPositionGenerator = new LinearMotionGenerator(2, CityConfig.HALF_SCENE_DEPTH);

  animator.tick = function() {
    camera.position.x += xPositionGenerator.nextValue();
    camera.position.z += -zPositionGenerator.nextValue();

    var mapX = Coordinates.sceneXToMapX(camera.position.x);
    var mapZ = Coordinates.sceneZToMapZ(camera.position.z);

    var y = terrain.heightAtCoordinates(mapX, mapZ);
    camera.position.y = y + 0.5;
  };

  return animator;
};

var MotionGenerator = function() {
  var motionGenerator = {};

  motionGenerator.nextValue = function() {
    return 0.0;
  };

  motionGenerator.isComplete = function() {
    return false;
  };

  motionGenerator.onComplete = function() { };

  return motionGenerator;
};

var LinearMotionGenerator = function(delta, target) {
  var accumulator = 0.0;
  var isComplete = false;

  var linearMotionGenerator = new MotionGenerator();

  linearMotionGenerator.nextValue = function() {
    if (isComplete) {
      return 0.0;
    }

    accumulator += delta;

    if (accumulator <= target) {
      return delta;
    }
    else {
      isComplete = true;
      return (delta - (accumulator - target));
    }
  };

  linearMotionGenerator.isComplete = function() {
    return isComplete;
  };

  linearMotionGenerator.onComplete = function() { };

  return linearMotionGenerator;
};

var AnimationManager = function() {
  var TARGET_FRAME_WINDOW = 1000 / 60;   // 60 frames per second

  var animationManager = {};
  var animators = [];
  var previousFrameTimestamp;

  var deltaX;
  var deltaZ;
  var targetX;
  var targetGridX;
  var targetZ;
  var targetGridZ;
  var targetAngle = 0;
  var deltaAngle;

  var init = function() {
    var START_X = 0;
    var START_Y = 40;
    var START_Z = CityConfig.HALF_SCENE_DEPTH;

    camera.position.x = START_X;
    camera.position.y = START_Y;
    camera.position.z = START_Z;

    targetX = 0;
    targetZ = 0;
    deltaX = 0.0;
    deltaZ = -0.2;

    var framesUntilCityEdge = Math.abs(((CityConfig.HALF_TERRAIN_ROWS - CityConfig.HALF_BLOCK_ROWS) * CityConfig.BLOCK_AND_STREET_DEPTH) / deltaZ);
    var swoopDescentDelta = (START_Y - 0.5) / framesUntilCityEdge;

    var ramp = new rampAnimation(framesUntilCityEdge, -swoopDescentDelta, 0.5, 1000000);
    var forward = new forwardAnimation(targetX, deltaX, targetZ, deltaZ);
    animators = [ramp, forward];
  };

  var determineNextTargetPoint = function() {
    if (targetGridX == undefined || targetGridZ == undefined) {
      targetGridX = 0;
      targetGridZ = -1;
    }
    else {
      var oldTargetGridX = targetGridX;
      var oldTargetGridZ = targetGridZ;

      while (oldTargetGridX == targetGridX && oldTargetGridZ == targetGridZ) {
        if (deltaX == 0) {
          targetGridX = Math.floor(Math.random() * CityConfig.BLOCK_ROWS) - (CityConfig.BLOCK_ROWS / 2);
        }
        else if (deltaZ == 0) {
          targetGridZ = Math.floor(Math.random() * CityConfig.BLOCK_COLUMNS) - (CityConfig.BLOCK_COLUMNS / 2);
        }
      }
    }

    targetX = (targetGridX * CityConfig.BLOCK_WIDTH) + (targetGridX * CityConfig.STREET_WIDTH);
    targetZ = (targetGridZ * CityConfig.BLOCK_WIDTH) + (targetGridZ * CityConfig.STREET_WIDTH);

    deltaX = (camera.position.x == targetX) ? 0 : 0.2;
    deltaZ = (camera.position.z == targetZ) ? 0 : 0.2;
    deltaX *= (camera.position.x > targetX) ? -1 : 1;
    deltaZ *= (camera.position.z > targetZ) ? -1 : 1;
  };

  var determineRotationAngle = function() {
    var RIGHT_ANGLE = Math.PI / 2;
    var ROTATION_DELTA = 0.03;

    var oldTargetAngle = targetAngle;
    if (deltaX != 0 && oldTargetAngle == 0) {  // NORTH
      targetAngle = (deltaX < 0) ? targetAngle + RIGHT_ANGLE : targetAngle - RIGHT_ANGLE;
    }
    else if (deltaZ != 0 && (oldTargetAngle == RIGHT_ANGLE || oldTargetAngle == ((-3 * Math.PI) / 2))) {  // EAST
      targetAngle = (deltaZ < 0) ? targetAngle - RIGHT_ANGLE : targetAngle + RIGHT_ANGLE;
    }
    else if (deltaX != 0 && (oldTargetAngle == Math.PI || oldTargetAngle == Math.PI * -1)) {  // SOUTH
      targetAngle = (deltaX < 0) ? targetAngle - RIGHT_ANGLE : targetAngle + RIGHT_ANGLE;
    }
    else if (deltaZ != 0 && (oldTargetAngle == Math.PI / -2 || oldTargetAngle == (Math.PI * 3) / 2)) {  // WEST
      targetAngle = (deltaZ < 0) ? targetAngle + RIGHT_ANGLE : targetAngle - RIGHT_ANGLE;
    }

    deltaAngle = ROTATION_DELTA;
    deltaAngle *= (targetAngle > oldTargetAngle) ? 1 : -1;
  };

  animationManager.animate = function() {
    var currentTimestamp = new Date().getTime();
    var frameCount;

    if (animators.length === 0) {
      init();
    }

    if (previousFrameTimestamp === undefined) {
      frameCount = 1;
    }
    else {
      frameCount = Math.floor((currentTimestamp - previousFrameTimestamp) / TARGET_FRAME_WINDOW);
      if (frameCount < 1) {
        frameCount = 1;
      }
    }
    previousFrameTimestamp = currentTimestamp;

    var newAnimators = [];
    for (var i = 0; i < animators.length; i++) {
      animators[i].animate(frameCount);

      if (animators[i].finished === true) {
        if (animators[i] instanceof rampAnimation) {
          newAnimators.push(new hoverAnimation());
        }
        else if (animators[i] instanceof forwardAnimation) {
          determineNextTargetPoint();
          determineRotationAngle();
          newAnimators.push(new rotationAnimation(targetAngle, deltaAngle));
        }
        else if (animators[i] instanceof rotationAnimation) {
          newAnimators.push(new forwardAnimation(targetX, deltaX, targetZ, deltaZ));
        }
        else if (animators[i] instanceof hoverAnimation) {
          newAnimators.push(new hoverAnimation());
        }
      }
      else {
        newAnimators.push(animators[i]);
      }
    }
    animators = newAnimators;

    var mapX = Coordinates.sceneXToMapX(camera.position.x);
    var mapZ = Coordinates.sceneZToMapZ(camera.position.z);

    var y = terrain.heightAtCoordinates(mapX, mapZ);
    camera.position.y = Math.max(camera.position.y, y + 0.5);

    renderer.render(scene, camera);
    requestAnimFrame(animationManager.animate);
  };

  return animationManager;
};

function forwardAnimation(targetX, deltaX, targetZ, deltaZ) {
  this.targetX = targetX;
  this.deltaX = deltaX;
  this.targetZ = targetZ;
  this.deltaZ = deltaZ;
  this.finished = false;
}

forwardAnimation.prototype.animate = function(frameCount) {
  camera.position.x += this.deltaX * frameCount;
  camera.position.z += this.deltaZ * frameCount;

  if ((this.deltaX < 0 && camera.position.x < this.targetX) || (this.deltaX > 0 && camera.position.x > this.targetX) ||
      (this.deltaZ < 0 && camera.position.z < this.targetZ) || (this.deltaZ > 0 && camera.position.z > this.targetZ)) {
    camera.position.x = this.targetX;
    camera.position.z = this.targetZ;

    this.finished = true;
  }
}

function rotationAnimation(targetAngle, deltaAngle) {
  this.targetAngle = targetAngle;
  this.deltaAngle = deltaAngle;
  this.finished = false;
}

rotationAnimation.prototype.animate = function(frameCount) {
  camera.rotation.y += this.deltaAngle * frameCount;

  if ((this.deltaAngle < 0 && camera.rotation.y <= this.targetAngle) || (this.deltaAngle > 0 && camera.rotation.y >= this.targetAngle)) {
    if (this.targetAngle >= Math.PI * 2 || this.targetAngle <= Math.PI * -2) {
      this.targetAngle = 0;
    }

    camera.rotation.y = this.targetAngle;
    this.finished = true;
  }
}

function rampAnimation(frameDistance, deltaY, minHeight, maxHeight) {
  this.ticks = 0;
  this.frameDistance = frameDistance;
  this.deltaY = deltaY;
  this.minHeight = minHeight;
  this.maxHeight = maxHeight;
  this.finished = false;
}

rampAnimation.prototype.animate = function(frameCount) {
  if (camera.position.y >= this.minHeight && camera.position.y <= this.maxHeight) {
    camera.position.y += this.deltaY * frameCount;
  }
  if (camera.position.y < this.minHeight) {
    camera.position.y = this.minHeight;
  }
  if (camera.position.y > this.maxHeight) {
    camera.position.y = this.maxHeight;
  }

  this.ticks += frameCount;
  if (this.ticks > this.frameDistance) {
    this.finished = true;
  }
}

function hoverAnimation() {
  var frameDistance = (Math.random() * 300) + 300;
  var deltaY = (camera.position.y > 0.5) ? -0.05 : 0.05;

  this.rampAnimation = new rampAnimation(frameDistance, deltaY, 0.5, 15);
  this.finished = false;
}

hoverAnimation.prototype.animate = function(frameCount) {
  this.rampAnimation.animate(frameCount);
  this.finished = this.rampAnimation.finished;
}
<|endoftext|>


<commit_before>(function(jQuery) {
"use strict";

var $ = jQuery;

var canvas = Echo.Control.manifest("Echo.Canvas");

if (Echo.Control.isDefined(canvas)) return;

/**
 * @class Echo.Canvas
 * Class which implements Canvas mechanics on the client side.
 * The instance of this class is created for each Canvas found on the page by
 * the Echo.Loader. The instance of the class can also be created manually in
 * case the Canvas data already exists on the page.
 *
 * @package environment.pack.js
 *
 * @extends Echo.Control
 *
 * @constructor
 * Canvas object constructor to initialize the Echo.Canvas instance
 *
 * @param {Object} config
 * Configuration options
 */

/** @hide @method getRelativeTime */
/** @hide @echo_label justNow */
/** @hide @echo_label today */
/** @hide @echo_label yesterday */
/** @hide @echo_label lastWeek */
/** @hide @echo_label lastMonth */
/** @hide @echo_label secondAgo */
/** @hide @echo_label secondsAgo */
/** @hide @echo_label minuteAgo */
/** @hide @echo_label minutesAgo */
/** @hide @echo_label hourAgo */
/** @hide @echo_label hoursAgo */
/** @hide @echo_label dayAgo */
/** @hide @echo_label daysAgo */
/** @hide @echo_label weekAgo */
/** @hide @echo_label weeksAgo */
/** @hide @echo_label monthAgo */
/** @hide @echo_label monthsAgo */
/** @hide @echo_label loading */
/** @hide @echo_label retrying */
/** @hide @echo_label error_busy */
/** @hide @echo_label error_timeout */
/** @hide @echo_label error_waiting */
/** @hide @echo_label error_view_limit */
/** @hide @echo_label error_view_update_capacity_exceeded */
/** @hide @echo_label error_result_too_large */
/** @hide @echo_label error_wrong_query */
/** @hide @echo_label error_incorrect_appkey */
/** @hide @echo_label error_internal_error */
/** @hide @echo_label error_quota_exceeded */
/** @hide @echo_label error_incorrect_user_id */
/** @hide @echo_label error_unknown */

/**
 * @echo_event Echo.Canvas.onReady
 * Triggered when the app initialization is finished completely.
 */
/**
 * @echo_event Echo.Canvas.onRefresh
 * Triggered when the app is refreshed. For example after the user
 * login/logout action or as a result of the "refresh" function call.
 */
/**
 * @echo_event Echo.Canvas.onRender
 * Triggered when the app is rendered.
 */
/**
 * @echo_event Echo.Canvas.onRerender
 * Triggered when the app is rerendered.
 */

canvas.init = function() {
	var ids, cssClass;
	var self = this, target = this.config.get("target");
	// parent init function takes care about init finalization (rendering
	// and the "onReady" event firing)
	var parent = $.proxy(this.parent, this);

	// check if the canvas was already initialized
	if (target.data("echo-canvas-initialized")) {
		this._error({
			"args": {"target": target},
			"code": "canvas_already_initialized"
		});
		return;
	}

	// define initialized state for the canvas
	// to prevent multiple initialization of the same canvas
	target.data("echo-canvas-initialized", true);

	// extending Canvas config with the parameters defined in the target
	var overrides = this._getOverrides(target, ["id", "useSecureAPI", "mode"]);
	if (!$.isEmptyObject(overrides)) {
		this.config.extend(overrides);
	}

	if (Echo.Loader.isDebug()) this.config.set("mode", "dev");

	// exit if no "id" is defined for the canvas,
	// skip this validation in case the "data" is defined explicitly in the config
	if (!this._isManuallyConfigured() && !this.config.get("id")) {
		this._error({
			"args": {"target": target},
			"code": "invalid_canvas_config"
		});
		return;
	}

	// apply our canvas id as a CSS class if we aren't manually configured
	if (this.config.get("id")) {
		ids = this._getIds().normalized;
		// adding a primary canvas ID and unique page identifier
		// as a CSS class if provided
		cssClass = Echo.Utils.foldl("", ["main", "unique"], function(type, acc) {
			return (acc += ids[type] ? self.get("cssPrefix") + ids[type] + " " : "");
		});
		target.addClass(cssClass);
	}

	// fetch canvas config from remote storage
	this._fetchConfig(function() {
		var backplane = self.get("data.backplane");
		if (backplane) {
			Backplane.init(backplane);
		}
		self._loadAppResources(parent);
	});
};

canvas.config = {
	/**
	 * @cfg {String} [id]
	 * Unique ID of the Canvas, used by the Echo.Canvas instance
	 * to retrieve the data from the Canvases data storage.
	 */
	"id": "",

	/**
	 * @cfg {Object} [data]
	 * Object which contains the Canvas data in the format
	 * used to store the Canvas config in the Canvas storage.
	 */
	"data": {},

	/**
	 * @cfg {String} target(required)
	 * Specifies the DOM element where the control will be displayed.
	 *
	 * Note: if only the "target" config parameter is defined, the target DOM element
	 * should contain the following HTML attribute:
	 *
	 * + "data-canvas-id" with the unique Canvas ID which should be initialized
	 *
	 * The values of the HTML parameters override the "id" parameter value
	 * (respectively) passed via the Canvas config.
	 */

	/**
	 * @cfg {Object} [overrides]
	 * Object which contains the overrides applied for this Canvas on the page
	 * via Echo.Loader.override function call.
	 */
	"overrides": {},

	/**
	 * @cfg {String} [mode]
	 * This parameter specifies the mode in which Canvas works.
	 * There are two possible values for this parameter:
	 *
	 * + "dev" - in this case the Canvas works with the development configuration storage
	 * + "prod" - in this case the Canvas works with the production configuration storage
	 *
	 * More information about defference between production and development configuration
	 * storages can be found in the ["How to deploy an App using a Canvas guide"](#!/guide/how_to_deploy_an_app_using_a_canvas)
	 *
	 * The value of this parameter can be overridden by specifying the "data-canvas-mode"
	 * target DOM element attribute.
	 * More information about HTML attributes of the target DOM element can be found [here](#!/guide/how_to_deploy_an_app_using_a_canvas)
	 */
	"mode": "prod"
};

canvas.vars = {
	"apps": []
};

canvas.labels = {
	/**
	 * @echo_label
	 */
	"error_no_apps": "No applications defined for this canvas",
	/**
	 * @echo_label
	 */
	"error_no_config": "Unable to retrieve Canvas config",
	/**
	 * @echo_label
	 */
	"error_no_suitable_app_class": "Unable to init an app, no suitable JS class found",
	/**
	 * @echo_label
	 */
	"error_unable_to_retrieve_app_config": "Unable to retrieve Canvas config from the storage",
	/**
	 * @echo_label
	 */
	"error_incomplete_app_config": "Unable to init an app, config is incomplete",
	/**
	 * @echo_label
	 */
	"error_canvas_already_initialized": "Canvas has been initialized already",
	/**
	 * @echo_label
	 */
	"error_invalid_canvas_config": "Canvas with invalid configuration found"
};

/**
 * @echo_template
 */
canvas.templates.main =
	'

';

/**
 * @echo_template
 */
canvas.templates.app =
	'
' +
		'
{data:caption}
' +
		'

' +
	'
';

canvas.destroy = function() {
	$.map(this.get("apps"), $.proxy(this._destroyApp, this));
	this.config.get("target").data("echo-canvas-initialized", false);
	// remove cached canvas config
	Echo.Utils.remove(Echo.Loader.canvasesConfigById, this._getIds().unique);
};

/**
 * @echo_renderer
 */
canvas.renderers.container = function(element) {
	var self = this;
	$.map(this.get("data.apps"), function(app, id) {
		self._initApp(app, element, id);
	});
	return element;
};

canvas.methods._initApp = function(app, element, id) {
	var self = this;
	var Application = Echo.Utils.getComponent(app.component);
	if (!Application) {
		this._error({
			"args": {"app": app},
			"code": "no_suitable_app_class"
		});
		return;
	}

	app.id = app.id || id;  // define app position in array as id if not specified
	app.config = app.config || {};
	app.config.canvasId = this.config.get('id');

	var view = this.view.fork({
		"renderer": null,
		"renderers": {
			"appHeader": function(element) {
				// show|hide app header depending on the caption existance
				return element[app.caption ? "show" : "hide"]();
			},
			"appBody": function(element) {
				var className = self.get("cssPrefix") + "appId-" + app.id;
				return element.addClass(className);
			}
		}
	});
	element.append(view.render({
		"data": app,
		"template": this.templates.app
	}));

	app.config.target = view.get("appBody");

	var overrides = this.config.get("overrides")[app.id];
	var config = overrides
		? $.extend(true, app.config, overrides)
		: app.config;
	this.apps.push(new Application(config));
};

canvas.methods._destroyApp = function(app) {
	if (app && $.isFunction(app.destroy)) app.destroy();
};

canvas.methods._isManuallyConfigured = function() {
	return !$.isEmptyObject(this.get("data"));
};

canvas.methods._getAppScriptURL = function(config) {
	if (!config.scripts) return config.script;
	var isSecure, script = {
		"dev": config.scripts.dev || config.scripts.prod,
		"prod": config.scripts.prod || config.scripts.dev
	}[Echo.Loader.isDebug() ? "dev" : "prod"];
	if (typeof script === "string") return script;
	isSecure = /^https/.test(window.location.protocol);
	return script[isSecure ? "secure" : "regular"];
};

canvas.methods._loadAppResources = function(callback) {
	var self = this, resources = [], isManual = this._isManuallyConfigured();
	$.map(this.get("data.apps"), function(app) {
		var script = self._getAppScriptURL(app);
		if (!app.component || !script || !(isManual || app.id)) {
			self._error({
				"args": {"app": app},
				"code": "incomplete_app_config"
			});
			return;
		}
		resources.push({
			"url": script,
			"loaded": function() {
				return Echo.Control.isDefined(app.component);
			}
		});
	});
	Echo.Loader.download(resources, callback);
};

canvas.methods._getOverrides = function(target, spec) {
	return Echo.Utils.foldl({}, spec || [], function(item, acc) {
		// We should convert spec item to lower case because of jQuery
		// HTML5 data attributes implementation http://api.jquery.com/data/#data-html5
		// Since we have config keys in camel case representation like "useSecureAPI",
		// we should follow to these rules.
		var key = "canvas-" + item.toLowerCase();
		var value = target.data(key);
		if (typeof value !== "undefined") {
			acc[item] = value;
		}
	});
};

canvas.methods._error = function(args) {
	args.message = args.message || this.labels.get("error_" + args.code);

	/**
	 * @echo_event Echo.Canvas.onError
	 * Event which is triggered in case of errors such as invalid configuration,
	 * problems fetching the data from the server side, etc.
	 *
	 * @param {String} topic
	 * Name of the event produced.
	 *
	 * @param {Object} data
	 * Object which contains debug information regarding the error.
	 */
	Echo.Events.publish({
		"topic": "Echo.Canvas.onError",
		"data": args
	});

	Echo.Utils.log($.extend(args, {"type": "error", "component": "Echo.Canvas"}));
	if (args.renderError) {
		this.showMessage({
			"type": "error",
			"message": args.message
		});
	}
};

canvas.methods._getIds = function() {
	var id = this.config.get("id");
	var parts = id.split("#");
	var normalize = function(s) { return s.replace(/[^a-z\d]/ig, "-"); };
	return {
		"unique": id,
		"main": parts[0],
		"normalized": {
			"unique": normalize(id),
			"main": normalize(parts[0])
		}
	};
};

canvas.methods._fetchConfig = function(callback) {
	var self = this, target = this.config.get("target");
	var isManual = this._isManuallyConfigured();
	// no need to perform server side request in case
	// we already have all the data on the client side
	if (isManual) {
		callback.call(this);
		return;
	}
	var mode = this.config.get("mode");
	var getConfig = function() {
		return Echo.Loader.canvasesConfigById[self._getIds().unique];
	};
	var parts = Echo.Utils.parseURL(Echo.Loader.config.storageURL[mode]);
	var URL = this.substitute({
		"template": "{data:scheme}://{data:domain}{data:path}{data:endpoint}",
		"data": $.extend(parts, {
			// taking care of the Canvas unique identifier on the page,
			// specified as "#XXX" in the Canvas ID. We don't need to send this
			// unique page identifier, we send only the primary Canvas ID.
			"endpoint": this._getIds().main,
			"scheme": this.config.get("useSecureAPI") ? "https" : parts.scheme || "http"
		})
	});

	$.ajax({
		"url": URL,
		"crossDomain": true,
		"dataType": "script",
		"cache": mode !== "dev",
		"timeout": Echo.Loader.config.errorTimeout,
		"success": function() {
			var config = getConfig();
			if (!config || !config.apps || !config.apps.length) {
				var message = self.labels.get("error_no_" + (config ? "apps" : "config"));
				self._error({
					"args": {"config": config, "target": target},
					"code": "invalid_canvas_config",
					"message": message
				});
				return;
			}
			self.set("data", config); // store Canvas data into the instance
			callback.call(self);
		},
		"error": function() {
			self._error({
				"args": arguments,
				"code": "unable_to_retrieve_app_config",
				"renderError": true
			});
		}
	});
};

Echo.Control.create(canvas);

})(Echo.jQuery);
<commit_msg>[Echo.Canvas -> _fetchConfig] making $.ajax function async in all cases<commit_after>(function(jQuery) {
"use strict";

var $ = jQuery;

var canvas = Echo.Control.manifest("Echo.Canvas");

if (Echo.Control.isDefined(canvas)) return;

/**
 * @class Echo.Canvas
 * Class which implements Canvas mechanics on the client side.
 * The instance of this class is created for each Canvas found on the page by
 * the Echo.Loader. The instance of the class can also be created manually in
 * case the Canvas data already exists on the page.
 *
 * @package environment.pack.js
 *
 * @extends Echo.Control
 *
 * @constructor
 * Canvas object constructor to initialize the Echo.Canvas instance
 *
 * @param {Object} config
 * Configuration options
 */

/** @hide @method getRelativeTime */
/** @hide @echo_label justNow */
/** @hide @echo_label today */
/** @hide @echo_label yesterday */
/** @hide @echo_label lastWeek */
/** @hide @echo_label lastMonth */
/** @hide @echo_label secondAgo */
/** @hide @echo_label secondsAgo */
/** @hide @echo_label minuteAgo */
/** @hide @echo_label minutesAgo */
/** @hide @echo_label hourAgo */
/** @hide @echo_label hoursAgo */
/** @hide @echo_label dayAgo */
/** @hide @echo_label daysAgo */
/** @hide @echo_label weekAgo */
/** @hide @echo_label weeksAgo */
/** @hide @echo_label monthAgo */
/** @hide @echo_label monthsAgo */
/** @hide @echo_label loading */
/** @hide @echo_label retrying */
/** @hide @echo_label error_busy */
/** @hide @echo_label error_timeout */
/** @hide @echo_label error_waiting */
/** @hide @echo_label error_view_limit */
/** @hide @echo_label error_view_update_capacity_exceeded */
/** @hide @echo_label error_result_too_large */
/** @hide @echo_label error_wrong_query */
/** @hide @echo_label error_incorrect_appkey */
/** @hide @echo_label error_internal_error */
/** @hide @echo_label error_quota_exceeded */
/** @hide @echo_label error_incorrect_user_id */
/** @hide @echo_label error_unknown */

/**
 * @echo_event Echo.Canvas.onReady
 * Triggered when the app initialization is finished completely.
 */
/**
 * @echo_event Echo.Canvas.onRefresh
 * Triggered when the app is refreshed. For example after the user
 * login/logout action or as a result of the "refresh" function call.
 */
/**
 * @echo_event Echo.Canvas.onRender
 * Triggered when the app is rendered.
 */
/**
 * @echo_event Echo.Canvas.onRerender
 * Triggered when the app is rerendered.
 */

canvas.init = function() {
	var ids, cssClass;
	var self = this, target = this.config.get("target");
	// parent init function takes care about init finalization (rendering
	// and the "onReady" event firing)
	var parent = $.proxy(this.parent, this);

	// check if the canvas was already initialized
	if (target.data("echo-canvas-initialized")) {
		this._error({
			"args": {"target": target},
			"code": "canvas_already_initialized"
		});
		return;
	}

	// define initialized state for the canvas
	// to prevent multiple initialization of the same canvas
	target.data("echo-canvas-initialized", true);

	// extending Canvas config with the parameters defined in the target
	var overrides = this._getOverrides(target, ["id", "useSecureAPI", "mode"]);
	if (!$.isEmptyObject(overrides)) {
		this.config.extend(overrides);
	}

	if (Echo.Loader.isDebug()) this.config.set("mode", "dev");

	// exit if no "id" is defined for the canvas,
	// skip this validation in case the "data" is defined explicitly in the config
	if (!this._isManuallyConfigured() && !this.config.get("id")) {
		this._error({
			"args": {"target": target},
			"code": "invalid_canvas_config"
		});
		return;
	}

	// apply our canvas id as a CSS class if we aren't manually configured
	if (this.config.get("id")) {
		ids = this._getIds().normalized;
		// adding a primary canvas ID and unique page identifier
		// as a CSS class if provided
		cssClass = Echo.Utils.foldl("", ["main", "unique"], function(type, acc) {
			return (acc += ids[type] ? self.get("cssPrefix") + ids[type] + " " : "");
		});
		target.addClass(cssClass);
	}

	// fetch canvas config from remote storage
	this._fetchConfig(function() {
		var backplane = self.get("data.backplane");
		if (backplane) {
			Backplane.init(backplane);
		}
		self._loadAppResources(parent);
	});
};

canvas.config = {
	/**
	 * @cfg {String} [id]
	 * Unique ID of the Canvas, used by the Echo.Canvas instance
	 * to retrieve the data from the Canvases data storage.
	 */
	"id": "",

	/**
	 * @cfg {Object} [data]
	 * Object which contains the Canvas data in the format
	 * used to store the Canvas config in the Canvas storage.
	 */
	"data": {},

	/**
	 * @cfg {String} target(required)
	 * Specifies the DOM element where the control will be displayed.
	 *
	 * Note: if only the "target" config parameter is defined, the target DOM element
	 * should contain the following HTML attribute:
	 *
	 * + "data-canvas-id" with the unique Canvas ID which should be initialized
	 *
	 * The values of the HTML parameters override the "id" parameter value
	 * (respectively) passed via the Canvas config.
	 */

	/**
	 * @cfg {Object} [overrides]
	 * Object which contains the overrides applied for this Canvas on the page
	 * via Echo.Loader.override function call.
	 */
	"overrides": {},

	/**
	 * @cfg {String} [mode]
	 * This parameter specifies the mode in which Canvas works.
	 * There are two possible values for this parameter:
	 *
	 * + "dev" - in this case the Canvas works with the development configuration storage
	 * + "prod" - in this case the Canvas works with the production configuration storage
	 *
	 * More information about defference between production and development configuration
	 * storages can be found in the ["How to deploy an App using a Canvas guide"](#!/guide/how_to_deploy_an_app_using_a_canvas)
	 *
	 * The value of this parameter can be overridden by specifying the "data-canvas-mode"
	 * target DOM element attribute.
	 * More information about HTML attributes of the target DOM element can be found [here](#!/guide/how_to_deploy_an_app_using_a_canvas)
	 */
	"mode": "prod"
};

canvas.vars = {
	"apps": []
};

canvas.labels = {
	/**
	 * @echo_label
	 */
	"error_no_apps": "No applications defined for this canvas",
	/**
	 * @echo_label
	 */
	"error_no_config": "Unable to retrieve Canvas config",
	/**
	 * @echo_label
	 */
	"error_no_suitable_app_class": "Unable to init an app, no suitable JS class found",
	/**
	 * @echo_label
	 */
	"error_unable_to_retrieve_app_config": "Unable to retrieve Canvas config from the storage",
	/**
	 * @echo_label
	 */
	"error_incomplete_app_config": "Unable to init an app, config is incomplete",
	/**
	 * @echo_label
	 */
	"error_canvas_already_initialized": "Canvas has been initialized already",
	/**
	 * @echo_label
	 */
	"error_invalid_canvas_config": "Canvas with invalid configuration found"
};

/**
 * @echo_template
 */
canvas.templates.main =
	'

';

/**
 * @echo_template
 */
canvas.templates.app =
	'
' +
		'
{data:caption}
' +
		'

' +
	'
';

canvas.destroy = function() {
	$.map(this.get("apps"), $.proxy(this._destroyApp, this));
	this.config.get("target").data("echo-canvas-initialized", false);
	// remove cached canvas config
	Echo.Utils.remove(Echo.Loader.canvasesConfigById, this._getIds().unique);
};

/**
 * @echo_renderer
 */
canvas.renderers.container = function(element) {
	var self = this;
	$.map(this.get("data.apps"), function(app, id) {
		self._initApp(app, element, id);
	});
	return element;
};

canvas.methods._initApp = function(app, element, id) {
	var self = this;
	var Application = Echo.Utils.getComponent(app.component);
	if (!Application) {
		this._error({
			"args": {"app": app},
			"code": "no_suitable_app_class"
		});
		return;
	}

	app.id = app.id || id;  // define app position in array as id if not specified
	app.config = app.config || {};
	app.config.canvasId = this.config.get('id');

	var view = this.view.fork({
		"renderer": null,
		"renderers": {
			"appHeader": function(element) {
				// show|hide app header depending on the caption existance
				return element[app.caption ? "show" : "hide"]();
			},
			"appBody": function(element) {
				var className = self.get("cssPrefix") + "appId-" + app.id;
				return element.addClass(className);
			}
		}
	});
	element.append(view.render({
		"data": app,
		"template": this.templates.app
	}));

	app.config.target = view.get("appBody");

	var overrides = this.config.get("overrides")[app.id];
	var config = overrides
		? $.extend(true, app.config, overrides)
		: app.config;
	this.apps.push(new Application(config));
};

canvas.methods._destroyApp = function(app) {
	if (app && $.isFunction(app.destroy)) app.destroy();
};

canvas.methods._isManuallyConfigured = function() {
	return !$.isEmptyObject(this.get("data"));
};

canvas.methods._getAppScriptURL = function(config) {
	if (!config.scripts) return config.script;
	var isSecure, script = {
		"dev": config.scripts.dev || config.scripts.prod,
		"prod": config.scripts.prod || config.scripts.dev
	}[Echo.Loader.isDebug() ? "dev" : "prod"];
	if (typeof script === "string") return script;
	isSecure = /^https/.test(window.location.protocol);
	return script[isSecure ? "secure" : "regular"];
};

canvas.methods._loadAppResources = function(callback) {
	var self = this, resources = [], isManual = this._isManuallyConfigured();
	$.map(this.get("data.apps"), function(app) {
		var script = self._getAppScriptURL(app);
		if (!app.component || !script || !(isManual || app.id)) {
			self._error({
				"args": {"app": app},
				"code": "incomplete_app_config"
			});
			return;
		}
		resources.push({
			"url": script,
			"loaded": function() {
				return Echo.Control.isDefined(app.component);
			}
		});
	});
	Echo.Loader.download(resources, callback);
};

canvas.methods._getOverrides = function(target, spec) {
	return Echo.Utils.foldl({}, spec || [], function(item, acc) {
		// We should convert spec item to lower case because of jQuery
		// HTML5 data attributes implementation http://api.jquery.com/data/#data-html5
		// Since we have config keys in camel case representation like "useSecureAPI",
		// we should follow to these rules.
		var key = "canvas-" + item.toLowerCase();
		var value = target.data(key);
		if (typeof value !== "undefined") {
			acc[item] = value;
		}
	});
};

canvas.methods._error = function(args) {
	args.message = args.message || this.labels.get("error_" + args.code);

	/**
	 * @echo_event Echo.Canvas.onError
	 * Event which is triggered in case of errors such as invalid configuration,
	 * problems fetching the data from the server side, etc.
	 *
	 * @param {String} topic
	 * Name of the event produced.
	 *
	 * @param {Object} data
	 * Object which contains debug information regarding the error.
	 */
	Echo.Events.publish({
		"topic": "Echo.Canvas.onError",
		"data": args
	});

	Echo.Utils.log($.extend(args, {"type": "error", "component": "Echo.Canvas"}));
	if (args.renderError) {
		this.showMessage({
			"type": "error",
			"message": args.message
		});
	}
};

canvas.methods._getIds = function() {
	var id = this.config.get("id");
	var parts = id.split("#");
	var normalize = function(s) { return s.replace(/[^a-z\d]/ig, "-"); };
	return {
		"unique": id,
		"main": parts[0],
		"normalized": {
			"unique": normalize(id),
			"main": normalize(parts[0])
		}
	};
};

canvas.methods._fetchConfig = function(callback) {
	var self = this, target = this.config.get("target");
	var isManual = this._isManuallyConfigured();
	// no need to perform server side request in case
	// we already have all the data on the client side
	if (isManual) {
		callback.call(this);
		return;
	}
	var mode = this.config.get("mode");
	var getConfig = function() {
		return Echo.Loader.canvasesConfigById[self._getIds().unique];
	};
	var parts = Echo.Utils.parseURL(Echo.Loader.config.storageURL[mode]);
	var URL = this.substitute({
		"template": "{data:scheme}://{data:domain}{data:path}{data:endpoint}",
		"data": $.extend(parts, {
			// taking care of the Canvas unique identifier on the page,
			// specified as "#XXX" in the Canvas ID. We don't need to send this
			// unique page identifier, we send only the primary Canvas ID.
			"endpoint": this._getIds().main,
			"scheme": this.config.get("useSecureAPI") ? "https" : parts.scheme || "http"
		})
	});

	// we rely on asynchronius beahavor of the $.ajax method call
	// to implement store/fetch mechanism. In case of fetching
	// a cached file it fails in some heuristic cases. So we try to avoid
	// this situation by making $.ajax function async in all cases.
	setTimeout(function() {
		$.ajax({
			"url": URL,
			"crossDomain": true,
			"dataType": "script",
			"cache": mode !== "dev",
			"timeout": Echo.Loader.config.errorTimeout,
			"success": function() {
				var config = getConfig();
				if (!config || !config.apps || !config.apps.length) {
					var message = self.labels.get("error_no_" + (config ? "apps" : "config"));
					self._error({
						"args": {"config": config, "target": target},
						"code": "invalid_canvas_config",
						"message": message
					});
					return;
				}
				self.set("data", config); // store Canvas data into the instance
				callback.call(self);
			},
			"error": function() {
				self._error({
					"args": arguments,
					"code": "unable_to_retrieve_app_config",
					"renderError": true
				});
			}
		});
	}, 0);
};

Echo.Control.create(canvas);

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