<commit_before>import Ember from 'ember';
import moment from 'moment';
import interact from 'interact';
import OccurrenceComponent from '../occurrence';

export default OccurrenceComponent.extend({
  classNameBindings: [':as-calendar-occurrence--timetable'],

  timetable: null,
  isInteracting: false,
  isDraggable: true,
  isResizable: true,
  isRemovable: true,
  isEditable: true,
  dayWidth: Ember.computed.oneWay('timetable.dayWidth'),
  referenceElement: Ember.computed.oneWay('timetable.referenceElement'),

  _calendar: Ember.computed.oneWay('model.calendar'),
  _dayEndingTime: Ember.computed.oneWay('day.endingTime'),
  _dragBottomDistance: null,
  _dragTopDistance: null,
  _dragVerticalOffset: null,
  _preview: Ember.computed.oneWay('_calendar.occurrencePreview'),

  _setupInteractable: Ember.on('didInsertElement', function() {
    var interactable = interact(this.$()[0]).on('mouseup', (event) => {
      Ember.run(this, this._mouseUp, event);
    });

    if (this.get('isResizable')) {
      interactable.resizable({
        edges: { bottom: '.as-calendar-occurrence__resize-handle' },

        onstart: (event) => {
          Ember.run(this, this._resizeStart, event);
        },

        onmove: (event) => {
          Ember.run(this, this._resizeMove, event);
        },

        onend: (event) => {
          Ember.run(this, this._resizeEnd, event);
        },
      });
    }

    if (this.get('isDraggable')) {
      interactable.draggable({
        onstart: (event) => {
          Ember.run(this, this._dragStart, event);
        },

        onmove: (event) => {
          Ember.run(this, this._dragMove, event);
        },

        onend: (event) => {
          Ember.run(this, this._dragEnd, event);
        },
      });
    }

    interactable.on('tap', (event) => {
      if (event.double) { return; }
      Ember.run(this, this._tap, event);
    });

    interactable.on('doubletap', (event) => {
      Ember.run(this, this._doubleTap, event);
    });
  }),

  _teardownInteractable: Ember.on('willDestroyElement', function() {
    interact(this.$()[0]).off();
  }),

  _resizeStart: function() {
    this.set('isInteracting', true);
    this.set('_calendar.occurrencePreview', this.get('model').copy());
  },

  _resizeMove: function(event) {
    var newDuration = moment.duration(
      Math.floor(event.rect.height / this.get('timeSlotHeight')) *
      this.get('computedTimeSlotDuration').as('ms')
    );

    var changes = {
      endsAt: moment(this.get('_startingTime')).add(newDuration).toDate()
    };

    this._validateAndSavePreview(changes);
  },

  _resizeEnd: function() {
    this.attrs.onUpdate(this.get('content'), {
      endsAt: this.get('_preview.content.endsAt')
    });

    this.set('isInteracting', false);
    this.set('_calendar.occurrencePreview', null);
  },

  _dragStart: function() {
    var $this = this.$();
    var $referenceElement = Ember.$(this.get('referenceElement'));
    const preview = this.get('model').copy();

    preview.set('isPreview', true);
    this.set('isInteracting', true);
    this.set('_calendar.occurrencePreview', preview);
    this.set('_dragVerticalOffset', 0);
    this.set('_dragTopDistance', $referenceElement.offset().top - $this.offset().top);

    this.set('_dragBottomDistance',
             ($referenceElement.offset().top + $referenceElement.height()) -
      ($this.offset().top + $this.height()));
  },

  _dragMove: function(event) {
    this.set('_dragVerticalOffset', this.get('_dragVerticalOffset') + event.dy);

    var dragTimeSlotOffset = this._dragTimeSlotOffset(event);
    var dragDayOffset = this._dragDayOffset(event);
    var startsAt = moment(this.get('_startingTime')).add(dragTimeSlotOffset).add(dragDayOffset);

    var changes = {
      startsAt: startsAt.toDate(),
      endsAt: moment(startsAt).add(this.get('_duration')).toDate()
    };

    this._validateAndSavePreview(changes);
  },

  _dragTimeSlotOffset: function() {
    var verticalDrag = this._clamp(
      this.get('_dragVerticalOffset'),
      this.get('_dragTopDistance'),
      this.get('_dragBottomDistance')
    );

    return moment.duration(
      Math.floor(verticalDrag / this.get('timeSlotHeight')) * this.get('computedTimeSlotDuration')
    );
  },

  _dragDayOffset: function(event) {
    var $referenceElement = Ember.$(this.get('referenceElement'));

    var offsetX = this._clamp(
      event.pageX - $referenceElement.offset().left,
      0,
      $referenceElement.width() - 1
    );

    return moment.duration(
      Math.floor(offsetX / this.get('dayWidth')) -
      this.get('day.offset'),
      'days'
    );
  },

  _dragEnd: function() {
    this.attrs.onUpdate(this.get('content'), {
      startsAt: this.get('_preview.content.startsAt'),
      endsAt: this.get('_preview.content.endsAt')
    });

    this.set('isInteracting', false);
    this.set('_calendar.occurrencePreview', null);
    this.set('_dragVerticalOffset', null);
    this.set('_dragTopDistance', null);
    this.set('_dragBottomDistance', null);
  },

  _mouseUp: function() {
    Ember.$(document.documentElement).css('cursor', '');
  },

  _tap: function(event) {
    this.attrs.onClick(this.get('content'));
    event.preventDefault();
  },

  _doubleTap: function(event) {
    this.attrs.onDoubleClick(this.get('content'));
    event.preventDefault();
  },

  _validateAndSavePreview: function(changes) {
    if (this._validatePreviewChanges(changes)) {
      this.attrs.onUpdate(this.get('_preview.content'), changes);
    }
  },

  _validatePreviewChanges: function(changes) {
    var newPreview = this.get('_preview').copy();

    newPreview.get('content').setProperties(changes);

    return newPreview.get('startingTime') >= newPreview.get('day.startingTime') &&
           newPreview.get('endingTime') <= newPreview.get('day.endingTime') &&
           newPreview.get('duration') >= this.get('computedTimeSlotDuration');
  },

  _clamp: function(number, min, max) {
    return Math.max(min, Math.min(number, max));
  },

  actions: {
    remove: function() {
      this.attrs.onRemove(this.get('content'));
      this.set('isInteracting', false);
    },
    edit: function() {
      this.attrs.onEdit(this.get('content'));
      this.set('isInteracting', false);
    }
  }
});
<commit_msg>don't setup tap/doubletap events unless actions are defined<commit_after>import Ember from 'ember';
import moment from 'moment';
import interact from 'interact';
import OccurrenceComponent from '../occurrence';

