/**
 * A FORGE.Camera tells the renderer wich part of the scene to render.
 *
 * @constructor FORGE.Camera
 * @param {FORGE.Viewer} viewer - {@link FORGE.Viewer} reference.
 * @extends {FORGE.BaseObject}
 */
FORGE.Camera = function(viewer)
{
    /**
     * The viewer reference.
     * @name FORGE.Camera#_viewer
     * @type {FORGE.Viewer}
     * @private
     */
    this._viewer = viewer;

    /**
     * Camera configuration that has been loaded.
     * @name  FORGE.Camera#_config
     * @type {?CameraConfig}
     * @private
     */
    this._config = null;

    /**
     * The yaw value in radians.
     * @name FORGE.Camera#_yaw
     * @type {number}
     * @private
     */
    this._yaw = 0;

    /**
     * The yaw minimum value in radians.
     * @name FORGE.Camera#_yawMin
     * @type {number}
     * @private
     */
    this._yawMin = -Infinity;

    /**
     * The yaw maximum value in radians.
     * @name FORGE.Camera#_yawMax
     * @type {number}
     * @private
     */
    this._yawMax = Infinity;

    /**
     * The pitch value in radians.
     * @name FORGE.Camera#_pitch
     * @type {number}
     * @private
     */
    this._pitch = 0;

    /**
     * The pitch minimum value in radians.
     * @name FORGE.Camera#_pitchMin
     * @type {number}
     * @private
     */
    this._pitchMin = -Infinity;

    /**
     * The pitch maximum value  in radians.
     * @name FORGE.Camera#_pitchMax
     * @type {number}
     * @private
     */
    this._pitchMax = Infinity;

    /**
     * The roll value in radians.
     * @name FORGE.Camera#_roll
     * @type {number}
     * @private
     */
    this._roll = 0;

    /**
     * The roll minimum value  in radians.
     * @name FORGE.Camera#_rollMin
     * @type {number}
     * @private
     */
    this._rollMin = -Infinity;

    /**
     * The roll maximum value in radians.
     * @name FORGE.Camera#_rollMax
     * @type {number}
     * @private
     */
    this._rollMax = Infinity;

    /**
     * The fov value in radians.
     * @name FORGE.Camera#_fov
     * @type {number}
     * @private
     */
    this._fov = 90;

    /**
     * The fov minimum value in radians.
     * @name FORGE.Camera#_fovMin
     * @type {number}
     * @private
     */
    this._fovMin = 0;

    /**
     * The fov maximum value in radians.
     * @name FORGE.Camera#_fovMax
     * @type {number}
     * @private
     */
    this._fovMax = Infinity;

    /**
     * Parallax setting
     * Value range is between 0 and 1
     * @name FORGE.Camera#_parallax
     * @type {number}
     * @private
     */
    this._parallax = 0;

    /**
     * Does the camera keep its orientation between scenes?
     * @name FORGE.Camera#_keep
     * @type {boolean}
     * @private
     */
    this._keep = false;

    /**
     * The modelview rotation matrix.
     * @name FORGE.Camera#_modelView
     * @type {THREE.Matrix4}
     * @private
     */
    this._modelView = null;

    /**
     * The inverse of the modelview rotation matrix.
     * @name FORGE.Camera#_modelViewInverse
     * @type {THREE.Matrix4}
     * @private
     */
    this._modelViewInverse = null;

    /**
     * Rotation quaternion of the camera
     * @name FORGE.Camera#_quaternion
     * @type {THREE.Quaternion}
     * @private
     */
    this._quaternion = null;

    /**
     * Three Perspective Camera object
     * @name FORGE.Camera#_main
     * @type {THREE.PerspectiveCamera}
     * @private
     */
    this._main = null;

    /**
     * Three Orthographic Camera object
     * @name FORGE.Camera#_flat
     * @type {THREE.OrthographicCamera}
     * @private
     */
    this._flat = null;

    /**
     * Left camera for VR rendering
     * @name  FORGE.Camera._left
     * @type {THREE.PerspectiveCamera}
     * @private
     */
    this._left = null;

    /**
     * Right camera for VR rendering
     * @name  FORGE.Camera._right
     * @type {THREE.PerspectiveCamera}
     * @private
     */
    this._right = null;

    /**
     * Three Perspective Camera radius (depends on parallax)
     * @name FORGE.Camera#_radius
     * @type {number}
     * @private
     */
    this._radius = 0;

    /**
     * Camera animation object
     * @name FORGE.Camera#_cameraAnimation
     * @type {FORGE.CameraAnimation}
     * @private
     */
    this._cameraAnimation = null;

    /**
     * Camera gaze cursor
     * @name FORGE.Camera#_gaze
     * @type {FORGE.CameraGaze}
     * @private
     */
    this._gaze = null;

    /**
     * Is the camera have load its configuration at least one time? For keep feature.
     * @name FORGE.Camera#_initialized
     * @type {boolean}
     * @private
     */
    this._initialized = false;

    /**
     * Log the camera changes between two updates.
     * @name FORGE.Camera#_changelog
     * @type {?CameraChangelog}
     * @private
     */
    this._changelog = null;

    /**
     * On camera change event dispatcher.
     * @name FORGE.Camera#_onChange
     * @type {FORGE.EventDispatcher}
     * @private
     */
    this._onChange = null;

    /**
     * On camera orientation change event dispatcher.
     * @name FORGE.Camera#_onOrientationChange
     * @type {FORGE.EventDispatcher}
     * @private
     */
    this._onOrientationChange = null;

    /**
     * On camera fov change event dispatcher.
     * @name FORGE.Camera#_onFovChange
     * @type {FORGE.EventDispatcher}
     * @private
     */
    this._onFovChange = null;

    FORGE.BaseObject.call(this, "Camera");

    this._boot();
};

FORGE.Camera.prototype = Object.create(FORGE.BaseObject.prototype);
FORGE.Camera.prototype.constructor = FORGE.Camera;

