src/provider/WorldProvider.js

goog.module('gep.provider.WorldProvider');

const {Event,EventTarget} = goog.require('goog.events');
const userAgent = goog.require('goog.userAgent');

/**
 * This provider (singleton) takes care of settings, status changes (e.g. also change of camera view) of the 3D scene.
 * @extends {EventTarget}
 */
class WorldProvider extends EventTarget
{
    constructor()
    {
        super();

        /**
         * Checks if the device is a mobile device.
         * @type {boolean}
         */
        this.isMobileDevice = userAgent.MOBILE || userAgent.IOS || userAgent.ANDROID;

        /**
         * Current {@Link WorldState}
         * @type {string}
         * @private
         */
        this.state_ = '';

        /**
         * Main loading progress of the 3D scene
         * @type {number}
         * @private
         */
        this.loadProgress_ = 0;

        /**
         * Is Cannon physics calculation enabled for the 3D Scene
         * @type {boolean}
         */
        this.physicIsEnabled = false;

        /**
         * If the scene is currently viewed from the top view
         * @type {boolean}
         * @private
         */
        this.isTopView_ = false;

        /**
         * If the scene is currently being viewed from an exhibit view (e.g. from an exhibit orbit view)
         * @type {boolean}
         * @private
         */
        this.isExhibitView_ = false;

        /**
         * If the viewer is currently in an activation zone of an exhibit,
         * its model id and the corresponding artwork model id are stored
         * here for later assignment.
         * @type {?{id:number,elementId:number}}
         */
        this.activeExhibit_ = null;

        /**
         * List of THREE objects that could interact with the THREE raycaster (e.g. for collision detection).
         * @type {Array<THREE.Object3D>}
         */
        this.intersectables = [];

        /**
         * Indicates whether the transition of a view change (e.g. animation from the visitor view to the top view) takes place at the moment.
         * @type {boolean}
         */
        this.isSwitchingView = false;

        /**
         * General settings object to set properties in the 3D and address them globally. So these values are also more easily updateable via a GUI {@Link https://github.com/dataarts/dat.gui/blob/master/API.md}.
         * @type {{lights:Object,background:Object,fog:Object,artwork:Object,ground:Object,physic:Object,moving:Object,profile:Object,sound:Object}}
         */
        this.settings = {
            lights: {
                'hemiLightColor': 0xf2f2f2,
                'hemiLightGroundColor': 0xeed69a,
                'hemiLightIntensity': 1,
                'hemiLightIntensityTopview': 1,
                'dirLightColor': 0xffffff,
                'dirLightIntensity': .5,
                'dirLightOffsetX': 30,
                'dirLightOffsetY': 200,
                'dirLightOffsetZ': 50,
            },
            background: {
                'color': 0xfffbf6,
            },
            fog: {
                'color': 0xfffbf6,
                'density': 0.015,
            },
            ground: {
                'color': 0xfff2dc,
            },
            artwork: {
                'envMap': true,
            },
            physic: {
                'fixedTimeStep': 1 / 60,
                'maxSubSteps': 3,
                'worldScale': 1,
                'attractionSpeed': 15,
                'mass': 3,
                'linearDamping': .95,
                'scaleSpeed': 0.002
            },
            moving: {
                'accelerationMoveDirect': this.getMoveSettings('accelerationMoveDirect'),
                'frictionMoveDirect': this.getMoveSettings('frictionMoveDirect'),
                'accelerationMoveSide': 25,
                'frictionMoveSide': this.getMoveSettings('frictionMoveSide'),
                'accelerationRotateHorizontal': this.getMoveSettings('accelerationRotateHorizontal'),
                'frictionRotateHorizontal': this.getMoveSettings('frictionRotateHorizontal'),
                'accelerationRotateVertical':  20,
                'frictionRotateVertical': 8,
                'chairRotationIncreaseFactor': this.getMoveSettings('chairRotationIncreaseFactor'),
                'chairRotationSpeed': 30,
                'chairRotationStaticAngleLimit': 20,
                'chairRotationStaticTriggerInterval': 0.05,
                'chairRotationStaticSpeed': 0.1,
                'verticalRotationLimitDown': this.getMoveSettings('verticalRotationLimitDown'),
                'verticalRotationLimitUp': this.getMoveSettings('verticalRotationLimitUp'),
                'useStaticAngleLimit': false,
            },
            profile: {
                'changeMultiplier': .1
            },
            sound: {
                'influenceRadius': 10
            }
        };
    }