export default OccurrenceComponent.extend({
  classNameBindings: [':as-calendar-occurrence--timetable'],

  timetable: null,
  isInteracting: false,
  isDraggable: true,
  isResizable: true,
  isRemovable: true,
  isEditable: true,
  dayWidth: Ember.computed.oneWay('timetable.dayWidth'),
  referenceElement: Ember.computed.oneWay('timetable.referenceElement'),

  _calendar: Ember.computed.oneWay('model.calendar'),
  _dayEndingTime: Ember.computed.oneWay('day.endingTime'),
  _dragBottomDistance: null,
  _dragTopDistance: null,
  _dragVerticalOffset: null,
  _preview: Ember.computed.oneWay('_calendar.occurrencePreview'),

  _setupInteractable: Ember.on('didInsertElement', function() {
    var interactable = interact(this.$()[0]).on('mouseup', (event) => {
      Ember.run(this, this._mouseUp, event);
    });

    if (this.get('isResizable')) {
      interactable.resizable({
        edges: { bottom: '.as-calendar-occurrence__resize-handle' },

        onstart: (event) => {
          Ember.run(this, this._resizeStart, event);
        },

        onmove: (event) => {
          Ember.run(this, this._resizeMove, event);
        },

        onend: (event) => {
          Ember.run(this, this._resizeEnd, event);
        },
      });
    }

    if (this.get('isDraggable')) {
      interactable.draggable({
        onstart: (event) => {
          Ember.run(this, this._dragStart, event);
        },

        onmove: (event) => {
          Ember.run(this, this._dragMove, event);
        },

        onend: (event) => {
          Ember.run(this, this._dragEnd, event);
        },
      });
    }

    if (this.get('onClick')) {
      interactable.on('tap', (event) => {
        if (event.double) { return; }
        Ember.run(this, this._tap, event);
      });
    }

    if (this.get('onDoubleClick')) {
      interactable.on('doubletap', (event) => {
        Ember.run(this, this._doubleTap, event);
      });
    }
  }),

  _teardownInteractable: Ember.on('willDestroyElement', function() {
    interact(this.$()[0]).off();
  }),

  _resizeStart: function() {
    this.set('isInteracting', true);
    this.set('_calendar.occurrencePreview', this.get('model').copy());
  },

  _resizeMove: function(event) {
    var newDuration = moment.duration(
      Math.floor(event.rect.height / this.get('timeSlotHeight')) *
      this.get('computedTimeSlotDuration').as('ms')
    );

    var changes = {
      endsAt: moment(this.get('_startingTime')).add(newDuration).toDate()
    };

    this._validateAndSavePreview(changes);
  },

  _resizeEnd: function() {
    this.attrs.onUpdate(this.get('content'), {
      endsAt: this.get('_preview.content.endsAt')
    });

    this.set('isInteracting', false);
    this.set('_calendar.occurrencePreview', null);
  },

  _dragStart: function() {
    var $this = this.$();
    var $referenceElement = Ember.$(this.get('referenceElement'));
    const preview = this.get('model').copy();

    preview.set('isPreview', true);
    this.set('isInteracting', true);
    this.set('_calendar.occurrencePreview', preview);
    this.set('_dragVerticalOffset', 0);
    this.set('_dragTopDistance', $referenceElement.offset().top - $this.offset().top);

    this.set('_dragBottomDistance',
             ($referenceElement.offset().top + $referenceElement.height()) -
      ($this.offset().top + $this.height()));
  },

  _dragMove: function(event) {
    this.set('_dragVerticalOffset', this.get('_dragVerticalOffset') + event.dy);

    var dragTimeSlotOffset = this._dragTimeSlotOffset(event);
    var dragDayOffset = this._dragDayOffset(event);
    var startsAt = moment(this.get('_startingTime')).add(dragTimeSlotOffset).add(dragDayOffset);

    var changes = {
      startsAt: startsAt.toDate(),
      endsAt: moment(startsAt).add(this.get('_duration')).toDate()
    };

    this._validateAndSavePreview(changes);
  },

  _dragTimeSlotOffset: function() {
    var verticalDrag = this._clamp(
      this.get('_dragVerticalOffset'),
      this.get('_dragTopDistance'),
      this.get('_dragBottomDistance')
    );

    return moment.duration(
      Math.floor(verticalDrag / this.get('timeSlotHeight')) * this.get('computedTimeSlotDuration')
    );
  },

  _dragDayOffset: function(event) {
    var $referenceElement = Ember.$(this.get('referenceElement'));

    var offsetX = this._clamp(
      event.pageX - $referenceElement.offset().left,
      0,
      $referenceElement.width() - 1
    );

    return moment.duration(
      Math.floor(offsetX / this.get('dayWidth')) -
      this.get('day.offset'),
      'days'
    );
  },

  _dragEnd: function() {
    this.attrs.onUpdate(this.get('content'), {
      startsAt: this.get('_preview.content.startsAt'),
      endsAt: this.get('_preview.content.endsAt')
    });

    this.set('isInteracting', false);
    this.set('_calendar.occurrencePreview', null);
    this.set('_dragVerticalOffset', null);
    this.set('_dragTopDistance', null);
    this.set('_dragBottomDistance', null);
  },

  _mouseUp: function() {
    Ember.$(document.documentElement).css('cursor', '');
  },

  _tap: function(event) {
    this.attrs.onClick(this.get('content'));
    event.preventDefault();
  },

  _doubleTap: function(event) {
    this.attrs.onDoubleClick(this.get('content'));
    event.preventDefault();
  },

  _validateAndSavePreview: function(changes) {
    if (this._validatePreviewChanges(changes)) {
      this.attrs.onUpdate(this.get('_preview.content'), changes);
    }
  },

  _validatePreviewChanges: function(changes) {
    var newPreview = this.get('_preview').copy();

    newPreview.get('content').setProperties(changes);

    return newPreview.get('startingTime') >= newPreview.get('day.startingTime') &&
           newPreview.get('endingTime') <= newPreview.get('day.endingTime') &&
           newPreview.get('duration') >= this.get('computedTimeSlotDuration');
  },

  _clamp: function(number, min, max) {
    return Math.max(min, Math.min(number, max));
  },

  actions: {
    remove: function() {
      this.attrs.onRemove(this.get('content'));
      this.set('isInteracting', false);
    },
    edit: function() {
      this.attrs.onEdit(this.get('content'));
      this.set('isInteracting', false);
    }
  }
});
<|endoftext|>


<commit_before><commit_msg>fix mapbox layer<commit_after><|endoftext|>


<commit_before>const ObjectId = require('bson-objectid').default;
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const commands = require('../schema').commands;

const MIGRATION_USER = 1;

const messages = {
    permissionRoleActionError: 'Cannot {action} permission({permission}) with role({role}) - {resource} does not exist'
};

/**
 * Creates a migrations which will add a new table from schema.js to the database
 * @param {string} name - table name
 * @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration,
 * this parameter MUST be present, otherwise @daniellockyer will hunt you down
 *
 * @returns {Object} migration object returning config/up/down properties
 */
function addTable(name, tableSpec) {
    return createNonTransactionalMigration(
        async function up(connection) {
            const tableExists = await connection.schema.hasTable(name);
            if (tableExists) {
                logging.warn(`Skipping adding table: ${name} - table already exists`);
                return;
            }

            logging.info(`Adding table: ${name}`);
            return commands.createTable(name, connection, tableSpec);
        },
        async function down(connection) {
            const tableExists = await connection.schema.hasTable(name);
            if (!tableExists) {
                logging.warn(`Skipping dropping table: ${name} - table does not exist`);
                return;
            }

            logging.info(`Dropping table: ${name}`);
            return commands.deleteTable(name, connection);
        }
    );
}

/**
 * Creates migration which will drop a table
 *
 * @param {[string]} names  - names of the tables to drop
 */
function dropTables(names) {
    return createIrreversibleMigration(
        async function up(connection) {
            for (const name of names) {
                const exists = await connection.schema.hasTable(name);

                if (!exists) {
                    logging.warn(`Failed to drop table: ${name} - table does not exist`);
                } else {
                    logging.info(`Dropping table: ${name}`);
                    await commands.deleteTable(name, connection);
                }
            }
        }
    );
}

/**
 * Creates a migration which will add a permission to the database
 *
 * @param {Object} config
 * @param {string} config.name - The name of the permission
 * @param {string} config.action - The action_type of the permission
 * @param {string} config.object - The object_type of the permission
 *
 * @returns {Migration}
 */
function addPermission(config) {
    return createTransactionalMigration(
        async function up(connection) {
            const existingPermission = await connection('permissions').where({
                name: config.name,
                action_type: config.action,
                object_type: config.object
            }).first();

            if (existingPermission) {
                logging.warn(`Permission for ${config.action}:${config.object} already added`);
                return;
            }

            logging.info(`Adding permission for ${config.action}:${config.object}`);

            const date = connection.raw('CURRENT_TIMESTAMP');

            await connection('permissions').insert({
                id: ObjectId().toHexString(),
                name: config.name,
                action_type: config.action,
                object_type: config.object,
                created_at: date,
                created_by: MIGRATION_USER,
                updated_at: date,
                updated_by: MIGRATION_USER
            });
        },
        async function down(connection) {
            const existingPermission = await connection('permissions').where({
                name: config.name,
                action_type: config.action,
                object_type: config.object
            }).first();

            if (!existingPermission) {
                logging.warn(`Permission for ${config.action}:${config.object} already removed`);
                return;
            }

            logging.info(`Removing permission for ${config.action}:${config.object}`);

            await connection('permissions').where({
                action_type: config.action,
                object_type: config.object
            }).del();
        }
    );
}

/**
 * Creates a migration which will link a permission to a role in the database
 *
 * @param {Object} config
 * @param {string} config.permission - The name of the permission
 * @param {string} config.role - The name of the role
 *
 * @returns {Migration}
 */