/**
 * Camera default radius for parallax.
 * @name FORGE.Camera.RADIUS
 * @type {number}
 * @const
 */
FORGE.Camera.RADIUS = 50;

/**
 * Camera default configuration in degrees like in the json configuration.
 * @name FORGE.Camera.DEFAULT_CONFIG
 * @type {CameraConfig}
 * @const
 */
FORGE.Camera.DEFAULT_CONFIG =
{
    keep: false,
    parallax: 0,
    yaw:
    {
        min: -Infinity,
        max: Infinity,
        default: 0
    },
    pitch:
    {
        min: -Infinity,
        max: Infinity,
        default: 0
    },
    roll:
    {
        min: -Infinity,
        max: Infinity,
        default: 0
    },
    fov:
    {
        min: 0,
        max: Infinity,
        default: 90
    },
    gaze:
    {
        delay: 2000,
        cursor:
        {
            innerRadius: 0.02,
            outerRadius: 0.04,
            color: 0xffffff,
            opacity: 0.5
        },
        progress:
        {
            innerRadius: 0.02,
            outerRadius: 0.04,
            color: 0xff0000,
            opacity: 0.5
        }
    }
};

/**
 * Init sequence.
 * @method FORGE.Camera#_boot
 * @private
 */
FORGE.Camera.prototype._boot = function()
{
    this._resetChangelog();

    this._modelView = new THREE.Matrix4();
    this._modelViewInverse = new THREE.Matrix4();
    this._quaternion = new THREE.Quaternion();

    this._gaze = new FORGE.CameraGaze(this._viewer, FORGE.Camera.DEFAULT_CONFIG.gaze);

    this._viewer.renderer.view.onChange.add(this._updateInternals, this);
    this._viewer.renderer.onBackgroundReady.add(this._updateInternals, this);

    this._createMainCamera();
    this._createFlatCamera();
    this._createVRCameras();

    // Check config to allow default to be set if they were depending
    // on some parameter external to the camera. For example: multiresolution fovMin set
    // by the background renderer
    if (this._config !== null)
    {
        this._parseConfig(this._config);
    }
};

/**
 * Parse a camera configuration.
 * @method FORGE.Camera#_parseConfig
 * @param {?CameraConfig} config - The camera configuration to parse.
 * @private
 */
FORGE.Camera.prototype._parseConfig = function(config)
{
    this._parallax = config.parallax;
    this._radius = this._parallax * FORGE.Camera.RADIUS;
    this._keep = config.keep;

    if(this._keep === true && this._initialized === true)
    {
        return;
    }

    if (typeof config.fov.min === "number")
    {
        this._fovMin = FORGE.Math.degToRad(config.fov.min);
    }

    if (typeof config.fov.max === "number")
    {
        this._fovMax = FORGE.Math.degToRad(config.fov.max);
    }

    if (typeof config.fov.default === "number")
    {
        this._setFov(config.fov.default, FORGE.Math.DEGREES);
    }

    if (typeof config.yaw.min === "number")
    {
        this._yawMin = FORGE.Math.degToRad(config.yaw.min);
    }

    if (typeof config.yaw.max === "number")
    {
        this._yawMax = FORGE.Math.degToRad(config.yaw.max);
    }

    if (typeof config.yaw.default === "number")
    {
        this._setYaw(config.yaw.default, FORGE.Math.DEGREES);
    }

    if (typeof config.pitch.min === "number")
    {
        this._pitchMin = FORGE.Math.degToRad(config.pitch.min);
    }

    if (typeof config.pitch.max === "number")
    {
        this._pitchMax = FORGE.Math.degToRad(config.pitch.max);
    }

    if (typeof config.pitch.default === "number")
    {
        this._setPitch(config.pitch.default, FORGE.Math.DEGREES);
    }

    if (typeof config.roll.min === "number")
    {
        this._rollMin = FORGE.Math.degToRad(config.roll.min);
    }

    if (typeof config.roll.max === "number")
    {
        this._rollMax = FORGE.Math.degToRad(config.roll.max);
    }

    if (typeof config.roll.default === "number")
    {
        this._setRoll(config.roll.default, FORGE.Math.DEGREES);
    }

    this._updateFromEuler();

    this._updateMainCamera();
    this._updateFlatCamera();

    this._gaze.load(/** @type {CameraGazeConfig} */ (config.gaze));
};

/**
 * Reset the camera changelog.
 * @method FORGE.Camera#_resetChangelog
 * @private
 */
FORGE.Camera.prototype._resetChangelog = function()
{
    this._changelog =
    {
        yaw: false,
        pitch: false,
        roll: false,
        fov: false
    };
};

/**
 * Init the THREE PerspectiveCamera.
 * @method FORGE.Camera#_createMainCamera
 * @private
 */
FORGE.Camera.prototype._createMainCamera = function()
{
    if (typeof this._viewer.renderer !== "undefined")
    {
        var aspect = this._viewer.renderer.displayResolution.ratio;
        this._main = new THREE.PerspectiveCamera(this._fov, aspect, FORGE.RenderManager.DEPTH_NEAR, 2 * FORGE.RenderManager.DEPTH_FAR);
        this._main.name = "CameraMain";
        this._main.matrixAutoUpdate = false;
    }
};

/**
 * Init the THREE OrthographicCamera.
 * @method FORGE.Camera#_createFlatCamera
 * @private
 */
FORGE.Camera.prototype._createFlatCamera = function()
{
    if (typeof this._viewer.renderer !== "undefined")
    {
        this._flat = new THREE.OrthographicCamera(
            -1000, 1000,
            1000, -1000,
            FORGE.RenderManager.DEPTH_NEAR,
            FORGE.RenderManager.DEPTH_FAR);

        this._flat.name = "CameraFlat";
        this._flat.matrixAutoUpdate = false;
    }
};

/**
 * Create the left and right THREE PerspectiveCamera for VR.
 * @method FORGE.Camera#_createVRCameras
 * @private
 */
