# Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
# Secretary of Defense (Personnel & Readiness).
# 
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
# 
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.

## The component representation of a navigation scene
## 
## @name navscene.vwf
## @namespace

--- 
extends: http://vwf.example.com/scene.vwf
properties: 
  
  ## Navigation mode. Default value is 'walk.' Other options include 'orbit' and 'none.'
  ## 
  ## @name navscene.vwf#navmode
  ## @property

  navmode:
    set: |
      switch ( value ) {
        case "walk":
        case "orbit":
        case "none":
          if ( this.setNavMode ) {
            this.setNavMode( value );
          }
          this.navmode = value;
          break;
      }
    value: "walk"

  ## Orbit radius
  ## 
  ## @name navscene.vwf#orbitRadius
  ## @property

  orbitRadius: -1

  ## A node.vwf#find search pattern identifying the object to orbit.
  ## 
  ## @name navscene.vwf#orbitObjectPattern
  ## @property

  orbitObjectPattern: "sceneCenter"

  ## Rotation speed. At a speed of 1, will take 4 seconds to go around 360 degrees.
  ## 
  ## @name navscene.vwf#rotationSpeed
  ## @property

  rotationSpeed: 1.0

  ## Translation speed
  ## 
  ## @name navscene.vwf#translationSpeed
  ## @property

  translationSpeed: 1.0

  ## Maximum speed
  ## 
  ## @name navscene.vwf#maxSpeed
  ## @property

  maxSpeed: 500

  ## Drag mode move boolean. Default value is false.
  ## 
  ## @name navscene.vwf#dragModeMove
  ## @property

  dragModeMove: false

events:
  keyUp:
  keyDown:
  pointerDown:
  pointerUp:
  pointerMove:
  pointerOut:
  pointerWheel:

methods:
  
  ## Updates scene
  ## 
  ## @name navscene.vwf#update
  ## @function
  ## 
  ## @returns undefined

  update:

  ## Gets camera at vector
  ## 
  ## @name navscene.vwf#getCameraAt
  ## @function
  ## 
  ## @returns camera at vector

  getCameraAt:

  ## For internal use only - needs to be public for future calls
  ## 
  ## @name navscene.vwf#disableViewSideNav
  ## @function
  ## 
  ## @returns undefined

  disableViewSideNav:

  ## For internal use only - needs to be public for future calls
  ## 
  ## @name navscene.vwf#setNavMode
  ## @function
  ## 
  ## @returns undefined

  setNavMode:

scripts:
- |
    var counter = 1;
    var _piDiv180 = Math.PI / 180;
    var _180DivPI = 180 / Math.PI;
    var _2PI = 2 * Math.PI;
    var _85 = 85.0 * _piDiv180;
    var _10 = 10.0 * _piDiv180;
    var phi, theta;
    this.initialize = function() {
      this.input = { 
        futureActive: function() {
          return ( this.lookActive || this.moveActive || this.button1Down || this.button2Down || this.keysAreDown() ); 
        },
        timeSinceLastEvent: function() { return this.time - this.pointerEventTime; },
        timeSinceDownEvent: function() { return this.time - this.pointerDownTime; },
        pointerDelta: function() { 
          if ( this.pointerInfo && this.lastPointerInfo ) {
            return [ (this.lastPointerInfo.position[0] - this.pointerInfo.position[0]) * 50,
                     (this.lastPointerInfo.position[1] - this.pointerInfo.position[1]) * 50 
                   ];
          }
          return undefined;
        },        
        keysAreDown: function() { return ( this.keyInfo && Object.keys( this.keyInfo.keysDown ).length > 0 ); },
        keyInfo: undefined,
        pointerInfo: undefined,
        lastPointerInfo: undefined,
        pickInfo: undefined,
        lastPickInfo: undefined,        
        pointerDownTime: undefined,
        pointerEventTime: undefined,
        lookActive: false, 
        moveActive: false, 
        look: [ 0, 0 ],
        move: [ 0, 0 ],
        button1Down: false,
        button2Down: false,
        turnLeftDown: false,
        turnRightDown: false,
        lastUpdateTime: undefined,
        lastInputTime: undefined,
      };

      // Wait until the queue resumes to set the navigation mode, so that we are sure that the camera has been
      // created
      // TODO: When we can assume that initialize doesn't complete until all created nodes are in place, this
      //       will no longer need to be a future call because we will be able to assume that the camera exists
      
      // Disable view-side navigation so navscene.vwf can do the navigation
      this.future( 0 ).disableViewSideNav();
      this.future( 0 ).setNavMode( this.navmode );
    }
    this.timeElapsed = function() {
      var timeElapsed = this.time - this.input.lastInputTime;
      if ( !this.input.lastInputTime || timeElapsed > 1 ) {
        timeElapsed = 1;  
      }
      return timeElapsed;
    }
    this.disableViewSideNav = function() {
      if ( this.camera )
        this.camera.navmode = "none";
    }
    this.setNavMode = function( mode ) {
      if (!this.activeCameraComp) {
        this.getActiveCamera();
      }
      if ( this.activeCameraComp ) {
        switch( mode ) {
          case "orbit":
            if ( this.orbitObjectPattern ) {
              this.find( this.orbitObjectPattern, function( orbitObject ) {
                this.activeCameraComp.lookAt = orbitObject.id;
                this.updateRadius();
              } );
            }
            break;
          default:
            this.activeCameraComp.lookAt = "";
            break;
        }
      }
    }
    this.changeNavMode = function( state ) {
      if ( state == 1 ) {
        if ( this.navmode == "walk" ) this.navmode = "orbit";
        else if ( this.navmode == "orbit" ) this.navmode = "walk"; 
      }
    }
    this.raiseOrZoomIn = function( state ) {
      var active = this.input.futureActive();
      if ( this.navMode == "walk" ) {
        this.cameraZ( 1.0 );
      } else if ( this.navMode == "orbit" ) {
        this.zoom( true );
      }     
      this.input.button1Down = state;
      if ( state && !active ) {
         this.future( 0.1 ).update();
      }
    }
    this.lowerOrZoomOut = function( state ) {
      var active = this.input.futureActive();
      if ( this.navMode == "walk" ) {
        this.cameraZ( -1.0 );
      } else if ( this.navMode == "orbit" ) {
        this.zoom( false );
      }      
      this.input.button2Down = state;
      if ( state && !active ) {
         this.future( 0.1 ).update();
      }      
    }
    this.pointerIsDown = function() {
      if ( this.input && this.input.pointerInfo ) {
        var bInfo = this.input.pointerInfo.buttons;
        if ( bInfo ) {
          return bInfo.left || bInfo.right || bInfo.middle;        
        }
      }
      return false;
    }        
    this.pointerDown = function( parms, pickInfo ){
      this.input.lastPointerInfo = undefined;
      this.input.pointerInfo = parms;
      this.input.lastPickInfo = undefined;
      this.input.pickInfo = pickInfo;      
      this.input.pointerDownTime = this.time;
      this.input.lastInputTime = this.time;
    }
    this.pointerUp = function( parms, pickInfo ){
      this.input.lastPointerInfo = this.input.pointerInfo;
      this.input.pointerInfo = undefined;
      this.input.lastPickInfo = this.input.pickInfo;
      this.input.pickInfo = undefined;
      this.input.lastInputTime = this.time;            
    }
    this.pointerMove = function( parms, pickInfo ){
      this.input.pointerEventTime = this.time;
      this.input.lastPointerInfo = this.input.pointerInfo;
      this.input.pointerInfo = parms;
      this.input.lastPickInfo = this.input.pickInfo;
      this.input.pickInfo = pickInfo;  
 
      if ( this.pointerIsDown() /* && this.input.pointerEventTime - this.input.pointerDownTime > 6 */ ) {
        var active = this.input.futureActive();
        var delta = this.input.pointerDelta();
        var valid = this.navmode == "none" ? false : true;
        if ( valid && !active ) {
          this.future( 1/30 ).update();
        }
      } 
      this.input.lastInputTime = this.time;   
    }
    this.pointerOut = function( parms ){
      this.input.lastPointerInfo = this.input.pointerInfo;
      this.input.pointerInfo = undefined;
      this.input.lastPickInfo = this.input.pickInfo;
      this.input.pickInfo = undefined; 
      this.input.lastInputTime = this.time;             
    }
    this.pointerWheel = function( parms, pickInfo ) {
      this.input.pointerEventTime = this.time;

      this.input.lastPointerInfo = this.input.pointerInfo;
      this.input.pointerInfo = parms;      
      this.input.lastPickInfo = this.input.pickInfo;
      this.input.pickInfo = pickInfo;

      if (!this.activeCameraComp) {
        this.getActiveCamera();
      }
      if ( this.activeCameraComp ) {
        var cameraPos = this.activeCameraComp.translation;
        var cameraAt = this.getCameraAt();

        if ( this.orbitRadius == -1 || this.input.pointerEventTime - this.input.lastUpdateTime > 0.5 ) {
          this.updateRadius();
        }

        cameraAt = goog.vec.Vec3.scale(
          cameraAt,
          this.input.pickInfo.wheel.deltaY * -1,
          cameraAt
        );
        
        if ( goog.vec.Vec3.magnitudeSquared( cameraAt ) > goog.vec.EPSILON ) {
          this.activeCameraComp.translation = goog.vec.Vec3.add(
            this.activeCameraComp.translation,
            goog.vec.Vec3.scale(
              goog.vec.Vec3.normalize( cameraAt, cameraAt ),
              this.distance(),
              cameraAt
            ),
            cameraAt
          );
        }
      }
      this.input.lastInputTime = this.time;
    }
    this.keyDown = function( parms ) {
      var active = this.input.futureActive();

      // capture the event input locally
      this.input.keyInfo = $.extend(true, {}, parms);;

      // store the current Time
      this.input.lastInputTime = this.time;

      // call future.update if update was not currently occurring
      if ( !active ) {
        this.future( 0.1 ).update();
      }
      
    }
    this.keyUp = function( parms ) {
      this.input.keyInfo = $.extend(true, {}, parms);
      if ( Object.keys( parms.keysDown ).length == 0 ){
        this.handleKeyDown();
      } 
      this.input.lastInputTime = this.time;
    }
    this.handleKeyDown = function() {
      if (!this.activeCameraComp) {
        this.getActiveCamera();
      }      
      if ( this.activeCameraComp ) {

        var deltaZ = 0;
        var kd = $.extend(true, {}, this.input.keyInfo.keysDown, this.input.keyInfo.keysUp);

        this.input.look = [ 0, 0 ];
        this.input.move = [ 0, 0 ];
        
        var temp;
        for ( var keyPress in kd ) {
          switch ( Number( kd[ keyPress ].code ) ) {
            case 87:  //w
            case 38:  //up
              this.input.move[1] += 1;
              break;
            case 83:  //s
            case 40:  //down
              this.input.move[1] += -1;
              break;
            case 37: // left              
            case 65:  //a
              this.input.move[0] += -1;
              break;
            case 39: // right              
            case 68:  //d
              this.input.move[0] += 1;
              break;
            case 81: // q
              this.input.look[0] += -1;
              break;
            case 69: // e
              this.input.look[0] += 1;
              break;
            case 90: // z
              temp = this.logger.enabled;
              this.logger.enabled = true;
              this.logger.info( "translation: [ " + Array.prototype.slice.call( this.activeCameraComp.translation ) + " ]" );
              this.logger.info( " quaternion: [ " + Array.prototype.slice.call( this.activeCameraComp.quaternion ) + " ]" );              
              this.logger.info( "   rotation: [ " + Array.prototype.slice.call( this.activeCameraComp.rotation ) + " ]" );
              this.logger.info( "  transform: [ " + Array.prototype.slice.call( this.activeCameraComp.transform )  + " ]" );
              this.logger.info( "  worldTransform: [ " + Array.prototype.slice.call( this.activeCameraComp.worldTransform )  + " ]" );
              this.logger.enabled = temp;
              break;
            case 88: // x
              break;
            case 82: // r
              deltaZ += 1.0;
              break;
            case 67: // c
              deltaZ += - 1.0;
              break;
            default:
              break;
          }
        }

        this.input.keyInfo.keysUp = {};
        
        if ( this.input.look[0] != 0 || this.input.look[1] != 0 ) {
          this.look( this.input.look[0], this.input.look[1] );
        }
        if ( this.input.move[0] != 0 || this.input.move[1] != 0 ) {
          this.move( this.input.move[0], this.input.move[1] );
        }
        if ( deltaZ != 0 ) {
          if ( this.navmode == "walk" ) {
            this.cameraZ( deltaZ );
          }
        }
         
      } 
    }

    this.onMove = function( x, y ) {
      if ( Math.abs( x ) < 0.1 && Math.abs( y ) < 0.1 ) {
        if ( this.input.moveActive ) {
          this.input.moveActive = false;
          this.input.move = [ 0, 0 ];
        }
        return; 
      }
      this.input.move = [ x, y ];      
      var active = this.input.futureActive();
      if ( !this.input.moveActive ) {
        this.input.moveActive = true;
        this.move( x, y );
      }
      if ( !active ) {
        this.future( 0.1 ).update();
      }
    }    
    this.move = function( x, y ) {
      if ( this.navmode == "walk" ) {
        this.translate( x, y );
      } else if ( this.navmode == "orbit" ) {
        this.orbit( x, y );
      }
    }
    this.onLook = function( x, y ) {
      if ( Math.abs( x ) < 0.1 && Math.abs( y ) < 0.1 ) {
        if ( this.input.lookActive ) {
          this.input.lookActive = false;
          this.input.look = [ 0, 0 ];  
        }        
        return; 
      }  
      this.input.look = [ x, y ];
      var active = this.input.futureActive();
      if ( !this.input.lookActive ) {
        this.input.lookActive = true;
        this.look( x, y );
      }
      if ( !active ) {
        this.future( 0.1 ).update();
      }
    }
    this.look = function( x, y ) {
      if (!this.activeCameraComp) 
        this.getActiveCamera();
      if ( this.activeCameraComp ) {
        if ( this.input.pointerInfo && this.input.pointerInfo.modifiers.ctrl ) {
          if ( y != 0 ) { this.activeCameraComp.rotateBy( [ 1, 0, 0, -y * this.angularDistance() ], 0 ) }
        } else {
          if ( x != 0 ) { this.activeCameraComp.rotateBy( [ 0, 0, 1, -x * this.angularDistance() ], 0 ) }
        } 
      }
    }    
    this.zoom = function( dirIn ) {
      if (!this.activeCameraComp) {
        this.getActiveCamera();
      }
      if ( this.activeCameraComp ) {
        var camerapos = this.activeCameraComp.translation;
        var camerarot = this.getCameraAt();
        
        var yinc = 0;
        var xinc = 0;
        var zinc = 0;

        var speed = this.distance();

        if ( !dirIn ) speed = -speed;
        yinc = yinc + parseFloat( camerarot[1] ) * speed; 
        xinc = xinc + parseFloat( camerarot[0] ) * speed;

        if ( this.orbitRadius == -1 || this.time - this.input.lastUpdateTime > 0.5 ) {
          this.updateRadius();
        }
        
        if ( xinc != 0 || yinc != 0 ) {
          var origX, origY, origZ;
          var x, y, z, atBounds = false;
          
          origX = camerapos[0];
          origY = camerapos[1];
          origZ = camerapos[2];
        
          x = origX + xinc * this.distance() * mult;
          y = origY + yinc * this.distance() * mult;
          z = origZ;

          var ratioHR = camerapos[2] / this.orbitRadius;
          zinc = Math.sqrt( Math.pow( x-origX, 2 ) + Math.pow( y-origY, 2 ) ) * ratioHR;
          if ( parms.wheelDeltaY > 0 ) zinc *= -1;
          z = origZ + zinc;
          
          this.activeCameraComp.translation = [ x, y, z ];
        }
      }
    }
    this.distance = function(){
      var dist = this.translationSpeed * 10 * this.timeElapsed();
      return dist;
    }
    this.angularDistance = function() {
      // Should take 4 seconds to go around 360 degrees
      var dist = this.rotationSpeed * 90 * this.timeElapsed();
      return dist;
    }
    this.cameraZ = function( dir ) {
      if (!this.activeCameraComp) this.getActiveCamera();
      
      if ( this.activeCameraComp ) {
        var pos = this.activeCameraComp.translation;
        
        this.activeCameraComp.translation = [ pos[0], pos[1], pos[2] + ( this.distance() * dir ) ];
      }
    }
    this.updateRadius = function() {
      var pos = this.activeCameraComp.translation || [ 1, 1, 1 ];
      var oldRadius = this.orbitRadius;

      // Assuming the orbit center is the origin, the radius is just the length of the line segment from the
      // origin to the active camera component
      this.orbitRadius = Math.sqrt( (pos[0] * pos[0]) + (pos[1] * pos[1]) + (pos[2] * pos[2]) );
      var dist2D = Math.sqrt( (pos[0] * pos[0]) + (pos[1] * pos[1]) );
      
      phi = Math.acos( pos[2] / this.orbitRadius );
      if ( 0 <= pos[0] ) theta = Math.asin( pos[1]/dist2D );
      else theta = Math.PI - Math.asin( pos[1]/dist2D );

      this.input.lastUpdateTime = this.time;
    }    
    this.orbit = function( xDelta, yDelta ){
      if (!this.activeCameraComp) this.getActiveCamera();
      if ( this.activeCameraComp ) {
        this.updateRadius();

        var pixelToRadian = _2PI * this.angularDistance() / 360;
        var phiDelta = -yDelta * pixelToRadian;
        var thetaDelta = -xDelta * pixelToRadian;
        
        phi = phi - phiDelta;
        theta = theta - thetaDelta;
        
        if ( theta >= _2PI ) theta -= _2PI;
        else if ( theta < 0 ) theta += _2PI;

        if ( phi > _85 ) phi = _85;
        else if ( phi < _10 ) phi = _10;
          
        // Spherical to Cartesian
        var x = this.orbitRadius * Math.sin( phi ) * Math.cos( theta );
        var y = this.orbitRadius * Math.sin( phi ) * Math.sin( theta );
        var z = this.orbitRadius * Math.cos( phi );
        this.activeCameraComp.translation = [ x, y, z ];
      }
    }
    this.getCameraAt = function() {
      var camRotMat = this.activeCameraComp.rotationMatrix;
      return goog.vec.Vec3.createFromValues( camRotMat[4], camRotMat[5], camRotMat[6] );  
    }
    this.getCameraUp = function() {
      var camRotMat = this.activeCameraComp.rotationMatrix;
      return goog.vec.Vec3.createFromValues( camRotMat[8], camRotMat[9], camRotMat[10] );  
    }
    this.getCameraRight = function() {
      var camRotMat = this.activeCameraComp.rotationMatrix;
      return goog.vec.Vec3.createFromValues( camRotMat[0], camRotMat[1], camRotMat[2] );  
    }            
    this.getCameraVec = function( x, y, z ) {
        var camRotMat = this.activeCameraComp.rotationMatrix;
        var camAt = goog.vec.Mat4.multVec4(
          camRotMat,
          goog.vec.Vec4.createFromValues( x, y, z, 1 ),
          goog.vec.Vec3.create()
        );
        return camAt;      
    }
    this.translate = function( x, y ) {
      if (!this.activeCameraComp) 
        this.getActiveCamera();

      if ( this.activeCameraComp ) {
        var trans = this.getCameraVec( x, y, 0 );
        trans[2] = 0;

        if ( goog.vec.Vec3.magnitudeSquared( trans ) > goog.vec.EPSILON ) {

          // TODO: Clean this up - right now the two vectors are added together and both vectors are replaced 
          //       w/ the result.  That's probably not the desired behavior.
          this.activeCameraComp.translation = goog.vec.Vec3.add(
            this.activeCameraComp.translation,
            goog.vec.Vec3.scale(
              goog.vec.Vec3.normalize( trans, trans ),
              this.distance(),
              trans
            ),
            trans
          );
        }
      }      
    }
    this.update = function() {
      if ( this.input ) {
        var delta = this.input.pointerDelta();
        var pi = this.input.pointerInfo;
        if ( delta ) {
          if ( this.pointerIsDown() ) {
            if ( this.input.pointerInfo.buttons.left ) {
              var pi = this.input.pointerInfo;
              switch( this.navmode ) {
                case "orbit":
                  this.orbit( delta[0], delta[1] );
                  break;          
                case "walk":
                  if ( this.dragModeMove )
                    this.move( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 );
                  else
                    this.look( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 );
                  break;
              } 
            }
          }   
          if ( this.input.lookActive ) {
            this.look( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 );
          }
          if ( this.input.moveActive ) {
            this.move( (pi.position[0]*2.0)-1,(pi.position[1]*2.0)-1 );
          }
        }
        if ( this.input.keysAreDown() ) {
        this.handleKeyDown();
        }        
        if ( this.input.button1Down ) {
          if ( this.navMode == "walk" ) {
            this.cameraZ( 1.0 );
          } else if ( this.navMode == "orbit" ) {
            this.zoom( true );
          }     
        }    
        if ( this.input.button2Down ) {
          if ( this.navMode == "walk" ) {
            this.cameraZ( -1.0 );
          } else if ( this.navMode == "orbit" ) {
            this.zoom( false );
          }     
        }     
        if ( this.input.futureActive() ) {
          this.future( 0.1 ).update();
        }
      } 
      this.input.lastInputTime = this.time;
    } //@ sourceURL=navscene.vwf