function addPermissionToRole(config) {
    return createTransactionalMigration(
        async function up(connection) {
            const permission = await connection('permissions').where({
                name: config.permission
            }).first();

            if (!permission) {
                throw new errors.InternalServerError({
                    message: tpl(messages.permissionRoleActionError, {
                        action: 'add',
                        permission: config.permission,
                        role: config.role,
                        resource: 'permission'
                    })
                });
            }

            const role = await connection('roles').where({
                name: config.role
            }).first();

            if (!role) {
                throw new errors.InternalServerError({
                    message: tpl(messages.permissionRoleActionError, {
                        action: 'add',
                        permission: config.permission,
                        role: config.role,
                        resource: 'role'
                    })
                });
            }

            const existingRelation = await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).first();

            if (existingRelation) {
                logging.warn(`Adding permission(${config.permission}) to role(${config.role}) - already exists`);
                return;
            }

            logging.warn(`Adding permission(${config.permission}) to role(${config.role})`);
            await connection('permissions_roles').insert({
                id: ObjectId().toHexString(),
                permission_id: permission.id,
                role_id: role.id
            });
        },
        async function down(connection) {
            const permission = await connection('permissions').where({
                name: config.permission
            }).first();

            if (!permission) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Permission not found.`);
                return;
            }

            const role = await connection('roles').where({
                name: config.role
            }).first();

            if (!role) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Role not found.`);
                return;
            }

            const existingRelation = await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).first();

            if (!existingRelation) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - already removed`);
                return;
            }

            logging.info(`Removing permission(${config.permission}) from role(${config.role})`);
            await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).del();
        }
    );
}

/**
 * Creates a migration which will add a permission to the database, and then link it to roles
 *
 * @param {Object} config
 * @param {string} config.name - The name of the permission
 * @param {string} config.action - The action_type of the permission
 * @param {string} config.object - The object_type of the permission
 *
 * @param {string[]} roles - A list of role names
 *
 * @returns {Migration}
 */
function addPermissionWithRoles(config, roles) {
    return combineTransactionalMigrations(
        addPermission(config),
        ...roles.map((role => addPermissionToRole({permission: config.name, role})))
    );
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 * @param {(connection: import('knex')) => Promise<void>} down
 *
 * @returns {Migration}
 */
function createNonTransactionalMigration(up, down) {
    return {
        config: {
            transaction: false
        },
        async up(config) {
            await up(config.connection);
        },
        async down(config) {
            await down(config.connection);
        }
    };
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 *
 * @returns {Migration}
 */
function createIrreversibleMigration(up) {
    return {
        config: {
            irreversible: true
        },
        async up(config) {
            await up(config.connection);
        },
        async down() {
            return Promise.reject();
        }
    };
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 * @param {(connection: import('knex')) => Promise<void>} down
 *
 * @returns {Migration}
 */
function createTransactionalMigration(up, down) {
    return {
        config: {
            transaction: true
        },
        async up(config) {
            await up(config.transacting);
        },
        async down(config) {
            await down(config.transacting);
        }
    };
}

/**
 * @param {Migration[]} migrations
 *
 * @returns {Migration}
 */
function combineTransactionalMigrations(...migrations) {
    return {
        config: {
            transaction: true
        },
        async up(config) {
            for (const migration of migrations) {
                await migration.up(config);
            }
        },
        async down(config) {
            // Down migrations must be run backwards!!
            const reverseMigrations = migrations.slice().reverse();
            for (const migration of reverseMigrations) {
                await migration.down(config);
            }
        }
    };
}

/**
 * @param {Migration[]} migrations
 *
 * @returns {Migration}
 */
function combineNonTransactionalMigrations(...migrations) {
    return {
        config: {
            transaction: false
        },
        async up(config) {
            for (const migration of migrations) {
                await migration.up(config);
            }
        },
        async down(config) {
            // Down migrations must be run backwards!!
            const reverseMigrations = migrations.slice().reverse();
            for (const migration of reverseMigrations) {
                await migration.down(config);
            }
        }
    };
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createAddColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing'
        })
    );
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createDropColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing'
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        })
    );
}

/**
 * Creates a migration which will insert a new setting in settings table
 * @param {object} settingSpec - setting key, value, group and type
 *
 * @returns {Object} migration object returning config/up/down properties
 */
function addSetting({key, value, type, group}) {
    return createTransactionalMigration(
        async function up(connection) {
            const settingExists = await connection('settings')
                .where('key', '=', key)
                .first();
            if (settingExists) {
                logging.warn(`Skipping adding setting: ${key} - setting already exists`);
                return;
            }

            logging.info(`Adding setting: ${key}`);
            const now = connection.raw('CURRENT_TIMESTAMP');

            return connection('settings')
                .insert({
                    id: ObjectId().toHexString(),
                    key,
                    value,
                    group,
                    type,
                    created_at: now,
                    created_by: MIGRATION_USER,
                    updated_at: now,
                    updated_by: MIGRATION_USER
                });
        },
        async function down(connection) {
            const settingExists = await connection('settings')
                .where('key', '=', key)
                .first();
            if (!settingExists) {
                logging.warn(`Skipping dropping setting: ${key} - setting does not exist`);
                return;
            }

            logging.info(`Dropping setting: ${key}`);
            return connection('settings')
                .where('key', '=', key)
                .del();
        }
    );
}

module.exports = {
    addTable,
    dropTables,
    addPermission,
    addPermissionToRole,
    addPermissionWithRoles,
    addSetting,
    createTransactionalMigration,
    createNonTransactionalMigration,
    createIrreversibleMigration,
    combineTransactionalMigrations,
    combineNonTransactionalMigrations,
    createAddColumnMigration,
    createDropColumnMigration,
    meta: {
        MIGRATION_USER
    }
};

/**
 * @typedef {Object} TransactionalMigrationFunctionOptions
 *
 * @prop {import('knex')} transacting
 */

/**
 * @typedef {(options: TransactionalMigrationFunctionOptions) => Promise<void>} TransactionalMigrationFunction
 */

/**
 * @typedef {Object} Migration
 *
 * @prop {Object} config
 * @prop {boolean} config.transaction
 *
 * @prop {TransactionalMigrationFunction} up
 * @prop {TransactionalMigrationFunction} down
 */
<commit_msg>Fixed logging level when adding permissions to roles<commit_after>const ObjectId = require('bson-objectid').default;
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const commands = require('../schema').commands;

const MIGRATION_USER = 1;

const messages = {
    permissionRoleActionError: 'Cannot {action} permission({permission}) with role({role}) - {resource} does not exist'
};

/**
 * Creates a migrations which will add a new table from schema.js to the database
 * @param {string} name - table name
 * @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration,
 * this parameter MUST be present, otherwise @daniellockyer will hunt you down
 *
 * @returns {Object} migration object returning config/up/down properties
 */
function addTable(name, tableSpec) {
    return createNonTransactionalMigration(
        async function up(connection) {
            const tableExists = await connection.schema.hasTable(name);
            if (tableExists) {
                logging.warn(`Skipping adding table: ${name} - table already exists`);
                return;
            }

            logging.info(`Adding table: ${name}`);
            return commands.createTable(name, connection, tableSpec);
        },
        async function down(connection) {
            const tableExists = await connection.schema.hasTable(name);
            if (!tableExists) {
                logging.warn(`Skipping dropping table: ${name} - table does not exist`);
                return;
            }

            logging.info(`Dropping table: ${name}`);
            return commands.deleteTable(name, connection);
        }
    );
}

/**
 * Creates migration which will drop a table
 *
 * @param {[string]} names  - names of the tables to drop
 */
function dropTables(names) {
    return createIrreversibleMigration(
        async function up(connection) {
            for (const name of names) {
                const exists = await connection.schema.hasTable(name);

                if (!exists) {
                    logging.warn(`Failed to drop table: ${name} - table does not exist`);
                } else {
                    logging.info(`Dropping table: ${name}`);
                    await commands.deleteTable(name, connection);
                }
            }
        }
    );
}

/**
 * Creates a migration which will add a permission to the database
 *
 * @param {Object} config
 * @param {string} config.name - The name of the permission
 * @param {string} config.action - The action_type of the permission
 * @param {string} config.object - The object_type of the permission
 *
 * @returns {Migration}
 */
function addPermission(config) {
    return createTransactionalMigration(
        async function up(connection) {
            const existingPermission = await connection('permissions').where({
                name: config.name,
                action_type: config.action,
                object_type: config.object
            }).first();

            if (existingPermission) {
                logging.warn(`Permission for ${config.action}:${config.object} already added`);
                return;
            }

            logging.info(`Adding permission for ${config.action}:${config.object}`);

            const date = connection.raw('CURRENT_TIMESTAMP');

            await connection('permissions').insert({
                id: ObjectId().toHexString(),
                name: config.name,
                action_type: config.action,
                object_type: config.object,
                created_at: date,
                created_by: MIGRATION_USER,
                updated_at: date,
                updated_by: MIGRATION_USER
            });
        },
        async function down(connection) {
            const existingPermission = await connection('permissions').where({
                name: config.name,
                action_type: config.action,
                object_type: config.object
            }).first();

            if (!existingPermission) {
                logging.warn(`Permission for ${config.action}:${config.object} already removed`);
                return;
            }

            logging.info(`Removing permission for ${config.action}:${config.object}`);

            await connection('permissions').where({
                action_type: config.action,
                object_type: config.object
            }).del();
        }
    );
}

/**
 * Creates a migration which will link a permission to a role in the database
 *
 * @param {Object} config
 * @param {string} config.permission - The name of the permission
 * @param {string} config.role - The name of the role
 *
 * @returns {Migration}
 */
function addPermissionToRole(config) {
    return createTransactionalMigration(
        async function up(connection) {
            const permission = await connection('permissions').where({
                name: config.permission
            }).first();

            if (!permission) {
                throw new errors.InternalServerError({
                    message: tpl(messages.permissionRoleActionError, {
                        action: 'add',
                        permission: config.permission,
                        role: config.role,
                        resource: 'permission'
                    })
                });
            }

            const role = await connection('roles').where({
                name: config.role
            }).first();

            if (!role) {
                throw new errors.InternalServerError({
                    message: tpl(messages.permissionRoleActionError, {
                        action: 'add',
                        permission: config.permission,
                        role: config.role,
                        resource: 'role'
                    })
                });
            }

            const existingRelation = await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).first();

            if (existingRelation) {
                logging.warn(`Adding permission(${config.permission}) to role(${config.role}) - already exists`);
                return;
            }

            logging.info(`Adding permission(${config.permission}) to role(${config.role})`);
            await connection('permissions_roles').insert({
                id: ObjectId().toHexString(),
                permission_id: permission.id,
                role_id: role.id
            });
        },
        async function down(connection) {
            const permission = await connection('permissions').where({
                name: config.permission
            }).first();

            if (!permission) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Permission not found.`);
                return;
            }

            const role = await connection('roles').where({
                name: config.role
            }).first();

            if (!role) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Role not found.`);
                return;
            }

            const existingRelation = await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).first();

            if (!existingRelation) {
                logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - already removed`);
                return;
            }

            logging.info(`Removing permission(${config.permission}) from role(${config.role})`);
            await connection('permissions_roles').where({
                permission_id: permission.id,
                role_id: role.id
            }).del();
        }
    );
}