FORGE.Camera.prototype._createVRCameras = function()
{
    this._left = this._main.clone();
    this._left.name = "CameraLeft";
    this._left.layers.enable(1);

    this._left.add(this._gaze.object);

    this._right = this._main.clone();
    this._right.name = "CameraRight";
    this._right.layers.enable(2);
};

/**
 * Update VR cameras.
 * @method FORGE.Camera#_updateVRCameras
 * @private
 */
FORGE.Camera.prototype._updateVRCameras = function()
{
    var display = this._viewer.renderer.display;

    // Get frame data before pose to ensure pose values are up to date
    var frameData = display.vrFrameData;
    var quat = display.getQuaternionFromPose();

    if (quat !== null)
    {
        this._quaternion = quat;
        this._updateFromQuaternion();
    }

    var eyeParamsL = display.vrDisplay.getEyeParameters("left");
    var eyeParamsR = display.vrDisplay.getEyeParameters("right");

    this._main.matrixWorld.decompose(this._left.position, this._left.quaternion, this._left.scale);
    this._left.matrixWorld = new THREE.Matrix4().makeRotationFromQuaternion(this._main.quaternion);

    this._main.matrixWorld.decompose(this._right.position, this._right.quaternion, this._right.scale);
    this._right.matrixWorld = new THREE.Matrix4().makeRotationFromQuaternion(this._main.quaternion);

    // Get translation from central camera matrix
    this._left.matrixWorld.elements[12] = this._main.matrixWorld.elements[12] + eyeParamsL.offset[0];
    this._left.matrixWorld.elements[13] = this._main.matrixWorld.elements[13] + eyeParamsL.offset[1];
    this._left.matrixWorld.elements[14] = this._main.matrixWorld.elements[14] + eyeParamsL.offset[2];

    // Get translation from central camera matrix
    this._right.matrixWorld.elements[12] = this._main.matrixWorld.elements[12] + eyeParamsR.offset[0];
    this._right.matrixWorld.elements[13] = this._main.matrixWorld.elements[13] + eyeParamsR.offset[1];
    this._right.matrixWorld.elements[14] = this._main.matrixWorld.elements[14] + eyeParamsR.offset[2];

    // Setup camera projection matrix
    if (frameData !== null)
    {
        this._left.projectionMatrix.elements = frameData.leftProjectionMatrix;
        this._right.projectionMatrix.elements = frameData.rightProjectionMatrix;
    }
    else
    {
        var eyeFOVL = {
            upDegrees: eyeParamsL.fieldOfView.upDegrees,
            downDegrees: eyeParamsL.fieldOfView.downDegrees,
            leftDegrees: eyeParamsL.fieldOfView.leftDegrees,
            rightDegrees: eyeParamsL.fieldOfView.rightDegrees
        };

        this._left.projectionMatrix = this._fovToProjectionMatrix(eyeFOVL, this._main);

        var eyeFOVR = {
            upDegrees: eyeParamsR.fieldOfView.upDegrees,
            downDegrees: eyeParamsR.fieldOfView.downDegrees,
            leftDegrees: eyeParamsR.fieldOfView.leftDegrees,
            rightDegrees: eyeParamsR.fieldOfView.rightDegrees
        };

        this._right.projectionMatrix = this._fovToProjectionMatrix(eyeFOVR, this._main);
    }

    this._updateComplete();
};

/**
 * Clone VR cameras objects.
 * @method FORGE.Camera#_cloneVRCamerasChildren
 * @private
 */
FORGE.Camera.prototype._cloneVRCamerasChildren = function()
{
    //First clear all children from camera right
    for (var i = 0, ii = this._right.children.length; i < ii; i++)
    {
        this._right.remove(this._right.children[i]);
    }

    //Then clone all children of camera left to camera right
    var clone = null;
    for (var j = 0, jj = this._left.children.length; j < jj; j++)
    {
        clone = this._left.children[j].clone();
        this._right.add(clone);
    }
};

/**
 * Get projection matrix from a VRFieldOfView
 * @method FORGE.Camera#_fovToProjectionMatrix
 * @param {VRFieldOfViewObject} fov - VRFieldOfView for an eye
 * @param {THREE.PerspectiveCamera} camera - reference camera
 * @return {THREE.Matrix4} projection matrix
 * @private
 */
FORGE.Camera.prototype._fovToProjectionMatrix = function(fov, camera)
{
    // Get projections of field of views on zn plane
    var fovUpTan = Math.tan(FORGE.Math.degToRad(fov.upDegrees));
    var fovDownTan = Math.tan(FORGE.Math.degToRad(fov.downDegrees));
    var fovLeftTan = Math.tan(FORGE.Math.degToRad(fov.leftDegrees));
    var fovRightTan = Math.tan(FORGE.Math.degToRad(fov.rightDegrees));

    // and with scale/offset info for normalized device coords
    var pxscale = 2.0 / (fovLeftTan + fovRightTan);
    var pxoffset = (fovLeftTan - fovRightTan) * pxscale * 0.5;
    var pyscale = 2.0 / (fovUpTan + fovDownTan);
    var pyoffset = (fovUpTan - fovDownTan) * pyscale * 0.5;

    // start with an identity matrix
    var matrix = new THREE.Matrix4();
    var m = matrix.elements;

    // X result, map clip edges to [-w,+w]
    m[0 * 4 + 0] = pxscale;
    m[0 * 4 + 1] = 0.0;
    m[0 * 4 + 2] = -pxoffset;
    m[0 * 4 + 3] = 0.0;

    // Y result, map clip edges to [-w,+w]
    // Y offset is negated because this proj matrix transforms from world coords with Y=up,
    // but the NDC scaling has Y=down (thanks D3D?)
    m[1 * 4 + 0] = 0.0;
    m[1 * 4 + 1] = pyscale;
    m[1 * 4 + 2] = pyoffset;
    m[1 * 4 + 3] = 0.0;

    // Z result (up to the app)
    m[2 * 4 + 0] = 0.0;
    m[2 * 4 + 1] = 0.0;
    m[2 * 4 + 2] = camera.far / (camera.near - camera.far);
    m[2 * 4 + 3] = (camera.far * camera.near) / (camera.near - camera.far);

    // W result (= Z in)
    m[3 * 4 + 0] = 0.0;
    m[3 * 4 + 1] = 0.0;
    m[3 * 4 + 2] = -1.0;
    m[3 * 4 + 3] = 0.0;

    matrix.transpose();

    return matrix;
};