    /**
     * Returns individual settings values that are adjusted taking into account the currently used control.
     * @param {string} key
     * @return {number}
     */
    getMoveSettings(key)
    {
        switch(key) {
            case 'accelerationMoveDirect': return 40;
            case 'frictionMoveDirect': return window['GAMEPAD_MODE'] == 'on' ? 3 : 6;
            case 'frictionMoveSide': return window['GAMEPAD_MODE'] == 'on' ? 5 : 6;
            case 'accelerationRotateHorizontal': return window['GAMEPAD_MODE']  == 'on'? 90 : 50;
            case 'frictionRotateHorizontal': return window['GAMEPAD_MODE'] == 'on' ? 3 : 5;
            case 'chairRotationIncreaseFactor': return window['GAMEPAD_MODE'] == 'on' ? 3 : 1.8;
            case 'verticalRotationLimitDown': return window['GAMEPAD_MODE'] == 'on' ? -10 : 0;
            case 'verticalRotationLimitUp': return window['GAMEPAD_MODE'] == 'on' ? 90 : -10;
            case 'cameraYMax': return window['GAMEPAD_MODE'] == 'on' ? 2 : 3;
            default: return this.settings.moving[key];
        }
    }

    /**
     * Fires the {@Link WorldEventType} `START_BUILDING` event
     */
    build()
    {
        this.dispatchEvent(new Event(WorldEventType.START_BUILDING));
    }

    /**
     * Changes the state to {@Link WorldState} `READY` and fires the {@Link WorldEventType} `LOADED` event
     */
    loaded()
    {
        this.state_ = WorldState.READY;
        this.dispatchEvent(new Event(WorldEventType.LOADED));
    }

    /**
     * Updates the loading progress and fires the {@Link WorldEventType} `LOAD_PROGRESS` event
     * @param {number} progress
     */
    updateLoadProgress(progress)
    {
        this.loadProgress_ = progress;
        this.dispatchEvent(new Event(WorldEventType.LOAD_PROGRESS));
    }

    /**
     * Changes the state to {@Link WorldState} `ACTIVE` and fires the {@Link WorldEventType} `ACTIVATE` event
     */
    activate()
    {
        this.state_ = WorldState.ACTIVE;
        this.dispatchEvent(new Event(WorldEventType.ACTIVATE));
    }

    /**
     * Changes the state to {@Link WorldState} `ERROR` and fires the {@Link WorldEventType} `ERROR` event
     */
    showNoSupportMessage()
    {
        this.state_ = WorldState.ERROR;
        this.dispatchEvent(new Event(WorldEventType.ERROR));
    }

    /**
     * Fires the {@Link WorldEventType} `PAUSE_RENDERING` event
     */
    pauseRendering()
    {
        this.dispatchEvent(new Event(WorldEventType.PAUSE_RENDERING));
    }

    /**
     * Fires the {@Link WorldEventType} `START_RENDERING` event
     */
    startRendering()
    {
        this.dispatchEvent(new Event(WorldEventType.START_RENDERING));
    }

    /**
     * Fires the {@Link WorldEventType} `CHANGE_MUTING` event
     */
    changeMuting()
    {
        this.dispatchEvent(new Event(WorldEventType.CHANGE_MUTING));
    }