/**
 * Creates a migration which will add a permission to the database, and then link it to roles
 *
 * @param {Object} config
 * @param {string} config.name - The name of the permission
 * @param {string} config.action - The action_type of the permission
 * @param {string} config.object - The object_type of the permission
 *
 * @param {string[]} roles - A list of role names
 *
 * @returns {Migration}
 */
function addPermissionWithRoles(config, roles) {
    return combineTransactionalMigrations(
        addPermission(config),
        ...roles.map((role => addPermissionToRole({permission: config.name, role})))
    );
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 * @param {(connection: import('knex')) => Promise<void>} down
 *
 * @returns {Migration}
 */
function createNonTransactionalMigration(up, down) {
    return {
        config: {
            transaction: false
        },
        async up(config) {
            await up(config.connection);
        },
        async down(config) {
            await down(config.connection);
        }
    };
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 *
 * @returns {Migration}
 */
function createIrreversibleMigration(up) {
    return {
        config: {
            irreversible: true
        },
        async up(config) {
            await up(config.connection);
        },
        async down() {
            return Promise.reject();
        }
    };
}

/**
 * @param {(connection: import('knex')) => Promise<void>} up
 * @param {(connection: import('knex')) => Promise<void>} down
 *
 * @returns {Migration}
 */
function createTransactionalMigration(up, down) {
    return {
        config: {
            transaction: true
        },
        async up(config) {
            await up(config.transacting);
        },
        async down(config) {
            await down(config.transacting);
        }
    };
}

/**
 * @param {Migration[]} migrations
 *
 * @returns {Migration}
 */
function combineTransactionalMigrations(...migrations) {
    return {
        config: {
            transaction: true
        },
        async up(config) {
            for (const migration of migrations) {
                await migration.up(config);
            }
        },
        async down(config) {
            // Down migrations must be run backwards!!
            const reverseMigrations = migrations.slice().reverse();
            for (const migration of reverseMigrations) {
                await migration.down(config);
            }
        }
    };
}

/**
 * @param {Migration[]} migrations
 *
 * @returns {Migration}
 */
function combineNonTransactionalMigrations(...migrations) {
    return {
        config: {
            transaction: false
        },
        async up(config) {
            for (const migration of migrations) {
                await migration.up(config);
            }
        },
        async down(config) {
            // Down migrations must be run backwards!!
            const reverseMigrations = migrations.slice().reverse();
            for (const migration of reverseMigrations) {
                await migration.down(config);
            }
        }
    };
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createAddColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing'
        })
    );
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createDropColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing'
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        })
    );
}

/**
 * Creates a migration which will insert a new setting in settings table
 * @param {object} settingSpec - setting key, value, group and type
 *
 * @returns {Object} migration object returning config/up/down properties
 */
function addSetting({key, value, type, group}) {
    return createTransactionalMigration(
        async function up(connection) {
            const settingExists = await connection('settings')
                .where('key', '=', key)
                .first();
            if (settingExists) {
                logging.warn(`Skipping adding setting: ${key} - setting already exists`);
                return;
            }

            logging.info(`Adding setting: ${key}`);
            const now = connection.raw('CURRENT_TIMESTAMP');

            return connection('settings')
                .insert({
                    id: ObjectId().toHexString(),
                    key,
                    value,
                    group,
                    type,
                    created_at: now,
                    created_by: MIGRATION_USER,
                    updated_at: now,
                    updated_by: MIGRATION_USER
                });
        },
        async function down(connection) {
            const settingExists = await connection('settings')
                .where('key', '=', key)
                .first();
            if (!settingExists) {
                logging.warn(`Skipping dropping setting: ${key} - setting does not exist`);
                return;
            }

            logging.info(`Dropping setting: ${key}`);
            return connection('settings')
                .where('key', '=', key)
                .del();
        }
    );
}

module.exports = {
    addTable,
    dropTables,
    addPermission,
    addPermissionToRole,
    addPermissionWithRoles,
    addSetting,
    createTransactionalMigration,
    createNonTransactionalMigration,
    createIrreversibleMigration,
    combineTransactionalMigrations,
    combineNonTransactionalMigrations,
    createAddColumnMigration,
    createDropColumnMigration,
    meta: {
        MIGRATION_USER
    }
};

/**
 * @typedef {Object} TransactionalMigrationFunctionOptions
 *
 * @prop {import('knex')} transacting
 */

/**
 * @typedef {(options: TransactionalMigrationFunctionOptions) => Promise<void>} TransactionalMigrationFunction
 */

/**
 * @typedef {Object} Migration
 *
 * @prop {Object} config
 * @prop {boolean} config.transaction
 *
 * @prop {TransactionalMigrationFunction} up
 * @prop {TransactionalMigrationFunction} down
 */
<|endoftext|>


<commit_before>import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Roles } from 'meteor/alanning:roles';

import { Queues } from '/imports/api/queues/queues';

import { WebNotifications } from '/imports/lib/client/web-notifications';

import '/imports/ui/components/queue-header/queue-header';
import '/imports/ui/components/alerts/alert-ending-soon/alert-ending-soon';
import '/imports/ui/components/alerts/alert-signup-gap/alert-signup-gap';
import '/imports/ui/components/queue-actions/queue-actions';
import '/imports/ui/components/queue-alert-restricted-session/queue-alert-restricted-session';
import '/imports/ui/components/queue-table/queue-table';
import '/imports/ui/components/modals/modal-queue-edit/modal-queue-edit';
import '/imports/ui/components/modals/modal-join-queue/modal-join-queue';

import './queue.html';

Template.Queue.onCreated(function onCreated() {
  this.getQueueId = () => { return FlowRouter.getParam('queueId'); };
  this.getView = () => { return FlowRouter.getQueryParam('view'); };
  this.getQueue = () => { return Queues.findOne(this.getQueueId()); };

  this.autorun(() => {
    this.subscribe('queues.byId', this.getQueueId());

    const queue = this.getQueue();
    if (queue) {
      this.subscribe('courses.byId', queue.courseId);
      this.subscribe('tickets.byQueueId', queue._id);
    }
  });
});

Template.Queue.onRendered(function onRendered() {
  WebNotifications.requestPermission();
  this.autorun(() => {
    if (this.subscriptionsReady()) {
      const queue = this.getQueue();
      if (!queue) {
        FlowRouter.go('/404');
        return;
      }
      document.title = `(${queue.activeTickets().count()}) ${queue.course().name} · ${queue.name} · SignMeUp`; // eslint-disable-line max-len

      // setup notifications
      if (!WebNotifications.canNotify()) return;
      if (Template.Queue.__helpers.get('taView')()) {
        var initial = true;
        queue.activeTickets().observe({
          added: (ticket) => {
            if (initial) return;
            WebNotifications.send('Someone has joined the queue2');
          },
        });
        initial = false;
      }
    }
  });
});

Template.Queue.helpers({
  queue() {
    return Template.instance().getQueue();
  },

  taView() {
    if (Template.instance().getView() === 'student') {
      return false;
    }

    const queue = Template.instance().getQueue();
    const courseId = queue && queue.courseId;
    if (Roles.userIsInRole(Meteor.user(), ['admin', 'mta', 'hta', 'ta'], courseId)) {
      return true;
    }

    return false;
  },
});
<commit_msg>Alert TAs with the name of the student<commit_after>import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Roles } from 'meteor/alanning:roles';

import { Queues } from '/imports/api/queues/queues';

import { WebNotifications } from '/imports/lib/client/web-notifications';

import '/imports/ui/components/queue-header/queue-header';
import '/imports/ui/components/alerts/alert-ending-soon/alert-ending-soon';
import '/imports/ui/components/alerts/alert-signup-gap/alert-signup-gap';
import '/imports/ui/components/queue-actions/queue-actions';
import '/imports/ui/components/queue-alert-restricted-session/queue-alert-restricted-session';
import '/imports/ui/components/queue-table/queue-table';
import '/imports/ui/components/modals/modal-queue-edit/modal-queue-edit';
import '/imports/ui/components/modals/modal-join-queue/modal-join-queue';

import './queue.html';