/**
 * Apply Camera change internally.
 * @method FORGE.Camera#_updateFromEuler
 * @private
 */
FORGE.Camera.prototype._updateFromEuler = function()
{
    this._modelView = FORGE.Math.eulerToRotationMatrix(this._yaw, this._pitch, this._roll, false);

    this._modelViewInverse = this._modelView.clone().transpose();

    this._quaternion = FORGE.Quaternion.fromEuler(this._yaw, this._pitch, this._roll);

    // complete camera update
    this._updateComplete();
};

/**
 * Camera update internals after quaternion has been set
 * @method FORGE.Camera#_updateFromQuaternion
 * @private
 */
FORGE.Camera.prototype._updateFromQuaternion = function()
{
    this._modelView = FORGE.Quaternion.toRotationMatrix(this._quaternion);

    this._modelViewInverse = this._modelView.clone().transpose();

    var euler = FORGE.Quaternion.toEuler(this._quaternion);

    this._setAll(euler.yaw, euler.pitch, euler.roll, null, FORGE.Math.RADIANS);
};

/**
 * Camera update internals after modelview matrix has been set.
 * @method FORGE.Camera#_updateFromMatrix
 * @private
 */
FORGE.Camera.prototype._updateFromMatrix = function()
{
    this._modelViewInverse = this._modelView.clone().transpose();

    var euler = FORGE.Math.rotationMatrixToEuler(this._modelView);

    this._setAll(euler.yaw, euler.pitch, euler.roll, null, FORGE.Math.RADIANS);

    this._quaternion = FORGE.Quaternion.fromRotationMatrix(this._modelView);
};

/**
 * THREE Perspective camera update internals after modelview matrix has been set.
 * @method FORGE.Camera#_updateMainCamera
 * @private
 */
FORGE.Camera.prototype._updateMainCamera = function()
{
    if (this._main === null || this._viewer.renderer.view.current === null)
    {
        return;
    }

    var mat = new THREE.Matrix4().copy(this._modelViewInverse);

    if (this._parallax !== 0)
    {
        mat.multiply(new THREE.Matrix4().makeTranslation(0, 0, -this._radius));
    }

    // Now set the object quaternion (side effect: it will override the world matrix)
    this._main.quaternion.setFromRotationMatrix(mat);

    this._main.matrixWorld = mat;
    this._main.matrixWorldInverse.getInverse(mat);

    this._main.fov = FORGE.Math.radToDeg(this._viewer.renderer.view.current.getProjectionFov());
    this._main.aspect = this._viewer.renderer.displayResolution.ratio;
    this._main.updateProjectionMatrix();
};

/**
 * THREE Orthographic camera update internals.
 * @method FORGE.Camera#_updateFlatCamera
 * @private
 */
FORGE.Camera.prototype._updateFlatCamera = function()
{
    if (this._flat === null)
    {
        return;
    }

    var camW = this._flat.right - this._flat.left;
    var camH = this._flat.top - this._flat.bottom;

    this._flat.left = this._flat.position.x - camW / 2;
    this._flat.right = this._flat.position.x + camW / 2;

    this._flat.top = this._flat.position.y + camH / 2;
    this._flat.bottom = this._flat.position.y - camH / 2;

    var max = this._fovMax;
    var view = this._viewer.renderer.view.current;

    if (view !== null && view.fovMax !== null)
    {
        max = Math.min(view.fovMax, this._fovMax);
        this._flat.zoom = max / this._fov;
    }
    else
    {
        this._flat.zoom = 1;
    }

    this._flat.updateProjectionMatrix();
};

/**
 * Final method call to complete camera update, ensure main camera is up to date.
 * @method FORGE.Camera#_updateComplete
 * @private
 */
FORGE.Camera.prototype._updateComplete = function()
{
    var changed = false;

    if(this._changelog.yaw === true || this._changelog.pitch === true || this._changelog.roll === true)
    {
        changed = true;

        if (this._onOrientationChange !== null)
        {
            this._onOrientationChange.dispatch(null, true);
        }
    }

    if(this._changelog.fov === true)
    {
        changed = true;

        if (this._onFovChange !== null)
        {
            this._onFovChange.dispatch(null, true);
        }
    }


    if (changed === true && this._onChange !== null)
    {
        this._onChange.dispatch(null, true);
    }
};


/**
 * Internal setter for yaw, take a value and a unit. Default unit is radians.
 * @method FORGE.Camera#_setYaw
 * @param {?number=} value - The value you want to set for yaw.
 * @param {string=} [unit="radians"] - The unit you use to set the yaw value.
 * @return {boolean} Returns true if the value has changed.
 * @private
 */
FORGE.Camera.prototype._setYaw = function(value, unit)
{
    if (typeof value !== "number" || isNaN(value) === true)
    {
        return false;
    }

    // If unit is not well defined, default will be radians
    unit = (unit === FORGE.Math.DEGREES || unit === FORGE.Math.RADIANS) ? unit : FORGE.Math.RADIANS;

    // Convert value in radians for clamp if unit is in degrees.
    value = (unit === FORGE.Math.DEGREES) ? FORGE.Math.degToRad(value) : value;

    // Wrap the value between -PI and +PI, except for FLAT view where we apply texture ratio
    if (this._viewer.renderer.backgroundRenderer !== null &&
        this._viewer.renderer.backgroundRenderer.displayObject !== null &&
        this._viewer.renderer.view.type === FORGE.ViewType.FLAT)
    {
        var displayObject = this._viewer.renderer.backgroundRenderer.displayObject;
        var ratio = displayObject.pixelWidth / displayObject.pixelHeight;

        if (displayObject.element instanceof HTMLVideoElement)
        {
            ratio = displayObject.element.videoWidth / displayObject.element.videoHeight;
        }

        value = FORGE.Math.wrap(value, -Math.PI * ratio, Math.PI * ratio);
    }
    else
    {
        value = FORGE.Math.wrap(value, -Math.PI, Math.PI);
    }

    var boundaries = this._getYawBoundaries();

    var yaw = FORGE.Math.clamp(value, boundaries.min, boundaries.max);

    var changed = this._yaw !== yaw;

    this._changelog.yaw = changed;

    this._yaw = yaw;

    return changed;
};

