NLeSC/ahn-pointcloud-viewer

View on GitHub
app/scripts/pointcloud/path.controls.js

Summary

Maintainability
B
5 hrs
Test Coverage
/*
 *  PathControls
 *  by Ben van Werkhoven (Netherlands eScience Center)
 *
 *  free look around with mouse drag
 */

(function() {
    'use strict';


    var camera;
    var clock;
    var drag = false;
    var lookatPathFactor = 1.08;
    var el;

    var bodyPosition;
    var xAngle = 0;
    var yAngle = 0;

    var MAX_YANGLE = 0.95 * Math.PI / 2;
    var MIN_YANGLE = -0.95 * Math.PI / 2;

    var mouseX = window.innerWidth / 2;
    var mouseY = window.innerHeight / 2;

    //    this factor controls mouse sensitivity
    //    should be more than 2*Math.PI to get full rotation
    var factor = 8;

    //    Map for key states
    var keys = [];
    var zoom = 45;
    var maxZoom = 45;
    var positionOnRoad = 0.0;
    var looptime = 240;
    var THREE;
    var RailService;

    var me;

    var PathControls = function(_THREE_, _RailService_, CameraService) {
        me = this;

        THREE = _THREE_;
        RailService = _RailService_;
        this.NORMAL_MOVEMENT_SPEED_MULTIPLIER = 30;
        this.FAST_MOVEMENT_SPEED_MULTIPLIER = 50;
        this.cameraEasing = 10.0;

        for (var i = 0; i < 130; i++) {
            keys.push(false);
        }

        camera = CameraService.camera;

        clock = new THREE.Clock();

        this.modes = {
            ONRAILS: 'onrails',
            FLY: 'fly',
            DEMO: 'demo',
            OFF: 'off'
        };

        this.mode = this.modes.FLY;
    };

    PathControls.prototype.initCamera = function(startPos) {
        camera.position.copy(startPos);
        camera.up.set(0, 1, 0);
        camera.rotation.order = 'YXZ';

        bodyPosition = camera.position;
        zoom = camera.fov;
        maxZoom = camera.fov;
    };

    PathControls.prototype.initListeners = function(element) {
        el = element;
        // make element focusable, see https://developer.mozilla.org/en-US/docs/Web/HTML/Focus_management_in_HTML
        element.setAttribute('tabindex', 1);

        element.addEventListener('keydown', onKeyDown, false);
        element.addEventListener('keyup', onKeyUp, false);

        element.addEventListener('mouseleave', onBlur, false);
        element.addEventListener('mouseout', onBlur, false);

        element.addEventListener('mousemove', mousemove, false);
        element.addEventListener('mousedown', mousedown, false);
        element.addEventListener('mouseup', mouseup, false);

        element.addEventListener('mousewheel', mousewheel, false);
        element.addEventListener('DOMMouseScroll', mousewheel, false); // firefox
    };

    PathControls.prototype.disableListeners = function(element) {
        element.removeEventListener('keydown', onKeyDown, false);
        element.removeEventListener('keyup', onKeyUp, false);

        element.removeEventListener('mouseleave', onBlur, false);
        element.removeEventListener('mouseout', onBlur, false);

        element.removeEventListener('mousemove', mousemove, false);
        element.removeEventListener('mousedown', mousedown, false);
        element.removeEventListener('mouseup', mouseup, false);

        element.removeEventListener('mousewheel', mousewheel, false);
        element.removeEventListener('DOMMouseScroll', mousewheel, false); // firefox
    };

    PathControls.prototype.init = function(element) {
    this.initCamera(RailService.firstCameraPosition());

        this.lookat(RailService.firstLookatPosition());
        camera.updateProjectionMatrix();

        this.initListeners(element);
    };

    function findNearestPointOnPath(path, point) {
        //first find nearest point on road
        var minDist = Number.MAX_VALUE;
        var dist = 0;
        var index = 0;
        var i;
        for (i=0; i < path.points.length; i++) {
            dist = point.distanceTo(path.points[i]);
            if (dist < minDist) {
                minDist = dist;
                index = i;
            }
        }

        return index;
    }

    function findPrecisePositionOnPath(cpath, point) {
        //first find nearest point on road
        var index = findNearestPointOnPath(cpath, point);

        //interpolate to find precise positionOnRoad
        //first find second nearest point on the road
        var distOne = Number.MAX_VALUE;
        var distTwo = Number.MAX_VALUE;
        var secondIndex = 1;
        if (index !== 0) {
            distOne = point.distanceTo(cpath.points[index-1]);
        }
        if (index < cpath.points.length-1) {
            distTwo = point.distanceTo(cpath.points[index+1]);
        }
        if (distOne > distTwo) {
            secondIndex = index+1;
        } else {
            index = index-1;
            secondIndex = index+1;
        }
        //interpolate using dot product of vector A and B

        //vector A is the vector from index to point
        var A = point.clone();
        A.sub(cpath.points[index]);

        //vector B is the vector from index to secondIndex
        var B = cpath.points[secondIndex].clone();
        B.sub(cpath.points[index].clone());
        B.normalize();

        //project vector A onto vector B
        var delta = A.dot(B) / A.length();

        //delta = delta / B.length();

        //compute new position on road
        return ((index + delta) / cpath.points.length) * looptime;
    }

    //go to a point on the road near the specified point
    PathControls.prototype.goToPointOnRoad = function(point) {
        //find position on road
        var path = RailService.cameraCurve;
        positionOnRoad = findPrecisePositionOnPath(path, point);

        //move the camera there
        bodyPosition.copy(path.getPointAt(positionOnRoad / looptime));
    };

    PathControls.prototype.lookat = function(center) {
        camera.up = new THREE.Vector3(0,1,0);
        camera.lookAt(center);

        xAngle = camera.rotation.y;
        yAngle = camera.rotation.x;
    };

    PathControls.prototype.moveTo = function(center) {
        bodyPosition.copy(center);
    };

    function cap(value) {
        return Math.min(Math.max(value, 0), 1);
    }

    function moveStep(step) {
        var vec = new THREE.Vector3(Math.sin(xAngle), Math.sin(-yAngle), Math.cos(xAngle));
        return vec.multiplyScalar(-step);
    }

    function strafeStep(step) {
        var vec = new THREE.Vector3(Math.cos(-xAngle), 0.0, Math.sin(-xAngle));
        return vec.multiplyScalar(-step);
    }

    function updateCameraRotation() {
        yAngle = Math.max(Math.min(yAngle,MAX_YANGLE),MIN_YANGLE);
         camera.rotation.set(yAngle, xAngle, 0, 'YXZ');
    }

    function updateOnRailsMode(delta) {
        // Forward/backward on the rails
        if (keys[87] || keys[38]) { // W or UP
            positionOnRoad += delta;
        }
        if (keys[83] || keys[40]) { // S or DOWN
            positionOnRoad -= delta;
        }

        positionOnRoad = positionOnRoad % looptime;
        //javascript modulus operator allows negative numbers, correct for that
        if (positionOnRoad < 0) {
            positionOnRoad = looptime + positionOnRoad;
        }

        var path = RailService.cameraCurve;
        camera.position.copy(path.getPointAt(positionOnRoad / looptime));
    }

    function updateForwardBackward(step) {
        // Forward/backward
        if (keys[87] || keys[119] || keys[38]) { // W or UP
            bodyPosition.add(moveStep(step));
        }
        if (keys[83] || keys[115] || keys[40]) { // S or DOWN
            bodyPosition.sub(moveStep(step));
        }
    }

    function updateUpDown(step) {
        // Fly up or down
        if (keys[90] || keys[122]) { // Z
            bodyPosition.y -= step;
        }
        if (keys[81] || keys[113]) { // Q
            bodyPosition.y += step;
        }
    }

    function updateStrafe(vec) {
        // Strafe
        if (keys[65] || keys[97] || keys[37]) { // A or left
            bodyPosition.add(vec);
        }
        if (keys[68] || keys[100] || keys[39]) { // D or right
            bodyPosition.sub(vec);
        }
    }

    function updateFlyMode(step) {
        updateForwardBackward(step);

        updateUpDown(step);

        updateStrafe(strafeStep(step));
    }

    function getLocalFactor() {
        var factor = 1;

        //compute the factor that will be used to scale the arclength used to index the lookatpath
        var lookatPath = RailService.lookatCurve;
        var estArcLookPath = findPrecisePositionOnPath(lookatPath, bodyPosition) / lookatPath.points.length;
        var path = RailService.cameraCurve;
        var estArcPath = findPrecisePositionOnPath(path, bodyPosition) / path.points.length;

        //prevent div by zero
        if (estArcPath !== 0 && estArcLookPath !== 0) {
            //divide the larger by the smaller value
            factor = Math.max(estArcPath,estArcLookPath) / Math.min(estArcPath, estArcLookPath);
        }

        return factor;
    }

    PathControls.prototype.updateDemoMode = function(delta) {
        positionOnRoad += delta;
        positionOnRoad = positionOnRoad % looptime;
        //javascript modulus operator allows negative numbers, correct for that
        if (positionOnRoad < 0) {
            positionOnRoad = looptime + positionOnRoad;
        }
        var path = RailService.cameraCurve;
        camera.position.copy(path.getPointAt(positionOnRoad / looptime));

        //slowly adjust the factor over time to the local factor
        lookatPathFactor = (1.0 - delta/this.cameraEasing) * lookatPathFactor + (delta/this.cameraEasing) * getLocalFactor();
        //console.log('f=' + lookatPathFactor);

        var lookatPath = RailService.lookatCurve;
        var positionOnLookPath = (positionOnRoad / looptime) * (  lookatPath.getLength() / path.getLength() ) * lookatPathFactor;
        var lookPoint = lookatPath.getPointAt(cap(positionOnLookPath));

        this.lookat(lookPoint);
    };

    PathControls.prototype.updateInput = function() {
        var path = RailService.cameraCurve;
        if (!path) {
            return;
        }

        var delta = clock.getDelta();
        delta *= this.NORMAL_MOVEMENT_SPEED_MULTIPLIER;
        if (keys[32]) {
            delta *= this.FAST_MOVEMENT_SPEED_MULTIPLIER;
        }

        if (me.mode !== me.modes.OFF) {
            updateCameraRotation();
        }

        if (this.mode === this.modes.DEMO) {
            this.updateDemoMode(delta);
        } else if (this.mode === this.modes.FLY) {
            updateFlyMode(10 * delta);
        } else if (this.mode === this.modes.ONRAILS) {
            updateOnRailsMode(delta);
        } else if (this.mode === this.modes.OFF) {
            //TODO: Implement something else
        } else {
            console.log('error: unknown control mode in path.controls');
        }

    };

    PathControls.prototype.enableFlightMode = function() {
        this.mode = this.modes.FLY;
    };

    PathControls.prototype.transitionFromFlightMode = function() {
        if (this.mode === this.modes.FLY) {
            this.goToPointOnRoad(bodyPosition);
        }
    };

    PathControls.prototype.enableRailsMode = function() {
        this.transitionFromFlightMode();
        this.mode = this.modes.ONRAILS;
    };

    PathControls.prototype.enableDemoMode = function() {
        this.transitionFromFlightMode();
        this.mode = this.modes.DEMO;
    };

    PathControls.prototype.disable = function() {
        this.previousMode = this.mode;
        this.mode = this.modes.OFF;
    };

    PathControls.prototype.enable = function() {
        this.mode = this.previousMode;
    };

    function onKeyDown(event) {
        keys[event.keyCode] = true;

        var spacebarKeyCode = 32;
        if (event.keyCode === spacebarKeyCode) {
            event.preventDefault();
        }
    }

    function onKeyUp(event) {
        keys[event.keyCode] = false;
    }

    //a blur event is fired when we lose focus
    //in such an event we want to turn off all keys
    function onBlur() {
        drag = false;

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

    function mousedown(event) {
        if (me.mode !== me.modes.OFF) {
            //right mouse button going down!!
            if (event.button === 2) {

                // claim focus when right click on canvas and not yet focused
                if (document.activeElement !== el) {
                    el.focus();
                }

                event.preventDefault();

                mouseX = event.pageX;
                mouseY = event.pageY;

                drag = true;
            }
        }
    }

    function mouseup(event) {
        if (me.mode !== me.modes.OFF) {
            //right mouse button going up!!
            if (event.button === 2) {
                event.preventDefault();
                drag = false;
            }
        }
    }

    function mousemove(event) {
        if (me.mode !== me.modes.OFF) {
            if (!drag) {
                return;
            }

            xAngle -= factor * (event.pageX - mouseX) / (window.innerWidth);
            yAngle -= factor * (event.pageY - mouseY) / (window.innerHeight);

            mouseX = event.pageX;
            mouseY = event.pageY;
        }
    }

    function mousewheel(event) {
        if (me.mode !== me.modes.OFF) {
            event.preventDefault();
            event.stopPropagation();

            var delta = 0;

            if (event.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
                delta = event.wheelDelta;
            } else if (event.detail !== undefined) { // Firefox
                delta = -event.detail;
            }

            if (delta < 0) {
                zoom += 2.5;
            } else {
                zoom -= 2.5;
            }

            if (zoom > maxZoom) {
                zoom = maxZoom;
            }
            if (zoom < 5) {
                zoom = 5;
            }

            camera.fov = zoom;
            camera.updateProjectionMatrix();
        }
    }

      angular.module('pattyApp.pointcloud')
        .service('PathControls', ['THREE', 'RailService', 'CameraService', PathControls]);
})();