Template.Queue.onCreated(function onCreated() {
  this.getQueueId = () => { return FlowRouter.getParam('queueId'); };
  this.getView = () => { return FlowRouter.getQueryParam('view'); };
  this.getQueue = () => { return Queues.findOne(this.getQueueId()); };

  this.autorun(() => {
    this.subscribe('queues.byId', this.getQueueId());

    const queue = this.getQueue();
    if (queue) {
      this.subscribe('courses.byId', queue.courseId);
      this.subscribe('tickets.byQueueId', queue._id);
    }
  });
});

// is the current user a TA (or above)?
const isTA = () => {
  if (Template.instance().getView() === 'student') {
    return false;
  }

  const queue = Template.instance().getQueue();
  const courseId = queue && queue.courseId;
  if (Roles.userIsInRole(Meteor.user(), ['admin', 'mta', 'hta', 'ta'], courseId)) {
    return true;
  }

  return false;
};

Template.Queue.onRendered(function onRendered() {
  WebNotifications.requestPermission();
  this.autorun(() => {
    if (this.subscriptionsReady()) {
      const queue = this.getQueue();
      if (!queue) {
        FlowRouter.go('/404');
        return;
      }
      document.title = `(${queue.activeTickets().count()}) ${queue.course().name} · ${queue.name} · SignMeUp`; // eslint-disable-line max-len

      // setup notifications
      if (!WebNotifications.canNotify()) return;
      if (isTA()) {
        let initial = true;
        queue.activeTickets().observe({
          added: (ticket) => {
            if (initial) return;
            const subscription = this.subscribe('users.byIds', ticket.studentIds, () => {
              const students = ticket.students().fetch();
              const names = students.map(student => student.fullName());
              const verb = names.length === 1 ? 'has' : 'have';
              WebNotifications.send(`${names.join(', ')} ${verb} joined the queue`);
              subscription.stop();
            });
          },
        });
        initial = false;
      }
    }
  });
});

Template.Queue.helpers({
  queue() {
    return Template.instance().getQueue();
  },

  taView() {
    return isTA();
  },
});
<|endoftext|>


<commit_before>'use strict';

module.exports = {
    compactNode: compactNode,
    compactGraph: compactGraph
};

function findNextEnd(prev, v, vertices, ends, vertexCoords, edgeData, trackIncoming, options) {
    var weight = vertices[prev][v],
        reverseWeight = vertices[v][prev],
        coordinates = [],
        path = [],
        reducedEdge = options.edgeDataSeed;

    if (options.edgeDataReduceFn) {
        reducedEdge = options.edgeDataReduceFn(reducedEdge, edgeData[v][prev]);
    }

    while (!ends[v]) {
        var edges = vertices[v];

        if (!edges) { break; }

        var next = Object.keys(edges).filter(function notPrevious(k) { return k !== prev; })[0];
        weight += edges[next];

        if (trackIncoming) {
            reverseWeight += vertices[next][v];

            if (path.indexOf(v) >= 0) {
                ends[v] = vertices[v];
                break;
            }
            path.push(v);
        }

        if (options.edgeDataReduceFn) {
            reducedEdge = options.edgeDataReduceFn(reducedEdge, edgeData[v][next]);
        }

        coordinates.push(vertexCoords[v]);
        prev = v;
        v = next;
    }

    return {
        vertex: v,
        weight: weight,
        reverseWeight: reverseWeight,
        coordinates: coordinates,
        reducedEdge: reducedEdge
    };
}

function compactNode(k, vertices, ends, vertexCoords, edgeData, trackIncoming, options) {
    options = options || {};
    var neighbors = vertices[k];
    return Object.keys(neighbors).reduce(function compactEdge(result, j) {
        var neighbor = findNextEnd(k, j, vertices, ends, vertexCoords, edgeData, trackIncoming, options);
        var weight = neighbor.weight;
        var reverseWeight = neighbor.reverseWeight;
        if (neighbor.vertex !== k) {
            if (!result.edges[neighbor.vertex] || result.edges[neighbor.vertex] > weight) {
                result.edges[neighbor.vertex] = weight;
                result.coordinates[neighbor.vertex] = [vertexCoords[k]].concat(neighbor.coordinates);
                result.reducedEdges[neighbor.vertex] = neighbor.reducedEdge;
            }
            if (trackIncoming &&
                !isNaN(reverseWeight) && (!result.incomingEdges[neighbor.vertex] || result.incomingEdges[neighbor.vertex] > reverseWeight)) {
                result.incomingEdges[neighbor.vertex] = reverseWeight;
                var coordinates = [vertexCoords[k]].concat(neighbor.coordinates);
                coordinates.reverse();
                result.incomingCoordinates[neighbor.vertex] = coordinates;
            }
        }
        return result;
    }, {edges: {}, incomingEdges: {}, coordinates: {}, incomingCoordinates: {}, reducedEdges: {}});
}

function compactGraph(vertices, vertexCoords, edgeData, options) {
    options = options || {};
    var progress = options.progress;
    var ends = Object.keys(vertices).reduce(function findEnds(es, k, i, vs) {
        var vertex = vertices[k];
        var edges = Object.keys(vertex);
        var numberEdges = edges.length;
        var remove;

        if (options.compact === false) {
            remove = false;
        } else if (numberEdges === 1) {
            var other = vertices[edges[0]];
            remove = !other[k];
        } else if (numberEdges === 2) {
            remove = edges.filter(function(n) {
                return vertices[n][k];
            }).length === numberEdges;
        } else {
            remove = false;
        }

        if (!remove) {
            es[k] = vertex;
        }

        if (i % 1000 === 0 && progress) {
            progress('compact:ends', i, vs.length);
        }

        return es;
    }, {});

    return Object.keys(ends).reduce(function compactEnd(result, k, i, es) {
        var compacted = compactNode(k, vertices, ends, vertexCoords, edgeData, false, options);
        result.graph[k] = compacted.edges;
        result.coordinates[k] = compacted.coordinates;

        if (options.edgeDataReduceFn) {
            result.reducedEdges[k] = compacted.reducedEdges;
        }

        if (i % 1000 === 0 && progress) {
            progress('compact:nodes', i, es.length);
        }

        return result;
    }, {graph: {}, coordinates: {}, reducedEdges: {}});
};
<commit_msg>Allow any falsy value for options.compact<commit_after>'use strict';

module.exports = {
    compactNode: compactNode,
    compactGraph: compactGraph
};

function findNextEnd(prev, v, vertices, ends, vertexCoords, edgeData, trackIncoming, options) {
    var weight = vertices[prev][v],
        reverseWeight = vertices[v][prev],
        coordinates = [],
        path = [],
        reducedEdge = options.edgeDataSeed;

    if (options.edgeDataReduceFn) {
        reducedEdge = options.edgeDataReduceFn(reducedEdge, edgeData[v][prev]);
    }

    while (!ends[v]) {
        var edges = vertices[v];

        if (!edges) { break; }

        var next = Object.keys(edges).filter(function notPrevious(k) { return k !== prev; })[0];
        weight += edges[next];

        if (trackIncoming) {
            reverseWeight += vertices[next][v];

            if (path.indexOf(v) >= 0) {
                ends[v] = vertices[v];
                break;
            }
            path.push(v);
        }

        if (options.edgeDataReduceFn) {
            reducedEdge = options.edgeDataReduceFn(reducedEdge, edgeData[v][next]);
        }

        coordinates.push(vertexCoords[v]);
        prev = v;
        v = next;
    }

    return {
        vertex: v,
        weight: weight,
        reverseWeight: reverseWeight,
        coordinates: coordinates,
        reducedEdge: reducedEdge
    };
}

function compactNode(k, vertices, ends, vertexCoords, edgeData, trackIncoming, options) {
    options = options || {};
    var neighbors = vertices[k];
    return Object.keys(neighbors).reduce(function compactEdge(result, j) {
        var neighbor = findNextEnd(k, j, vertices, ends, vertexCoords, edgeData, trackIncoming, options);
        var weight = neighbor.weight;
        var reverseWeight = neighbor.reverseWeight;
        if (neighbor.vertex !== k) {
            if (!result.edges[neighbor.vertex] || result.edges[neighbor.vertex] > weight) {
                result.edges[neighbor.vertex] = weight;
                result.coordinates[neighbor.vertex] = [vertexCoords[k]].concat(neighbor.coordinates);
                result.reducedEdges[neighbor.vertex] = neighbor.reducedEdge;
            }
            if (trackIncoming &&
                !isNaN(reverseWeight) && (!result.incomingEdges[neighbor.vertex] || result.incomingEdges[neighbor.vertex] > reverseWeight)) {
                result.incomingEdges[neighbor.vertex] = reverseWeight;
                var coordinates = [vertexCoords[k]].concat(neighbor.coordinates);
                coordinates.reverse();
                result.incomingCoordinates[neighbor.vertex] = coordinates;
            }
        }
        return result;
    }, {edges: {}, incomingEdges: {}, coordinates: {}, incomingCoordinates: {}, reducedEdges: {}});
}