/**
 * Compute the yaw boundaries with yaw min and yaw max.
 * @method FORGE.Camera#_getYawBoundaries
 * @param {boolean=} relative - do we need to get the yaw relative to the current fov (default true)
 * @param {number=} fov - specify a fov if we do not want to use the current one (useful for simulation)
 * @return {CameraBoundaries} Returns the min and max yaw computed from the camera configuration and the view limits.
 * @private
 */
FORGE.Camera.prototype._getYawBoundaries = function(relative, fov)
{
    var min = this._yawMin;
    var max = this._yawMax;

    fov = fov || this._fov;

    if (relative !== false && min !== max)
    {
        var halfHFov = 0.5 * fov * this._viewer.renderer.displayResolution.ratio;
        min += halfHFov;
        max -= halfHFov;
    }

    var view = this._viewer.renderer.view.current;

    if (view !== null)
    {
        min = Math.max(view.yawMin, min);
        max = Math.min(view.yawMax, max);
    }

    return { min: min, max: max };
};

/**
 * Internal setter for pitch, take a value and a unit. Default unit is radians.
 * @method FORGE.Camera#_setPitch
 * @param {?number=} value - The value you want to set for pitch.
 * @param {string=} [unit="radians"] - The unit you use to set the pitch value.
 * @return {boolean} Returns true if the value has changed.
 * @private
 */
FORGE.Camera.prototype._setPitch = function(value, unit)
{
    if (typeof value !== "number" || isNaN(value) === true)
    {
        return false;
    }

    var oldPitch = this._pitch;

    // If unit is not well defined, default will be radians
    unit = (unit === FORGE.Math.DEGREES || unit === FORGE.Math.RADIANS) ? unit : FORGE.Math.RADIANS;

    // Convert value in radians for clamp if unit is in degrees.
    value = (unit === FORGE.Math.DEGREES) ? FORGE.Math.degToRad(value) : value;

    // Wrap the value between -PI and +PI
    value = FORGE.Math.wrap(value, -Math.PI, Math.PI);

    var boundaries = this._getPitchBoundaries();

    var pitch = FORGE.Math.clamp(value, boundaries.min, boundaries.max);

    // If old view accepted pitch out of [-PI/2 , PI/2] and new one does not,
    // check if old pitch value was in authorized range and if not, set to zero
    if (Math.abs(oldPitch) > Math.PI / 2 && Math.abs(pitch) === Math.PI / 2)
    {
        pitch = 0;
    }

    var changed = this._pitch !== pitch;

    this._changelog.pitch = changed;

    this._pitch = pitch;

    return changed;
};

/**
 * Compute the pitch boundaries with pitch min and pitch max.
 * @method FORGE.Camera#_getPitchBoundaries
 * @param {boolean=} relative - do we need to get the pitch relative to the current fov (default true)
 * @param {number=} fov - specify a fov if we do not want to use the current one (useful for simulation)
 * @return {CameraBoundaries} Returns the min and max pitch computed from the camera configuration and the view limits.
 * @private
 */
FORGE.Camera.prototype._getPitchBoundaries = function(relative, fov)
{
    var min = this._pitchMin;
    var max = this._pitchMax;

    fov = fov || this._fov;

    if (relative !== false && min !== max)
    {
        var halfFov = 0.5 * fov;
        min += halfFov;
        max -= halfFov;
    }

    var view = this._viewer.renderer.view.current;

    if (view !== null)
    {
        min = Math.max(view.pitchMin, min);
        max = Math.min(view.pitchMax, max);
    }

    return { min: min, max: max };
};

/**
 * Internal setter for roll, take a value and a unit. Default unit is radians.
 * @method FORGE.Camera#_setRoll
 * @param {?number=} value - The value you want to set for roll.
 * @param {string=} [unit="radians"] - The unit you use to set the roll value.
 * @return {boolean} Returns true if the value has changed.
 * @private
 */
FORGE.Camera.prototype._setRoll = function(value, unit)
{
    if (typeof value !== "number" || isNaN(value) === true)
    {
        return false;
    }

    // If unit is not well defined, default will be radians
    unit = (unit === FORGE.Math.DEGREES || unit === FORGE.Math.RADIANS) ? unit : FORGE.Math.RADIANS;

    // Convert value in radians for clamp if unit is in degrees.
    value = (unit === FORGE.Math.DEGREES) ? FORGE.Math.degToRad(value) : value;

    // Wrap the value between -PI and +PI
    value = FORGE.Math.wrap(value, -Math.PI, Math.PI);

    var boundaries = this._getRollBoundaries();

    var roll = FORGE.Math.clamp(value, boundaries.min, boundaries.max);

    var changed = this._roll !== roll;

    this._changelog.roll = changed;

    this._roll = roll;

    return changed;
};

/**
 * Compute the roll boundaries with yaw min and yaw max.
 * @method FORGE.Camera#_getRollBoundaries
 * @return {CameraBoundaries} Returns the min and max roll computed from the camera configuration and the view limits.
 * @private
 */
FORGE.Camera.prototype._getRollBoundaries = function()
{
    var min = this._rollMin;
    var max = this._rollMax;
    var view = this._viewer.renderer.view.current;

    if (view !== null)
    {
        min = Math.max(view.rollMin, min);
        max = Math.min(view.rollMax, max);
    }

    return { min: min, max: max };
};