    /**
     * Toggles the normal and top view state value and fires the {@Link WorldEventType} `SWITCH_WORLD_VIEW` event
     */
    switchWorldView()
    {
        if(this.isSwitchingView)
            return;

        this.isTopView_ = !this.isTopView_;
        this.dispatchEvent(new Event(WorldEventType.SWITCH_WORLD_VIEW));
    }

    /**
     * Toggles the normal and exhibit/orbit view state value and fires the {@Link WorldEventType} `SWITCH_EXHIBIT_VIEW` event
     */
    switchExhibitView()
    {
        if(this.isSwitchingView)
            return;

        this.isExhibitView_ = !this.isExhibitView_;
        this.dispatchEvent(new Event(WorldEventType.SWITCH_EXHIBIT_VIEW));
    }

    /**
     * Sets the current active exhibit and fires the {@Link WorldEventType} `ACTIVATE_EXHIBIT` event
     * @param {number} exhibitId Exhibit model id
     * @param {number} elementId Artwork model id
     */
    activateExhibit(exhibitId, elementId)
    {
        this.activeExhibit_ = {id: exhibitId, elementId: elementId};
        this.dispatchEvent(new Event(WorldEventType.ACTIVATE_EXHIBIT));
    }

    /**
     * Sets the active exhibit to null and fires the {@Link WorldEventType} `DEACTIVATE_EXHIBIT` event
     */
    deactivateExhibit()
    {
        this.activeExhibit_ = null;
        this.dispatchEvent(new Event(WorldEventType.DEACTIVATE_EXHIBIT));
    }

    /**
     * Fires the {@Link WorldEventType}UPDATE_CONTROLLING} event
     */
    updateControlling()
    {
        this.dispatchEvent(new Event(WorldEventType.UPDATE_CONTROLLING));
    }

    /**
     * Getter
     * @return {string}
     */
    get state()
    {
        return this.state_;
    }

    /**
     * Getter
     * @return {number}
     */
    get loadProgress()
    {
        return this.loadProgress_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get isTopView()
    {
        return this.isTopView_;
    }

    /**
     * Getter
     * @return {boolean}
     */
    get isExhibitView()
    {
        return this.isExhibitView_;
    }

    /**
     * Getter
     * @return {?{id:number,elementId:number}}
     */
    get activeExhibit()
    {
        return this.activeExhibit_;
    }
}

goog.addSingletonGetter(WorldProvider);

/**
 * @enum {string}
 */
const WorldState = {
    /** ready */    READY: 'ready',
    /** active */   ACTIVE: 'active',
    /** error */    ERROR: 'error'
};

/**
 * @enum {string}
 */
const WorldEventType = {
    /** error-world */              ERROR: 'error-world',
    /** start-building-world */     START_BUILDING: 'start-building-world',
    /** loaded-world */             LOADED: 'loaded-world',
    /** loading-world-progress */   LOAD_PROGRESS: 'loading-world-progress',
    /** activate-world */           ACTIVATE: 'activate-world',
    /** show-world-guiding */       SHOW_GUIDING: 'show-world-guiding',
    /** hide-world-guiding */       HIDE_GUIDING: 'hide-world-guiding',
    /** complete-world-guiding */   COMPLETE_GUIDING: 'complete-world-guiding',
    /** pause-rendering */          PAUSE_RENDERING: 'pause-rendering',
    /** start-rendering */          START_RENDERING: 'start-rendering',
    /** change-muting */            CHANGE_MUTING: 'change-muting',
    /** switch-world-view */        SWITCH_WORLD_VIEW: 'switch-world-view',
    /** switch-exhibit-view */      SWITCH_EXHIBIT_VIEW: 'switch-exhibit-view',
    /** activate-view */            ACTIVATE_EXHIBIT: 'activate-view',
    /** deactivate-view */          DEACTIVATE_EXHIBIT: 'deactivate-view',
    /** update-controlling */       UPDATE_CONTROLLING: 'update-controlling'
};

exports = {WorldProvider,WorldState,WorldEventType};