function compactGraph(vertices, vertexCoords, edgeData, options) {
    options = options || {};
    var progress = options.progress;
    var ends = Object.keys(vertices).reduce(function findEnds(es, k, i, vs) {
        var vertex = vertices[k];
        var edges = Object.keys(vertex);
        var numberEdges = edges.length;
        var remove;

        if (options.compact !== undefined && !options.compact) {
            remove = false;
        } else if (numberEdges === 1) {
            var other = vertices[edges[0]];
            remove = !other[k];
        } else if (numberEdges === 2) {
            remove = edges.filter(function(n) {
                return vertices[n][k];
            }).length === numberEdges;
        } else {
            remove = false;
        }

        if (!remove) {
            es[k] = vertex;
        }

        if (i % 1000 === 0 && progress) {
            progress('compact:ends', i, vs.length);
        }

        return es;
    }, {});

    return Object.keys(ends).reduce(function compactEnd(result, k, i, es) {
        var compacted = compactNode(k, vertices, ends, vertexCoords, edgeData, false, options);
        result.graph[k] = compacted.edges;
        result.coordinates[k] = compacted.coordinates;

        if (options.edgeDataReduceFn) {
            result.reducedEdges[k] = compacted.reducedEdges;
        }

        if (i % 1000 === 0 && progress) {
            progress('compact:nodes', i, es.length);
        }

        return result;
    }, {graph: {}, coordinates: {}, reducedEdges: {}});
};
<|endoftext|>


<commit_before>import RBush from 'rbush';

import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';

import { currentLocale } from '../util/locale';
import { geoExtent, geoVecAdd } from '../geo';
import { QAItem } from '../osm';
import { utilRebind, utilTiler, utilQsString } from '../util';
import * as qaServices from '../../data/qa_data.json';

const tiler = utilTiler();
const dispatch = d3_dispatch('loaded');
const _tileZoom = 14;
const _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/en/api/0.3beta';
const _osmoseItems =
  Object.keys(qaServices.osmose.icons)
    .map(s => s.split('-')[0])
    .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);

// This gets reassigned if reset
let _cache;

function abortRequest(controller) {
  if (controller) {
    controller.abort();
  }
}

function abortUnwantedRequests(cache, tiles) {
  Object.keys(cache.inflightTile).forEach(k => {
    let wanted = tiles.find(tile => k === tile.id);
    if (!wanted) {
      abortRequest(cache.inflightTile[k]);
      delete cache.inflightTile[k];
    }
  });
}

function encodeIssueRtree(d) {
  return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
}

// Replace or remove QAItem from rtree
function updateRtree(item, replace) {
  _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);

  if (replace) {
    _cache.rtree.insert(item);
  }
}

// Issues shouldn't obscure eachother
function preventCoincident(loc) {
  let coincident = false;
  do {
    // first time, move marker up. after that, move marker right.
    let delta = coincident ? [0.00001, 0] : [0, 0.00001];
    loc = geoVecAdd(loc, delta);
    let bbox = geoExtent(loc).bbox();
    coincident = _cache.rtree.search(bbox).length;
  } while (coincident);

  return loc;
}

export default {
  init() {
    if (!_cache) {
      this.reset();
    }

    this.event = utilRebind(this, dispatch, 'on');
  },

  reset() {
    if (_cache) {
      Object.values(_cache.inflightTile).forEach(abortRequest);
    }
    _cache = {
      data: {},
      loadedTile: {},
      inflightTile: {},
      inflightPost: {},
      closed: {},
      rtree: new RBush(),
      strings: {},
      colors: {}
    };
  },

  loadIssues(projection) {
    let params = {
      // Tiles return a maximum # of issues
      // So we want to filter our request for only types iD supports
      item: _osmoseItems
    };

    // determine the needed tiles to cover the view
    let tiles = tiler
      .zoomExtent([_tileZoom, _tileZoom])
      .getTiles(projection);

    // abort inflight requests that are no longer needed
    abortUnwantedRequests(_cache, tiles);

    // issue new requests..
    tiles.forEach(tile => {
      if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;

      let [ x, y, z ] = tile.xyz;
      let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.json?` + utilQsString(params);

      let controller = new AbortController();
      _cache.inflightTile[tile.id] = controller;

      d3_json(url, { signal: controller.signal })
        .then(data => {
          delete _cache.inflightTile[tile.id];
          _cache.loadedTile[tile.id] = true;

          if (data.features) {
            data.features.forEach(issue => {
              const { item, class: cl, uuid: id } = issue.properties;
              /* Osmose issues are uniquely identified by a unique
                `item` and `class` combination (both integer values) */
              const itemType = `${item}-${cl}`;

              // Filter out unsupported issue types (some are too specific or advanced)
              if (itemType in qaServices.osmose.icons) {
                let loc = issue.geometry.coordinates; // lon, lat
                loc = preventCoincident(loc);

                let d = new QAItem(loc, 'osmose', itemType, id, { item });

                // Setting elems here prevents UI detail requests
                if (item === 8300 || item === 8360) {
                  d.elems = [];
                }

                _cache.data[d.id] = d;
                _cache.rtree.insert(encodeIssueRtree(d));
              }
            });
          }

          dispatch.call('loaded');
        })
        .catch(() => {
          delete _cache.inflightTile[tile.id];
          _cache.loadedTile[tile.id] = true;
        });
    });
  },

  loadIssueDetail(issue) {
    // Issue details only need to be fetched once
    if (issue.elems !== undefined) {
      return Promise.resolve(issue);
    }

    const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${currentLocale}`;
    const cacheDetails = data => {
      // Associated elements used for highlighting
      // Assign directly for immediate use in the callback
      issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);

      // Some issues have instance specific detail in a subtitle
      issue.detail = data.subtitle;

      this.replaceItem(issue);
    };

    return jsonPromise(url, cacheDetails).then(() => issue);
  },

  loadStrings(locale=currentLocale) {
    const items = Object.keys(qaServices.osmose.icons);

    if (
      locale in _cache.strings
      && Object.keys(_cache.strings[locale]).length === items.length
    ) {
        return Promise.resolve(_cache.strings[locale]);
    }

    // May be partially populated already if some requests were successful
    if (!(locale in _cache.strings)) {
      _cache.strings[locale] = {};
    }

    const format = string => {
      // Some strings contain markdown syntax
      string = string.replace(/\[((?:.|\n)+?)\]\((.+?)\)/g, '$1');
      return string.replace(/`(.+?)`/g, '$1');
    };

    // Only need to cache strings for supported issue types
    // Using multiple individual item + class requests to reduce fetched data size
    const allRequests = items.map(itemType => {
      // No need to request data we already have
      if (itemType in _cache.strings[locale]) return;

      const cacheData = data => {
        // Bunch of nested single value arrays of objects
        const [ cat = {items:[]} ] = data.categories;
        const [ item = {class:[]} ] = cat.items;
        const [ cl = null ] = item.class;

        // If null default value is reached, data wasn't as expected (or was empty)
        if (!cl) {
          /* eslint-disable no-console */
          console.log(`Osmose strings request (${itemType}) had unexpected data`);
          /* eslint-enable no-console */
          return;
        }

        // Cache served item colors to automatically style issue markers later
        const { item: itemInt, color } = item;
        if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {
          _cache.colors[itemInt] = color;
        }

        // Value of root key will be null if no string exists
        // If string exists, value is an object with key 'auto' for string
        const { title, detail, fix, trap } = cl;

        let issueStrings = {};
        if (title) issueStrings.title = title.auto;
        if (detail) issueStrings.detail = format(detail.auto);
        if (trap) issueStrings.trap = format(trap.auto);
        if (fix) issueStrings.fix = format(fix.auto);

        _cache.strings[locale][itemType] = issueStrings;
      };

      const [ item, cl ] = itemType.split('-');

      // Osmose API falls back to English strings where untranslated or if locale doesn't exist
      const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;

      return jsonPromise(url, cacheData);
    });

    return Promise.all(allRequests).then(() => _cache.strings[locale]);
  },

  getStrings(itemType, locale=currentLocale) {
    // No need to fallback to English, Osmose API handles this for us
    return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};
  },

  getColor(itemType) {
    return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';
  },

  postUpdate(issue, callback) {
    if (_cache.inflightPost[issue.id]) {
      return callback({ message: 'Issue update already inflight', status: -2 }, issue);
    }

    // UI sets the status to either 'done' or 'false'
    const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;
    const controller = new AbortController();
    const after = () => {
      delete _cache.inflightPost[issue.id];

      this.removeItem(issue);
      if (issue.newStatus === 'done') {
        // Keep track of the number of issues closed per `item` to tag the changeset
        if (!(issue.item in _cache.closed)) {
          _cache.closed[issue.item] = 0;
        }
        _cache.closed[issue.item] += 1;
      }
      if (callback) callback(null, issue);
    };

    _cache.inflightPost[issue.id] = controller;

    fetch(url, { signal: controller.signal })
      .then(after)
      .catch(err => {
        delete _cache.inflightPost[issue.id];
        if (callback) callback(err.message);
      });
  },

  // Get all cached QAItems covering the viewport
  getItems(projection) {
    const viewport = projection.clipExtent();
    const min = [viewport[0][0], viewport[1][1]];
    const max = [viewport[1][0], viewport[0][1]];
    const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();

    return _cache.rtree.search(bbox).map(d => d.data);
  },

  // Get a QAItem from cache
  // NOTE: Don't change method name until UI v3 is merged
  getError(id) {
    return _cache.data[id];
  },

  // Replace a single QAItem in the cache
  replaceItem(item) {
    if (!(item instanceof QAItem) || !item.id) return;

    _cache.data[item.id] = item;
    updateRtree(encodeIssueRtree(item), true); // true = replace
    return item;
  },

  // Remove a single QAItem from the cache
  removeItem(item) {
    if (!(item instanceof QAItem) || !item.id) return;

    delete _cache.data[item.id];
    updateRtree(encodeIssueRtree(item), false); // false = remove
  },

  // Used to populate `closed:osmose:*` changeset tags
  getClosedCounts() {
    return _cache.closed;
  }
};