/**
 * Internal setter for fov (field of view), take a value and a unit. Default unit is radians.
 * @method FORGE.Camera#_setFov
 * @param {?number=} value - The value you want to set for fov.
 * @param {string=} [unit="radians"] - The unit you use to set the fov value.
 * @return {boolean} Returns true if the value has changed.
 * @private
 */
FORGE.Camera.prototype._setFov = function(value, unit)
{
    if (typeof value !== "number" || isNaN(value) === true)
    {
        return false;
    }

    // If unit is not well defined, default will be radians
    unit = (unit === FORGE.Math.DEGREES || unit === FORGE.Math.RADIANS) ? unit : FORGE.Math.RADIANS;

    // Convert value in radians for clamp if unit is in degrees.
    value = (unit === FORGE.Math.DEGREES) ? FORGE.Math.degToRad(value) : value;

    var boundaries = this._getFovBoundaries();

    var fov = FORGE.Math.clamp(value, boundaries.min, boundaries.max);

    var changed = this._fov !== fov;

    this._changelog.fov = changed;

    this._fov = fov;

    if (changed)
    {
        this._setYaw(this._yaw);
        this._setPitch(this._pitch);
    }

    return changed;
};

/**
 * Compute the fov boundaries with yaw min and yaw max.
 * @method FORGE.Camera#_getFovBoundaries
 * @return {CameraBoundaries} Returns the min and max fov computed from the camera configuration and the view limits.
 * @private
 */
FORGE.Camera.prototype._getFovBoundaries = function()
{
    var min = this._fovMin;
    var max = this._fovMax;
    var view = this._viewer.renderer.view.current;

    // if JSON specifies a fov min (not default 0 value), use it
    // useful for multiresolution where fov limit will be computed depending
    // on max level of resolution available and stored in JSON
    if (this._viewer.renderer.backgroundRenderer !== null && "fovMin" in this._viewer.renderer.backgroundRenderer)
    {
        min = Math.max(this._viewer.renderer.backgroundRenderer.fovMin, min);
    }
    else if (min === 0)
    {
        if (view !== null)
        {
            min = Math.max(view.fovMin, min);
            max = Math.min(view.fovMax, max);
        }
    }

    if (view !== null && view.type !== FORGE.ViewType.FLAT)
    {
        // if there are limits, we may need to limit the maximum fov
        var pitchBoundaries = this._getPitchBoundaries(false);
        var pitchRange = pitchBoundaries.max - pitchBoundaries.min;

        if (pitchRange > 0)
        {
            max = Math.min(pitchRange, max);
        }

        var yawBoundaries = this._getYawBoundaries(false);
        var yawRange = yawBoundaries.max - yawBoundaries.min;
        yawRange /= this._viewer.renderer.displayResolution.ratio;

        if (yawRange > 0)
        {
            max = Math.min(yawRange, max);
        }

        // get the tiniest
        if (max < min)
        {
            min = max;
        }
    }

    return { min: min, max: max };
};

/**
 * Set all camera angles in one call (yaw, pitch, roll, fov)
 * @method FORGE.Camera#_setAll
 * @param {?number=} yaw - The yaw value you want to set.
 * @param {?number=} pitch - The pitch value you want to set.
 * @param {?number=} roll - The roll value you want to set.
 * @param {?number=} fov - The fov value you want to set.
 * @param {string=} unit - The unit you use for all the previous arguments (FORGE.Math.DEGREES or FORGE.Math.RADIANS)
 * @return {boolean} Returns true if any values has changed.
 * @private
 */
FORGE.Camera.prototype._setAll = function(yaw, pitch, roll, fov, unit)
{
    var fovChanged = this._setFov(fov, unit);
    var yawChanged = this._setYaw(yaw, unit);
    var pitchChanged = this._setPitch(pitch, unit);
    var rollChanged = this._setRoll(roll, unit);

    return (yawChanged === true || pitchChanged === true || rollChanged === true || fovChanged === true);
};

/**
 * Update internals after a remote component has changed something
 * @method FORGE.Camera#_updateInternals
 * @private
 */
FORGE.Camera.prototype._updateInternals = function()
{
    // Force camera to update its values to bound it in new boundaries after view change
    var changed = this._setAll(this._yaw, this._pitch, this._roll, this._fov);

    if (changed === true)
    {
        this._updateFromEuler();
    }
};

/**
 * Load a camera configuration.
 * @method FORGE.Camera#load
 * @param {CameraConfig} config - The camera configuration to load.
 */
FORGE.Camera.prototype.load = function(config)
{
    this._config = /** @type {CameraConfig} */ (FORGE.Utils.extendMultipleObjects(FORGE.Camera.DEFAULT_CONFIG, config));

    this._parseConfig(this._config);

    this._initialized = true;
};

/**
 * Set the Camera to look at a specified point into the yaw/pitch/roll space.
 * @method FORGE.Camera#lookAt
 * @param {?number=} yaw Euler yaw angle (deg)
 * @param {?number=} pitch Euler pitch angle (deg)
 * @param {?number=} roll Euler roll angle (deg)
 * @param {?number=} fov Field of view (deg)
 * @param {number=} durationMS - Rotation animation duration ms (undefined or zero means immediat effect)
 * @param {boolean=} [cancelRoll=false] - If set to true, roll will be cancelled (always at 0).<br> If false an auto roll movement will be done by the camera for a more natural movement effect.
 * @param {string=} easing - Easing method name (default to {@link FORGE.EasingType.LINEAR}).
 */
