Skip to content
Snippets Groups Projects
LocalKeyController.js 10.8 KiB
Newer Older
seba's avatar
seba committed
$(document).ready(function () {

    /**
     * Local controller. Can connect to one or more models. In each model, a gripper is created
     * at connection. If a gripperId is passed, an existing gripper with this id is assigned.
     * Otherwise a new gripper is added with the given id or alternatively the session id.
     * The user's keystrokes are used to control the assigned gripper(s) and any gripped object.
     * @param {optional Array of [socket, gripperId], where gripperId can be null, default:null} modelSockets
     */
    this.LocalKeyController = class LocalKeyController {
        constructor(modelSockets=null) {
            // array of model socket-gripperId pairs that are controlled
            this.models = modelSockets ? modelSockets : new Array();

            // assign functions to key codes: [function for keydown, function for keyup, down?]
            // grip and flip are one-time actions, move and rotate are looped
            this.keyAssignment = {
                13: [this.grip, null, false],					// Enter
                32: [this.grip, null, false],					// Space
                37: [this.moveLeft, this.stopMove, false],		// arrow left
                38: [this.moveUp, this.stopMove, false],		// arrow up
                39: [this.moveRight, this.stopMove, false],		// arrow right
                40: [this.moveDown, this.stopMove, false],		// arrow down
                65: [this.rotateLeft, this.stopRotate, false],	// a
                68: [this.rotateRight, this.stopRotate, false],	// d
                83: [this.flip, null, false],					// s
                87: [this.flip, null, false]					// w
            };

            // Set up key listeners
            this._initKeyListener();
        }

        // --- (Un)Subscribing models ---

        /**
         * Subscribe a new model. Only one subscription per model is allowed,
         * meaning in one model, only one gripper can be controlled!
         * A gripper is controlled once the model sends an 'attach_gripper'
         * event. See requestGripper for manually adding a gripper.
         * @param {socket of the model server to notify} socket
         */
        attachModel(socket) {
            // make sure not to subscribe a model-gripper pair twice
            for (let [existingSocket, g] of this.models) {
                if (existingSocket.id == socket.id) {
                    return;
                }
            }
            // use the id authoratively assigned by the model
            socket.on("attach_gripper", (assignedId) => {
                this.models.push([socket, assignedId]);
            });
        }

        /**
         * Remove a model from the internal list of models to notify. Remove the associated gripper.
         * @param {socket of the model API to unsubscribe} socket
         */
        detachModel(socket) {
            // remove any occurence of the socket
            for (let i = 0; i < this.models.length; i++) {
                if (this.models[i][0].id == socket.id) {
                    this.models[i][0].emit("remove_gripper", this.models[i][1]);
                    this.models.splice(i, 1);
                }
            }
        }

        // --- Notifying subscribed models ---

        /**
         * Notifies all subscribed models that a "grip" should be attempted.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        grip(thisArg, loop) {
            // send an event to each model
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("grip", {"id": grId, "loop":loop});
            });
        }

        /**
         * Request stopping ongoing looped gripping.
         */
        stopGrip(thisArg) {
            // send a request to each subscribed model
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("stop_grip", {"id":grId});
            });
        }

        /**
         * Notify models to move the gripper 1 unit to the left.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        moveLeft(thisArg, loop) { thisArg._moveGr(-1, 0, loop); }

        /**
         * Notify models to move the gripper 1 unit up.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        moveUp(thisArg, loop) { thisArg._moveGr(0, -1, loop); }

        /**
         * Notify models to move the gripper 1 unit to the right.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        moveRight(thisArg, loop) { thisArg._moveGr(1, 0, loop); }

        /**
         * Notify models to move the gripper 1 unit down.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        moveDown(thisArg, loop) { thisArg._moveGr(0, 1, loop); }

        /**
         * Helper function to notify models to move the gripper 1 block in a specified direction.
         * @param {number of blocks to move in x direction} dx
         * @param {number of blocks to move in y direction} dy
         * @param {set to true to request a looped action on the model side} loop
         */
        _moveGr(dx, dy, loop) {
            this.models.forEach(([socket, grId]) => {
                socket.emit("move", {"id": grId, "dx": dx, "dy": dy, "loop": loop});
            });
        }

        /**
         * Request stopping an ongoing looped movement.
         */
        stopMove(thisArg) {
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("stop_move", {"id": grId});
            });
        }

        /**
         *
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        rotateLeft(thisArg, loop) { thisArg._rotate(-1, loop); }

        /**
         *
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        rotateRight(thisArg, loop) { thisArg._rotate(1, loop); }

        /**
         * Helper function to notify models to rotate a gripped object in a specified direction.
         * @param {number of units to turn. Pass negative value for leftwards rotation} direction
         * @param {set to true to request a looped action on the model side} loop
         */
        _rotate(direction, loop) {
            this.models.forEach(([socket, grId]) => {
                socket.emit("rotate", {"id":grId, "direction":direction, "loop":loop});
            });
        }

        /**
         * Stop an ongoing looped rotation.
         */
        stopRotate(thisArg) {
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("stop_rotate", {"id":grId});
            });
        }

        /**
         * Notify models to flip a gripped object on a specified axis.
         * @param {reference to LocalKeyController instance (this)} thisArg
         * @param {set to true to request a looped action on the model side} loop
         */
        flip(thisArg, loop) {
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("flip", {"id":grId, "loop":loop});
            });
        }

        /**
         * Request stopping ongoing looped mirroring.
         */
        stopFlip(thisArg) {
            thisArg.models.forEach(([socket, grId]) => {
                socket.emit("stop_flip", {"id":grId});
            });
        }

        // --- Reacting to user events ---

        /**
         * Start fresh, delete any keys remembered as currently pressed.
         */
        resetKeys(){
            for (let key of Object.keys(this.keyAssignment)) {
                // set property pressed to false for each key
                this.keyAssignment[key][2] = false;
            }
        }

        /**
         * Register the key listeners to allow gripper manipulation.
         * Notifies the associated models.
         */
        _initKeyListener() {
            $(document).keydown( e => {
                if (this._downAssigned(e.keyCode)) {
                    // only progress if the key is not already in state "down"
                    if (!this._isDown(e.keyCode)) {
                        // Change the state to "down". This is done for all keys, not
                        // just loopable ones, to prevent the keydown event from
                        // firing repeatedly if the key is held.
                        this.keyAssignment[e.keyCode][2] = true;
                        // execute the function assigned to the keydown event
                        let loopable = this._upAssigned(e.keyCode);
                        this.keyAssignment[e.keyCode][0](this, loopable);
                    }
                }
            });

            $(document).keyup( e => {
                // check if a function is assigned. Only execute if the key was remembered as "down"
                if (this._upAssigned(e.keyCode) && this._isDown(e.keyCode)) {
                    // execute the function assigned to the keyup event
                    this.keyAssignment[e.keyCode][1](this);
                }
                // change the state to "up"
                if (this._registered(e.keyCode)) {
                    this.keyAssignment[e.keyCode][2] = false;
                }
            });
        }

        _registered(keyCode) {
            return this.keyAssignment[keyCode] ? true : false;
        }

        /**
         * Check whether a function is assigned to the keydown event of a given key code.
         * @param {int, code of the key in question} keyCode
         * @return bool, true signifying a function is assigned to keydown
         */
        _downAssigned(keyCode) {
            return this._registered(keyCode) && this.keyAssignment[keyCode][0] ? true : false;
        }

        /**
         * Check whether a function is assigned to the keyup event of a given key code.
         * @param {int, code of the key in question} keyCode
         * @return bool, true signifying a function is assigned to keyup
         */
        _upAssigned(keyCode) {
            return this._registered(keyCode) && this.keyAssignment[keyCode][1] ? true : false;
        }

        /**
         * Check whether a key is currently in "down" state aka currently pressed.
         * @return bool, true if the key is "down"
         */
        _isDown(keyCode) {
            return this._registered(keyCode) && this.keyAssignment[keyCode][2] ? true : false;
        }

    }; // class LocalKeyController end
}); // on document ready end