function jsonPromise(url, then) {
  return new Promise((resolve, reject) => {
    d3_json(url)
      .then(data => {
        then(data);
        resolve();
      })
      .catch(err => {
        reject(err);
      });
  });
}
<commit_msg>Use d3_json Promise directly for Osmose requests<commit_after>import RBush from 'rbush';

import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';

import { currentLocale } from '../util/locale';
import { geoExtent, geoVecAdd } from '../geo';
import { QAItem } from '../osm';
import { utilRebind, utilTiler, utilQsString } from '../util';
import * as qaServices from '../../data/qa_data.json';

const tiler = utilTiler();
const dispatch = d3_dispatch('loaded');
const _tileZoom = 14;
const _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/en/api/0.3beta';
const _osmoseItems =
  Object.keys(qaServices.osmose.icons)
    .map(s => s.split('-')[0])
    .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);

// This gets reassigned if reset
let _cache;

function abortRequest(controller) {
  if (controller) {
    controller.abort();
  }
}

function abortUnwantedRequests(cache, tiles) {
  Object.keys(cache.inflightTile).forEach(k => {
    let wanted = tiles.find(tile => k === tile.id);
    if (!wanted) {
      abortRequest(cache.inflightTile[k]);
      delete cache.inflightTile[k];
    }
  });
}

function encodeIssueRtree(d) {
  return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
}

// Replace or remove QAItem from rtree
function updateRtree(item, replace) {
  _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);

  if (replace) {
    _cache.rtree.insert(item);
  }
}

// Issues shouldn't obscure eachother
function preventCoincident(loc) {
  let coincident = false;
  do {
    // first time, move marker up. after that, move marker right.
    let delta = coincident ? [0.00001, 0] : [0, 0.00001];
    loc = geoVecAdd(loc, delta);
    let bbox = geoExtent(loc).bbox();
    coincident = _cache.rtree.search(bbox).length;
  } while (coincident);

  return loc;
}

export default {
  init() {
    if (!_cache) {
      this.reset();
    }

    this.event = utilRebind(this, dispatch, 'on');
  },

  reset() {
    if (_cache) {
      Object.values(_cache.inflightTile).forEach(abortRequest);
    }
    _cache = {
      data: {},
      loadedTile: {},
      inflightTile: {},
      inflightPost: {},
      closed: {},
      rtree: new RBush(),
      strings: {},
      colors: {}
    };
  },

  loadIssues(projection) {
    let params = {
      // Tiles return a maximum # of issues
      // So we want to filter our request for only types iD supports
      item: _osmoseItems
    };

    // determine the needed tiles to cover the view
    let tiles = tiler
      .zoomExtent([_tileZoom, _tileZoom])
      .getTiles(projection);

    // abort inflight requests that are no longer needed
    abortUnwantedRequests(_cache, tiles);

    // issue new requests..
    tiles.forEach(tile => {
      if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;

      let [ x, y, z ] = tile.xyz;
      let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.json?` + utilQsString(params);

      let controller = new AbortController();
      _cache.inflightTile[tile.id] = controller;

      d3_json(url, { signal: controller.signal })
        .then(data => {
          delete _cache.inflightTile[tile.id];
          _cache.loadedTile[tile.id] = true;

          if (data.features) {
            data.features.forEach(issue => {
              const { item, class: cl, uuid: id } = issue.properties;
              /* Osmose issues are uniquely identified by a unique
                `item` and `class` combination (both integer values) */
              const itemType = `${item}-${cl}`;

              // Filter out unsupported issue types (some are too specific or advanced)
              if (itemType in qaServices.osmose.icons) {
                let loc = issue.geometry.coordinates; // lon, lat
                loc = preventCoincident(loc);

                let d = new QAItem(loc, 'osmose', itemType, id, { item });

                // Setting elems here prevents UI detail requests
                if (item === 8300 || item === 8360) {
                  d.elems = [];
                }

                _cache.data[d.id] = d;
                _cache.rtree.insert(encodeIssueRtree(d));
              }
            });
          }

          dispatch.call('loaded');
        })
        .catch(() => {
          delete _cache.inflightTile[tile.id];
          _cache.loadedTile[tile.id] = true;
        });
    });
  },

  loadIssueDetail(issue) {
    // Issue details only need to be fetched once
    if (issue.elems !== undefined) {
      return Promise.resolve(issue);
    }

    const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${currentLocale}`;
    const cacheDetails = data => {
      // Associated elements used for highlighting
      // Assign directly for immediate use in the callback
      issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);

      // Some issues have instance specific detail in a subtitle
      issue.detail = data.subtitle;

      this.replaceItem(issue);
    };

    return d3_json(url).then(cacheDetails).then(() => issue);
  },

  loadStrings(locale=currentLocale) {
    const items = Object.keys(qaServices.osmose.icons);

    if (
      locale in _cache.strings
      && Object.keys(_cache.strings[locale]).length === items.length
    ) {
        return Promise.resolve(_cache.strings[locale]);
    }

    // May be partially populated already if some requests were successful
    if (!(locale in _cache.strings)) {
      _cache.strings[locale] = {};
    }

    const format = string => {
      // Some strings contain markdown syntax
      string = string.replace(/\[((?:.|\n)+?)\]\((.+?)\)/g, '$1');
      return string.replace(/`(.+?)`/g, '$1');
    };

    // Only need to cache strings for supported issue types
    // Using multiple individual item + class requests to reduce fetched data size
    const allRequests = items.map(itemType => {
      // No need to request data we already have
      if (itemType in _cache.strings[locale]) return;

      const cacheData = data => {
        // Bunch of nested single value arrays of objects
        const [ cat = {items:[]} ] = data.categories;
        const [ item = {class:[]} ] = cat.items;
        const [ cl = null ] = item.class;

        // If null default value is reached, data wasn't as expected (or was empty)
        if (!cl) {
          /* eslint-disable no-console */
          console.log(`Osmose strings request (${itemType}) had unexpected data`);
          /* eslint-enable no-console */
          return;
        }

        // Cache served item colors to automatically style issue markers later
        const { item: itemInt, color } = item;
        if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {
          _cache.colors[itemInt] = color;
        }

        // Value of root key will be null if no string exists
        // If string exists, value is an object with key 'auto' for string
        const { title, detail, fix, trap } = cl;

        let issueStrings = {};
        if (title) issueStrings.title = title.auto;
        if (detail) issueStrings.detail = format(detail.auto);
        if (trap) issueStrings.trap = format(trap.auto);
        if (fix) issueStrings.fix = format(fix.auto);

        _cache.strings[locale][itemType] = issueStrings;
      };

      const [ item, cl ] = itemType.split('-');

      // Osmose API falls back to English strings where untranslated or if locale doesn't exist
      const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;

      return d3_json(url).then(cacheData);
    });

    return Promise.all(allRequests).then(() => _cache.strings[locale]);
  },

  getStrings(itemType, locale=currentLocale) {
    // No need to fallback to English, Osmose API handles this for us
    return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};
  },

  getColor(itemType) {
    return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';
  },

  postUpdate(issue, callback) {
    if (_cache.inflightPost[issue.id]) {
      return callback({ message: 'Issue update already inflight', status: -2 }, issue);
    }

    // UI sets the status to either 'done' or 'false'
    const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;
    const controller = new AbortController();
    const after = () => {
      delete _cache.inflightPost[issue.id];

      this.removeItem(issue);
      if (issue.newStatus === 'done') {
        // Keep track of the number of issues closed per `item` to tag the changeset
        if (!(issue.item in _cache.closed)) {
          _cache.closed[issue.item] = 0;
        }
        _cache.closed[issue.item] += 1;
      }
      if (callback) callback(null, issue);
    };

    _cache.inflightPost[issue.id] = controller;

    fetch(url, { signal: controller.signal })
      .then(after)
      .catch(err => {
        delete _cache.inflightPost[issue.id];
        if (callback) callback(err.message);
      });
  },

  // Get all cached QAItems covering the viewport
  getItems(projection) {
    const viewport = projection.clipExtent();
    const min = [viewport[0][0], viewport[1][1]];
    const max = [viewport[1][0], viewport[0][1]];
    const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();

    return _cache.rtree.search(bbox).map(d => d.data);
  },

  // Get a QAItem from cache
  // NOTE: Don't change method name until UI v3 is merged
  getError(id) {
    return _cache.data[id];
  },

  // Replace a single QAItem in the cache
  replaceItem(item) {
    if (!(item instanceof QAItem) || !item.id) return;

    _cache.data[item.id] = item;
    updateRtree(encodeIssueRtree(item), true); // true = replace
    return item;
  },

  // Remove a single QAItem from the cache
  removeItem(item) {
    if (!(item instanceof QAItem) || !item.id) return;

    delete _cache.data[item.id];
    updateRtree(encodeIssueRtree(item), false); // false = remove
  },

  // Used to populate `closed:osmose:*` changeset tags
  getClosedCounts() {
    return _cache.closed;
  }
};
<|endoftext|>