FORGE.Camera.prototype.lookAt = function(yaw, pitch, roll, fov, durationMS, cancelRoll, easing)
{
    if (typeof durationMS !== "number" || durationMS === 0)
    {
        var changed = this._setAll(yaw, pitch, roll, fov, FORGE.Math.DEGREES);

        if (changed === true)
        {
            this._updateFromEuler();
        }
    }
    else
    {
        if (fov !== null && typeof fov !== "undefined")
        {
            var fovBoundaries = this._getFovBoundaries();

            fov = FORGE.Math.clamp(fov, FORGE.Math.radToDeg(fovBoundaries.min), FORGE.Math.radToDeg(fovBoundaries.max));

            if (yaw !== null && typeof yaw !== "undefined")
            {
                var yawBoundaries = this._getYawBoundaries(true, FORGE.Math.degToRad(fov));
                yaw = FORGE.Math.clamp(yaw, FORGE.Math.radToDeg(yawBoundaries.min), FORGE.Math.radToDeg(yawBoundaries.max));
            }

            if (pitch !== null && typeof pitch !== "undefined")
            {
                var pitchBoundaries = this._getPitchBoundaries(true, FORGE.Math.degToRad(fov));
                pitch = FORGE.Math.clamp(pitch, FORGE.Math.radToDeg(pitchBoundaries.min), FORGE.Math.radToDeg(pitchBoundaries.max));
            }
        }

        // before creating a track, set the goto point in future boundaries
        var track = new FORGE.DirectorTrack(
        {
            easing:
            {
                default: easing || "LINEAR",
                start: 0
            },

            cancelRoll: Boolean(cancelRoll),

            keyframes:
            [
                {
                    time: durationMS,
                    data:
                    {
                        yaw: yaw,
                        pitch: pitch,
                        roll: roll,
                        fov: fov
                    }
                }
            ]
        });

        this.animation.play(track.uid);
    }
};

/**
 * Update routine called by render manager before rendering a frame.
 * All internals should be up to date.
 * @method FORGE.Camera#update
 */
FORGE.Camera.prototype.update = function()
{
    if (this._viewer.renderer.display.presentingVR === true)
    {
        this._gaze.update();
        this._updateVRCameras();
        this._cloneVRCamerasChildren();
    }

    this._updateMainCamera();
    this._updateFlatCamera();

    this._resetChangelog();
};

/**
 * Destroy sequence.
 * @method FORGE.Camera#destroy
 */
FORGE.Camera.prototype.destroy = function()
{
    this._modelView = null;
    this._modelViewInverse = null;
    this._quaternion = null;
    this._main = null;
    this._flat = null;

    this._gaze.destroy();
    this._gaze = null;

    this._viewer.renderer.view.onChange.remove(this._updateInternals, this);
    this._viewer.renderer.onBackgroundReady.remove(this._updateInternals, this);

    if (this._onChange !== null)
    {
        this._onChange.destroy();
        this._onChange = null;
    }

    if (this._onOrientationChange !== null)
    {
        this._onOrientationChange.destroy();
        this._onOrientationChange = null;
    }

    if (this._onFovChange !== null)
    {
        this._onFovChange.destroy();
        this._onFovChange = null;
    }

    if (this._cameraAnimation !== null)
    {
        this._cameraAnimation.destroy();
        this._cameraAnimation = null;
    }

    this._viewer = null;

    FORGE.BaseObject.prototype.destroy.call(this);
};

/**
 * Get and set the camera configuration (default min & max for all angles yaw, pitch, roll and fov).
 * @name FORGE.Camera#config
 * @type {CameraConfig}
 */
Object.defineProperty(FORGE.Camera.prototype, "config",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._config;
    },

    /** @this {FORGE.Camera} */
    set: function(config)
    {
        this.load(config);
    }
});

/**
 * Get the keep flag
 * @name FORGE.Camera#keep
 * @type {boolean}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "keep",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._keep;
    }
});

/**
 * Get and set the yaw value in degree.
 * @name FORGE.Camera#yaw
 * @type {number}
 */
Object.defineProperty(FORGE.Camera.prototype, "yaw",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return FORGE.Math.radToDeg(this._yaw);
    },

    /** @this {FORGE.Camera} */
    set: function(value)
    {
        var yawChanged = this._setYaw(value, FORGE.Math.DEGREES);

        if (yawChanged === true)
        {
            this._updateFromEuler();
        }
    }
});

/**
 * Get the yaw min value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#yawMin
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "yawMin",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getYawBoundaries();
        return FORGE.Math.radToDeg(boundaries.min);
    }
});

/**
 * Get the yaw max value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#yawMax
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "yawMax",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getYawBoundaries();
        return FORGE.Math.radToDeg(boundaries.max);
    }
});

/**
 * Get and set the pitch value in degree.
 * @name FORGE.Camera#pitch
 * @type {number}
 */
Object.defineProperty(FORGE.Camera.prototype, "pitch",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return FORGE.Math.radToDeg(this._pitch);
    },

    /** @this {FORGE.Camera} */
    set: function(value)
    {
        var pitchChanged = this._setPitch(value, FORGE.Math.DEGREES);

        if (pitchChanged)
        {
            this._updateFromEuler();
        }
    }
});

/**
 * Get the pitch min value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#pitchMin
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "pitchMin",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getPitchBoundaries();
        return FORGE.Math.radToDeg(boundaries.min);
    }
});

/**
 * Get the pitch max value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#pitchMax
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "pitchMax",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getPitchBoundaries();
        return FORGE.Math.radToDeg(boundaries.max);
    }
});

/**
 * Get and set the roll value in degree.
 * @name FORGE.Camera#roll
 * @type {number}
 */
Object.defineProperty(FORGE.Camera.prototype, "roll",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return FORGE.Math.radToDeg(this._roll);
    },

    /** @this {FORGE.Camera} */
    set: function(value)
    {
        var rollChanged = this._setRoll(value, FORGE.Math.DEGREES);

        if (rollChanged === true)
        {
            this._updateFromEuler();
        }
    }
});

/**
 * Get the roll min value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#rollMin
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "rollMin",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getRollBoundaries();
        return FORGE.Math.radToDeg(boundaries.min);
    }
});

/**
 * Get the roll max value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#rollMax
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "rollMax",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getRollBoundaries();
        return FORGE.Math.radToDeg(boundaries.max);
    }
});

/**
 * Get and set the fov value in degree.
 * @name FORGE.Camera#fov
 * @type {number}
 */
Object.defineProperty(FORGE.Camera.prototype, "fov",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return FORGE.Math.radToDeg(this._fov);
    },

    /** @this {FORGE.Camera} */
    set: function(value)
    {
        var fovChanged = this._setFov(value, FORGE.Math.DEGREES);

        if (fovChanged === true)
        {
            this._updateFromEuler();
        }
    }
});

/**
 * Get the fov min value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#fovMin
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "fovMin",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getFovBoundaries();
        return FORGE.Math.radToDeg(boundaries.min);
    }
});

/**
 * Get the fov max value.
 * Return the most restrictive value between the camera value and the view value.
 * @name FORGE.Camera#fovMax
 * @type {number}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "fovMax",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        var boundaries = this._getFovBoundaries();
        return FORGE.Math.radToDeg(boundaries.max);
    }
});

/**
 * Get/set quaternion rotation object of the camera.
 * Setter will update internal quaternion object
 * @name FORGE.Camera#quaternion
 * @readonly
 * @type {THREE.Quaternion}
 */
Object.defineProperty(FORGE.Camera.prototype, "quaternion",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._quaternion;
    },
    /** @this {FORGE.Camera} */
    set: function(value)
    {
        this._quaternion = value;
        this._updateFromQuaternion();
        this._updateComplete();
    }
});

/**
 * Get camera animation manager.
 * @name FORGE.Camera#animation
 * @readonly
 * @type {FORGE.CameraAnimation}
 */
Object.defineProperty(FORGE.Camera.prototype, "animation",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._cameraAnimation === null)
        {
            this._cameraAnimation = new FORGE.CameraAnimation(this._viewer, this);
        }

        return this._cameraAnimation;
    }
});

/**
 * Get/Set parallax setting.
 * @name FORGE.Camera#parallax
 * @type number
 */
Object.defineProperty(FORGE.Camera.prototype, "parallax",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._parallax;
    },
    /** @this {FORGE.Camera} */
    set: function(value)
    {
        this._parallax = FORGE.Math.clamp(value, 0, 1);
        this._radius = this._parallax * FORGE.Camera.RADIUS;
        this._updateComplete();
    }
});

/**
 * Get the modelView of the camera.
 * @name FORGE.Camera#modelView
 * @type {THREE.Matrix4}
 */
Object.defineProperty(FORGE.Camera.prototype, "modelView",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._modelView;
    },
    /** @this {FORGE.Camera} */
    set: function(value)
    {
        this._modelView = value;
        this._updateFromMatrix();
        this._updateComplete();
    }
});

/**
 * Get the modelViewInverse of the camera.
 * @name FORGE.Camera#modelViewInverse
 * @readonly
 * @type {THREE.Matrix4}
 */
Object.defineProperty(FORGE.Camera.prototype, "modelViewInverse",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._modelViewInverse;
    }
});

/**
 * Get the main THREE.PerspectiveCamera of the camera.
 * @name FORGE.Camera#main
 * @readonly
 * @type {THREE.PerspectiveCamera}
 */
Object.defineProperty(FORGE.Camera.prototype, "main",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._main === null)
        {
            this._createMainCamera();
        }

        return this._main;
    }
});

/**
 * Get the flat THREE.OrthographicCamera of the camera.
 * @name FORGE.Camera#flat
 * @readonly
 * @type {THREE.OrthographicCamera}
 */
Object.defineProperty(FORGE.Camera.prototype, "flat",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._flat === null)
        {
            this._createFlatCamera();
        }

        return this._flat;
    }
});

/**
 * Get the THREE.PerspectiveCamera radius.
 * @name FORGE.Camera#perspectiveCameraRadius
 * @readonly
 * @type {number}
 */
Object.defineProperty(FORGE.Camera.prototype, "perspectiveCameraRadius",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._radius;
    }
});

/**
 * Get the left camera.
 * @name FORGE.Camera#left
 * @type {THREE.PerspectiveCamera}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "left",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._left;
    }
});

/**
 * Get the right camera.
 * @name FORGE.Camera#right
 * @type {THREE.PerspectiveCamera}
 * @readonly
 */
Object.defineProperty(FORGE.Camera.prototype, "right",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._right;
    }
});

/**
 * Get the camera gaze.
 * @name FORGE.Camera#gaze
 * @readonly
 * @type {FORGE.CameraGaze}
 */
Object.defineProperty(FORGE.Camera.prototype, "gaze",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        return this._gaze;
    }
});

/**
 * Get the "onChange" {@link FORGE.EventDispatcher} of the camera.
 * @name FORGE.Camera#onChange
 * @readonly
 * @type {FORGE.EventDispatcher}
 */
Object.defineProperty(FORGE.Camera.prototype, "onChange",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._onChange === null)
        {
            this._onChange = new FORGE.EventDispatcher(this);
        }

        return this._onChange;
    }
});

/**
 * Get the "onOrientationChange" {@link FORGE.EventDispatcher} of the camera.
 * @name FORGE.Camera#onOrientationChange
 * @readonly
 * @type {FORGE.EventDispatcher}
 */
Object.defineProperty(FORGE.Camera.prototype, "onOrientationChange",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._onOrientationChange === null)
        {
            this._onOrientationChange = new FORGE.EventDispatcher(this);
        }

        return this._onOrientationChange;
    }
});

/**
 * Get the "onFovChange" {@link FORGE.EventDispatcher} of the camera.
 * @name FORGE.Camera#onFovChange
 * @readonly
 * @type {FORGE.EventDispatcher}
 */
Object.defineProperty(FORGE.Camera.prototype, "onFovChange",
{
    /** @this {FORGE.Camera} */
    get: function()
    {
        if (this._onFovChange === null)
        {
            this._onFovChange = new FORGE.EventDispatcher(this);
        }

        return this._onFovChange;
    }
});