<commit_before>/**
 * Mouse + Pointerlock controls.
 *
 * Based on: https://github.com/aframevr/aframe/pull/1056
 */
module.exports = {
  schema: {
    enabled: { default: true },
    pointerlockEnabled: { default: true },
    sensitivity: { default: 1 / 25 }
  },

  init: function () {
    this.mouseDown = false;
    this.pointerLocked = false;
    this.lookVector = new THREE.Vector2();
    this.bindMethods();
  },

  play: function () {
    this.addEventListeners();
  },

  pause: function () {
    this.removeEventListeners();
    this.lookVector.set(0, 0);
  },

  remove: function () {
    this.pause();
  },

  bindMethods: function () {
    this.onMouseDown = this.onMouseDown.bind(this);
    this.onMouseMove = this.onMouseMove.bind(this);
    this.onMouseUp = this.onMouseUp.bind(this);
    this.onMouseUp = this.onMouseUp.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
  },

  addEventListeners: function () {
    var sceneEl = this.el.sceneEl;
    var canvasEl = sceneEl.canvas;
    var data = this.data;

    if (!canvasEl) {
      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
      return;
    }

    canvasEl.addEventListener('mousedown', this.onMouseDown, false);
    canvasEl.addEventListener('mousemove', this.onMouseMove, false);
    canvasEl.addEventListener('mouseup', this.onMouseUp, false);
    canvasEl.addEventListener('mouseout', this.onMouseUp, false);

    if (data.pointerlockEnabled) {
      document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
      document.addEventListener('mozpointerlockchange', this.onPointerLockChange, false);
      document.addEventListener('pointerlockerror', this.onPointerLockError, false);
    }
  },

  removeEventListeners: function () {
    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
    if (canvasEl) {
      canvasEl.removeEventListener('mousedown', this.onMouseDown, false);
      canvasEl.removeEventListener('mousemove', this.onMouseMove, false);
      canvasEl.removeEventListener('mouseup', this.onMouseUp, false);
      canvasEl.removeEventListener('mouseout', this.onMouseUp, false);
    }
    document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
    document.removeEventListener('mozpointerlockchange', this.onPointerLockChange, false);
    document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
  },

  isRotationActive: function () {
    return this.data.enabled && (this.mouseDown || this.pointerLocked);
  },

  /**
   * Returns the sum of all mouse movement since last call.
   */
  getRotationDelta: function () {
    var dRotation = this.lookVector.clone().multiplyScalar(this.data.sensitivity);
    this.lookVector.set(0, 0);
    return dRotation;
  },

  onMouseMove: function (event) {
    var previousMouseEvent = this.previousMouseEvent;

    if (!this.data.enabled || !(this.mouseDown || this.pointerLocked)) {
      return;
    }

    var movementX = event.movementX || event.mozMovementX || 0;
    var movementY = event.movementY || event.mozMovementY || 0;

    if (!this.pointerLocked) {
      movementX = event.screenX - previousMouseEvent.screenX;
      movementY = event.screenY - previousMouseEvent.screenY;
    }

    this.lookVector.x += movementX;
    this.lookVector.y += movementY;

    this.previousMouseEvent = event;
  },

  onMouseDown: function (event) {
    var canvasEl = this.el.sceneEl.canvas;

    this.mouseDown = true;
    this.previousMouseEvent = event;

    if (this.data.pointerlockEnabled && !this.pointerLocked) {
      if (canvasEl.requestPointerLock) {
        canvasEl.requestPointerLock();
      } else if (canvasEl.mozRequestPointerLock) {
        canvasEl.mozRequestPointerLock();
      }
    }
  },

  onMouseUp: function () {
    this.mouseDown = false;
  },

  onPointerLockChange: function () {
    this.pointerLocked = !!(document.pointerLockElement || document.mozPointerLockElement);
  },

  onPointerLockError: function () {
    this.pointerLocked = false;
  }
};
<commit_msg>[mouse-controls] No pointerlock if inspector is opened.<commit_after>/**
 * Mouse + Pointerlock controls.
 *
 * Based on: https://github.com/aframevr/aframe/pull/1056
 */
module.exports = {
  schema: {
    enabled: { default: true },
    pointerlockEnabled: { default: true },
    sensitivity: { default: 1 / 25 }
  },

  init: function () {
    this.mouseDown = false;
    this.pointerLocked = false;
    this.lookVector = new THREE.Vector2();
    this.bindMethods();
  },

  play: function () {
    this.addEventListeners();
  },

  pause: function () {
    this.removeEventListeners();
    this.lookVector.set(0, 0);
  },

  remove: function () {
    this.pause();
  },

  bindMethods: function () {
    this.onMouseDown = this.onMouseDown.bind(this);
    this.onMouseMove = this.onMouseMove.bind(this);
    this.onMouseUp = this.onMouseUp.bind(this);
    this.onMouseUp = this.onMouseUp.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
    this.onPointerLockChange = this.onPointerLockChange.bind(this);
  },

  addEventListeners: function () {
    var sceneEl = this.el.sceneEl;
    var canvasEl = sceneEl.canvas;
    var data = this.data;

    if (!canvasEl) {
      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
      return;
    }

    canvasEl.addEventListener('mousedown', this.onMouseDown, false);
    canvasEl.addEventListener('mousemove', this.onMouseMove, false);
    canvasEl.addEventListener('mouseup', this.onMouseUp, false);
    canvasEl.addEventListener('mouseout', this.onMouseUp, false);

    if (data.pointerlockEnabled) {
      document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
      document.addEventListener('mozpointerlockchange', this.onPointerLockChange, false);
      document.addEventListener('pointerlockerror', this.onPointerLockError, false);
    }
  },

  removeEventListeners: function () {
    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
    if (canvasEl) {
      canvasEl.removeEventListener('mousedown', this.onMouseDown, false);
      canvasEl.removeEventListener('mousemove', this.onMouseMove, false);
      canvasEl.removeEventListener('mouseup', this.onMouseUp, false);
      canvasEl.removeEventListener('mouseout', this.onMouseUp, false);
    }
    document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
    document.removeEventListener('mozpointerlockchange', this.onPointerLockChange, false);
    document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
  },

  isRotationActive: function () {
    return this.data.enabled && (this.mouseDown || this.pointerLocked);
  },

  /**
   * Returns the sum of all mouse movement since last call.
   */
  getRotationDelta: function () {
    var dRotation = this.lookVector.clone().multiplyScalar(this.data.sensitivity);
    this.lookVector.set(0, 0);
    return dRotation;
  },

  onMouseMove: function (event) {
    var previousMouseEvent = this.previousMouseEvent;

    if (!this.data.enabled || !(this.mouseDown || this.pointerLocked)) {
      return;
    }

    var movementX = event.movementX || event.mozMovementX || 0;
    var movementY = event.movementY || event.mozMovementY || 0;

    if (!this.pointerLocked) {
      movementX = event.screenX - previousMouseEvent.screenX;
      movementY = event.screenY - previousMouseEvent.screenY;
    }

    this.lookVector.x += movementX;
    this.lookVector.y += movementY;

    this.previousMouseEvent = event;
  },

  onMouseDown: function (event) {
    var canvasEl = this.el.sceneEl.canvas,
        isEditing = (AFRAME.INSPECTOR || {}).opened;

    this.mouseDown = true;
    this.previousMouseEvent = event;

    if (this.data.pointerlockEnabled && !this.pointerLocked && !isEditing) {
      if (canvasEl.requestPointerLock) {
        canvasEl.requestPointerLock();
      } else if (canvasEl.mozRequestPointerLock) {
        canvasEl.mozRequestPointerLock();
      }
    }
  },

  onMouseUp: function () {
    this.mouseDown = false;
  },

  onPointerLockChange: function () {
    this.pointerLocked = !!(document.pointerLockElement || document.mozPointerLockElement);
  },

  onPointerLockError: function () {
    this.pointerLocked = false;
  }
};
<|endoftext|>