src/components/Exhibition.js

goog.module('gep.components.Exhibition');

const Completer = goog.require('clulib.async.Completer');

const Abstract3DWorld = goog.require('gep.components.Abstract3DWorld');
const {WorldEventType} = goog.require('gep.provider.WorldProvider');
const {ExhibitionProvider,ExhibitionElementOrbitViewModel} = goog.require('gep.provider.ExhibitionProvider');
const {LayerProvider,LayerEventType,LayerEvent,LayerType} = goog.require('gep.provider.LayerProvider');
const VideoProvider = goog.require('gep.provider.VideoProvider');
const SoundProvider = goog.require('gep.provider.SoundProvider');
const AutoResetProvider = goog.require('gep.provider.AutoResetProvider');
const ControlProvider = goog.require('gep.provider.ControlProvider');
const {MoveController,MoveType} = goog.require('gep.controller.MoveController');
const Exhibit = goog.require('gep.world.Exhibit');
const Visitor = goog.require('gep.world.Visitor');
const {HintBox,HintBoxContentType} = goog.require('gep.components.HintBox');
const SecurityProvider = goog.require('gep.provider.SecurityProvider');

const {getBrowserPoints} = goog.require('nbsrc.utils.parser');

const {listen,EventType,Event,listenOnce} = goog.require('goog.events');
const KeyCodes = goog.require('goog.events.KeyCodes');
const classlist = goog.require('goog.dom.classlist');
const dataset = goog.require('goog.dom.dataset');
const {Coordinate} = goog.require('goog.math');

/**
 * This component mainly controls the loading, display, control and additional functionality of the entire 3D exhibition.
 * @extends {Abstract3DWorld}
 */
class Exhibition extends Abstract3DWorld
{
    constructor()
    {
        super();

        /**
         * Reference to SecurityProvider for monitoring the data security consent.
         * @type {SecurityProvider}
         * @private
         */
        this.securityProvider_ = SecurityProvider.getInstance();

        /**
         * Reference to ExhibitionProvider to communicate with the backend and provide and monitor all information that dynamically changes the application.
         * @type {ExhibitionProvider}
         * @private
         */
        this.exhibitionProvider_ = ExhibitionProvider.getInstance();

        /**
         * Reference to LayerProvider for monitoring and controlling layer appearance and disappearance.
         * @type {LayerProvider}
         * @private
         */
        this.layerProvider_ = LayerProvider.getInstance();

        /**
         * Reference to VideoProvider for creating and monitoring Vimeo video instances.
         * @type {VideoProvider}
         * @private
         */
        this.videoProvider_ = VideoProvider.getInstance();

        /**
         * Reference to AutoResetProvider to monitor and control the automatic reset of the page when the user is inactive for a specified period of time.
         * @type {AutoResetProvider}
         * @private
         */
        this.autoResetProvider_ = AutoResetProvider.getInstance();

        /**
         * Reference to SoundProvider for monitoring and executing sound loading processes and sound playback controling.
         * @type {SoundProvider}
         * @private
         */
        this.soundProvider_ = SoundProvider.getInstance();

        /**
         * Reference to ControlProvider to monitor the activation of a gamepad controller so that the application's control is adapted to it.
         * @type {ControlProvider}
         * @private
         */
        this.controlProvider_ = ControlProvider.getInstance();

        /**
         * Initialization of the MoveController with values for acceleration and friction.
         * `x`, `z`: values for the visitor movement in the in the 3D space;
         * `rh`, `rv`: values visitor rotation in the 3D space
         * @type {MoveController}
         * @private
         */
        this.moveController_ = new MoveController(
            new Map([
                ['x', this.worldProvider_.getMoveSettings('accelerationMoveSide')],
                ['y', 0],
                ['z', this.worldProvider_.getMoveSettings('accelerationMoveDirect')],
                ['rh', this.worldProvider_.getMoveSettings('accelerationRotateHorizontal')],
                ['rv', this.worldProvider_.getMoveSettings('accelerationRotateVertical')]]),
            new Map([
                ['x', this.worldProvider_.getMoveSettings('frictionMoveSide')],
                ['y', 0],
                ['z', this.worldProvider_.getMoveSettings('frictionMoveDirect')],
                ['rh', this.worldProvider_.getMoveSettings('frictionRotateHorizontal')],
                ['rv', this.worldProvider_.getMoveSettings('frictionRotateVertical')]]),
        );
        this.moveController_.velocity = {'x': 0, 'y': 0, 'z': 0, 'rh': 0, 'rv': 0};

        /**
         * Main THREE scene
         * @type {THREE.Scene}
         * @private
         */
        this.scene_ = new THREE.Scene();

        /**
         * List of created lights for the 3D scene
         * @type {Map<string,THREE.Light>}
         * @private
         */
        this.lights_ = new Map();

        /**
         * list of all created exhibits
         * @type {Array<Exhibit>}
         * @private
         */
        this.exhibits_ = [];

        /**
         * Cannon world for handling all Cannon physic objects.
         * @type {CANNON.World}
         * @private
         */
        this.physicWorld_ = null;

        /**
         * Instance of the created visitor object
         * @type {Visitor}
         * @private
         */
        this.visitor_ = null;

        /**
         * 3D container that contains the camera for the top view and is used for position changes.
         * @type {THREE.Group}
         * @protected
         */
        this.cameraTopviewWrapper_ = new THREE.Group();

        /**
         * Camera for rendering the top view
         * @type {THREE.Camera}
         * @protected
         */
        this.cameraTopview_ = null;

        /**
         * Camera vor randering an exhibit orbit view
         * @type {THREE.Camera}
         * @protected
         */
        this.cameraOribitview_ = null;

        /**
         * Ground color plane
         * @type {THREE.Mesh}
         * @private
         */
        this.ground_ = null;

        /**
         * List of Cannon sensors that have detected a collision change in {@Link Exhibition#handleCollisionStart_} and {@Link Exhibition#handleCollisionEnd_}
         * @type {Array<string>}
         * @private
         */
        this.collidedSensors_ = [];

        /**
         * Timeout id for the delayed appearing of view switch buttons
         * @type {number}
         * @private
         */
        this.viewSwitchTimeout_ = 0;

        /**
         * THREE orbit controls will be used for the exhibit orbit view
         * @type {THREE.OrbitControls}
         * @protected
         */
        this.orbitControls_ = null;

        /**
         * Holds the current orbit view configuration (azimuthal angle, polar angle and zoom)
         * @type {{azimuthalAngle:number,polarAngle:number,zoom:number}}
         * @private
         */
        this.orbit_ = {azimuthalAngle:0, polarAngle:0, zoom:0};

        /**
         * Any values can be stored in this object. The zoom value is stored before the change from the visitor view to
         * the exhibit orbit view in order to animate back to this value during the animation in the initial state.
         * @type {Map<string,number>}
         * @private
         */
        this.savedProperties_ = new Map();

        /**
         * Contains all THREE collision objects of the exhibits for checking the collision by raycasting.
         * @type {Array<THREE.Object3D>}
         * @private
         */
        this.allExhibitColliders_ = [];

        /**
         * This value indicates that it is the first entering of an activation zone by a visitor after reshuffling.
         * As a result, no interactions with the activation zones are triggered during reshuffling.
         * @type {boolean}
         * @private
         */
        this.isFirstActivation_ = true;

        /**
         * Specifies when to check in exhibits whether certain 3D elements can be removed after the first interaction with an artwork.
         * @type {boolean}
         * @private
         */
        this.checkExhibits_ = false;

        /**
         * Cannon physics debugger to display polygon outline segments for the mass collision objects when reshuffeling occurs.
         * @type {Object}
         * @private
         */
        this.physicDebugger_ = null;

        /**
         * An optional countdown for the start of reshuffling can be displayed here. The countdown is then displayed at the bottom right of the page.
         * @type {number}
         * @private
         */
        this.activationCountdown_ = 0;

        /**
         * Holds the values of a detected Cannon sensor that was activated during the reshuffeling and is executed if it is still active when the reshuffeling is complete.
         * @type {{elementId:number, exhibitId:number}|null}
         * @private
         */
        this.virtualActiveSensor_ = null;

        /**
         * Specifies whether debug rendering is generally enabled for the Cannon world. This can be activated via the URL parameter `physic=1`.
         * @type {boolean}
         * @private
         */
        this.enableDebugRendering_ = window['PHYSIC_MODE'] === 'on';

        /**
         * Defines if the countdown for the reshuffeling of the exhibits is running.
         * @type {boolean}
         * @private
         */
        this.countdownIsRunning_ = false;

        /**
         * Promise which is solved when the navigation hint boxes (`NAVIGATION_MOVEMENT`, `NAVIGATION_PERSPECTIVE` and `NAVIGATION_TOPVIEW`) were displayed.
         * @type {Promise}
         * @private
         */
        this.hintNavigationPromise_ = null;

        /**
         * Completer is a helper which returns a promise. It is solved when the first interaction of the user has been registered.
         * In this case, the user has clicked on the audio hint box button that is displayed at startup.
         * @type {Completer}
         * @private
         */
        this.firstInteractionCompleter_ = new Completer();

        /**
         * Represents the Helper GUI {@Link https://github.com/dataarts/dat.gui} to change some world settings {@Link WorldProvider#settings} on the fly.
         * The GUI is activated and displayed by the URL parameter `gui=1`.
         * @type {dat.GUI}
         * @private
         */
        this.gui_ = null;
    }

    /**
     * Component is ready and had loaded all dependencies (inherit method waitFor and sub components).
     * @inheritDoc
     */
    onInit()
    {
        this.shouldBuildImmediately_ = false;

        classlist.enable(document.body, 'is-mobile-device', this.worldProvider_.isMobileDevice);

        super.onInit();

        /**
         * Dom reference for the button which should show the exhibit infos.
         * @type {Element}
         * @private
         */
        this.exhibitInfoOpener_ = this.getElement().querySelector('.exhibit-info-opener');

        /**
         * Dom reference for the button which should switch into the exhibit/orbit view.
         * @type {Element}
         * @private
         */
        this.exhibitViewOpener_ = this.getElement().querySelector('.exhibit-view-opener--orbit');

        /**
         * Dom reference for the button which should switch back to the normal visitor view.
         * @type {Element}
         * @private
         */
        this.exhibitViewCloser_ = this.getElement().querySelector('.exhibit-view-closer');

        /**
         * Dom reference for the button which should show an external installation in an iframe layer.
         * @type {Element}
         * @private
         */
        this.exhibitFrameOpener_ = this.getElement().querySelector('.exhibit-view-opener--frame');

        /**
         * Dom reference for the interaction hint which is displayed until the reshuffeling is completed.
         * @type {Element}
         * @private
         */
        this.exhibitInteractiveHint_ = this.getElement().querySelector('.exhibit-hint--interactive');

        /**
         * Component reference for controlling the hint appearing.
         * @type {HintBox}
         * @private
         */
        this.hintBox_ = /** @type {HintBox} */ (this.queryComponent('[data-cmp="hint-box"]'));

        /**
         * Dom reference for the activation hint when a {@Link Exhibition#activationCountdown_} time is defined.
         * @type {Element}
         * @private
         */
        this.activationHint_ = /** @type {Element} */ (document.body.querySelector('.activation-hint'));
        /** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = this.activationCountdown_.toString();

        /**
         * Interval id for checking the loading progress.
         * @type {number}
         * @private
         */
        this.loadCheckInterval_ = 0;

        /**
         * Defines if the mass objects of all exhibits are scalled up for normal collision behaviour.
         * @type {boolean}
         * @private
         */
        this.allMassBodiesAreScalledUp_ = false;

        this.hintBox_.show(HintBoxContentType.ACTIVATE_AUDIO, 0).then(() => {
            // check the user data securioty consent
            if (window['GAMEPAD_MODE'] !== 'on' && this.securityProvider_.legalsAccepted === false)
            {
                this.layerProvider_.show('blocking');
                listenOnce(this.securityProvider_, EventType.CHANGE, () => {
                    this.firstInteractionCompleter_.resolve();
                }, false, this);
            }
            else
            {
                this.firstInteractionCompleter_.resolve();
            }
        }, (error) => {console.warn(error);});
        this.initScene_().then(() => {
            this.creationCompleter_.resolve();
        });

        this.ready().then(() => {
            this.soundProvider_.enableBackgroundSound = true;
            this.renderScene_(true);
            classlist.add(this.getElement(), 'is-ready');
            this.activateScene_();
            this.countdownIsRunning_ = true;
            if(this.activationCountdown_ > 0)
                setTimeout(() => { classlist.enable(this.activationHint_, 'is-visible', true); }, 1000);
            else
                this.hintBox_.show(HintBoxContentType.ACTIVATE_RESHUFFLING, 0).catch((error) => {console.warn(error);});
        });

        let profileOpener = this.activationHint_.querySelector('.activation-hint-message-link--profile');
        if(profileOpener)
            listen(profileOpener, EventType.CLICK, () => {
                this.layerProvider_.show('visitor', {'id': 'profile'});
            }, false, this);

        listen(this.controlProvider_, EventType.UPDATE, this.checkControlling_, false, this);

        //blocks browser scroll with arrow keys
        listen(window, EventType.KEYDOWN, (event) => { if([KeyCodes.UP, KeyCodes.DOWN].indexOf(event.keyCode) != -1) event.preventDefault(); }, false);

        listen(document.documentElement, EventType.KEYDOWN, (event) => {
            if(window['GAMEPAD_MODE'] === 'on' && [KeyCodes.R, KeyCodes.F, KeyCodes.V, KeyCodes.G, KeyCodes.X, KeyCodes.Y, KeyCodes.Z, KeyCodes.B].indexOf(event.keyCode) != -1) {
                this.layerProvider_.isOpen('guiding') ? this.layerProvider_.hide('guiding') : this.layerProvider_.show('guiding');
            }
        }, false, this);
    }

    /**
     * Checks and updates the controller settings.
     * @private
     */
    checkControlling_()
    {
        if(this.visitor_)
        {
            this.visitor_.cameraPolarAngleRange.start = this.worldProvider_.getMoveSettings('verticalRotationLimitDown') * Math.PI / 180;
            this.visitor_.cameraPolarAngleRange.end = this.worldProvider_.getMoveSettings('verticalRotationLimitUp') * Math.PI / 180;
        }

        this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationMoveSide'),'x');
        this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationMoveDirect'),'z');
        this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationRotateHorizontal'),'rh');
        this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationRotateVertical'),'rv');

        this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionMoveSide'), 'x');
        this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionMoveDirect'), 'z');
        this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionRotateHorizontal'), 'rh');
        this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionRotateVertical'), 'rv');
    }

    /**
     * Can be called to receive a Promise that is triggered when the main exhibition is loaded and the first interaction of the user was fulfilled.
     * @inheritDoc
     */
    ready()
    {
        return Promise.all([
            super.ready(),
            this.firstInteractionCompleter_.getPromise().then(() => { classlist.add(document.documentElement, 'has-completed-first-interaction'); })
        ]);
    }

    /**
     * Creates the main WebGLRenderer and updates some of its settings.
     * @inheritDoc
     */
    createRenderer_()
    {
        super.createRenderer_();

        if(this.renderer_)
        {
            this.renderer_.outputEncoding = THREE.sRGBEncoding;
            this.renderer_.toneMapping = THREE.ACESFilmicToneMapping;
        }
    }

    /**
     * Loads and creates the 3D scene.
     * @inheritDoc
     */
    async buildScene_()
    {
        // Define ids of exhibits which should be excluded in mobile or desktop version
        this.exhibitionProvider_.excludedItemIds = this.worldProvider_.isMobileDevice ? [] : [];

        await this.exhibitionProvider_.ready();
        const visitorTopTexture = await this.exhibitionProvider_.loadTextureFile('images/textures/visitor-sphere.png').then((texture) => {return texture;});

        this.visitor_ = new Visitor();
        this.visitor_.build(new THREE.Vector3(0, this.visitor_.cameraNormalY, 20), .6, visitorTopTexture);
        this.visitor_.object3D.rotation.y = 0 * Math.PI / 180;
        this.scene_.add(this.visitor_.object3D);

        this.cameraTopview_ = new THREE.PerspectiveCamera(30.0, window.innerWidth / window.innerHeight, 1, 1000); // 35.0, window.innerWidth / window.innerHeight, .1, 2000
        this.cameraTopviewWrapper_.add(this.cameraTopview_);
        this.cameraTopview_.name = "topviewCamera";
        this.scene_.add(this.cameraTopviewWrapper_);

        this.scene_.background = new THREE.Color(this.worldProvider_.settings.background['color']);
        this.scene_.fog = new THREE.FogExp2(this.worldProvider_.settings.fog['color'], this.worldProvider_.settings.fog['density']);

        let groundMaterial = new THREE.MeshPhongMaterial({'color': this.worldProvider_.settings.ground['color']});
        this.ground_ = new THREE.Mesh(
            new THREE.PlaneGeometry(2000, 2000),
            groundMaterial
        );
        this.ground_.name = 'ground';
        this.ground_.position.set(0, 0, 0);
        this.ground_.rotation.x = -90 * (Math.PI / 180);
        this.ground_.castShadow = false;
        this.ground_.receiveShadow = true;
        this.scene_.add(this.ground_);
        this.worldProvider_.intersectables.push(this.ground_);

        this.changeCamera_(this.visitor_.camera);

        let hemisphereLight = new THREE.HemisphereLight(
            this.worldProvider_.settings.lights['hemiLightColor'],
            this.worldProvider_.settings.lights['hemiLightGroundColor'],
            this.worldProvider_.settings.lights['hemiLightIntensity']
        );
        hemisphereLight.position.set(0, 0, 0);
        this.scene_.add(hemisphereLight);
        this.lights_.set('hemisphereLight', hemisphereLight);

        if(window['GUI_MODE'] === 'on')
            this.createDebugGui_();

        //physic
        this.physicWorld_ = new CANNON.World({
            'gravity': new CANNON.Vec3(0, 0, 0)
        });
        this.physicWorld_.solver.iterations = 10;
        this.physicWorld_.addBody(this.visitor_.physicBody);
        this.exhibitionProvider_.physicBodyReferences.set(this.visitor_.physicBody.id, 'visitor::mass');
        this.physicWorld_.addBody(this.visitor_.physicSensor);
        this.exhibitionProvider_.physicBodyReferences.set(this.visitor_.physicSensor.id, 'visitor');

        //loading
        let soundProgress = 0;
        this.soundProvider_.soundAssetPath = 'sounds/';
        this.soundProvider_.load(['background.mp3']).then(() => { soundProgress = 1; }, (error) => { console.warn(error); });

        let totalProgress = 1; // 2 for reflection texture and sound
        let cubeTextureProgress = 0;
        this.exhibitionProvider_.items.forEach((item) => {
            if(item.transformFile.trim() != '')
                totalProgress++;
            item.elements.forEach((element) => {
                if(element.file3D.trim() != '')
                    totalProgress++;
            });
        });

        // Interval checking for the loading progress
        this.loadCheckInterval_ = setInterval(() => {
            let progress = cubeTextureProgress + soundProgress;
            this.exhibits_.forEach((item) => {
                progress += item.progress;
            });
            let loadProgress = 1 / totalProgress * progress;
            this.worldProvider_.updateLoadProgress(loadProgress);
            if(loadProgress == 1)
            {
                clearTimeout(this.loadCheckInterval_);
            }
        }, 100);

        //reflection texture
        await this.exhibitionProvider_.loadReflectionTexture((progress) => {cubeTextureProgress = progress;}).then(() => {cubeTextureProgress = 1;});

        //exhibits
        const massObjects = [];
        for(let i=0; i<this.exhibitionProvider_.items.length; i++)
        {
            let exhibit = new Exhibit(i, this.exhibitionProvider_.items[i]);
            this.exhibits_.push(exhibit);
            await exhibit.build();
            exhibit.updateMassBodyScale(.001);
            this.scene_.add(exhibit.object3D);
            exhibit.physicBodies.forEach((physicBody) => {
                this.physicWorld_.addBody(physicBody);
            });
            exhibit.physicColliders.forEach((physicBody) => {
                this.physicWorld_.addBody(physicBody);
            });
            this.allExhibitColliders_ = this.allExhibitColliders_.concat(exhibit.collisionObjects);
            massObjects.push(exhibit.physicMassBody);
        }

        this.physicDebugger_ = CANNON.cannonDebugger(this.scene_, window['PHYSIC_MODE'] === 'on' ? this.physicWorld_.bodies : massObjects, {autoUpdate: false});

        this.worldProvider_.loaded();
    }

    /**
     * Defines the values that can be shown and changed in the GUI. The GUI is displayed with the url parameter `gui=1`.
     * @private
     */
    createDebugGui_()
    {
        if (this.gui_)
            return;

        this.gui_ = new dat.GUI({'width': 400});

        // background
        const backgroundGUI = this.gui_.addFolder('background');
        backgroundGUI.addColor(this.worldProvider_.settings.background, 'color')
            .onChange(() => {
                this.scene_.background.set(new THREE.Color(this.worldProvider_.settings.background['color']));
            });

        // lighting
        const lightingGUI = this.gui_.addFolder('lighting');
        lightingGUI.addColor(this.worldProvider_.settings.lights, 'hemiLightColor')
            .onChange(() => {
                this.lights_.get('hemisphereLight').color.set(this.worldProvider_.settings.lights['hemiLightColor']);
            });
        lightingGUI.addColor(this.worldProvider_.settings.lights, 'hemiLightGroundColor')
            .onChange(() => {
                /** @type {THREE.HemisphereLight} */ (this.lights_.get('hemisphereLight')).groundColor.set(this.worldProvider_.settings.lights['hemiLightGroundColor']);
            });
        lightingGUI.add(this.worldProvider_.settings.lights, 'hemiLightIntensity', 0, 1).step(0.01).listen()
            .onChange(() => {
                this.lights_.get('hemisphereLight').intensity = this.worldProvider_.settings.lights['hemiLightIntensity'];
            });

        // scene & color
        const fogGUI = this.gui_.addFolder('fog');
        fogGUI.addColor(this.worldProvider_.settings.fog, 'color')
            .onChange(() => {
                this.scene_.fog.color.set(this.worldProvider_.settings.fog['color']);
            });
        fogGUI.add(this.worldProvider_.settings.fog, 'density', 0, .1).step(0.001).listen()
            .onChange(() => {
                this.scene_.fog.density = this.worldProvider_.settings.fog['density'];
            });
        const groundGUI = this.gui_.addFolder('ground');
        groundGUI.addColor(this.worldProvider_.settings.ground, 'color')
            .onChange(() => {
                this.ground_.material.color.set(this.worldProvider_.settings.ground['color']);
            });
        const artworkGUI = this.gui_.addFolder('artwork');
        artworkGUI.add(this.worldProvider_.settings.artwork, 'envMap')
            .onChange(() => {
                this.exhibits_.forEach((exhibit) => {
                    exhibit.enableMaterialEnvMap(this.worldProvider_.settings.artwork['envMap']);
                })
            });

        // movment
        const controllsGUI = this.gui_.addFolder('controlls');
        controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationMoveDirect', 0, 200).step(1).listen()
            .onChange(() => {
                this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationMoveDirect'], 'z');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'frictionMoveDirect', 0, 20).step(1).listen()
            .onChange(() => {
                this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionMoveDirect'], 'z');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationMoveSide', 0, 200).step(1).listen()
            .onChange(() => {
                this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationMoveSide'], 'x');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'frictionMoveSide', 0, 20).step(1).listen()
            .onChange(() => {
                this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionMoveSide'], 'x');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationRotateHorizontal', 0, 100).step(1).listen()
            .onChange(() => {
                this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationRotateHorizontal'], 'rh');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'frictionRotateHorizontal', 0, 20).step(1).listen()
            .onChange(() => {
                this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionRotateHorizontal'], 'rh');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationRotateVertical', 0, 100).step(1).listen()
            .onChange(() => {
                this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationRotateVertical'], 'rv');
            });
        controllsGUI.add(this.worldProvider_.settings.moving, 'frictionRotateVertical', 0, 20).step(1).listen()
            .onChange(() => {
                this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionRotateVertical'], 'rv');
            });
    }

    /**
     * Enable the active scene for rendering and interaction.
     * Adds all important listener.
     * @inheritDoc
     */
    activateScene_()
    {
        super.activateScene_();

        this.worldProvider_.activate();

        this.autoResetProvider_.start();
        this.moveController_.activate(/** @type {!Element} */ (this.getElement()), false, false, false, window['GAMEPAD_MODE'] === 'on');
        listen(this.worldProvider_, WorldEventType.SWITCH_WORLD_VIEW, this.handleSwitchView_, false, this);
        listen(this.worldProvider_, WorldEventType.SWITCH_EXHIBIT_VIEW, this.handleSwitchExhibitView_, false, this);

        listen(this.exhibitInfoOpener_, EventType.CLICK, this.openExhibitInfo_, false, this);
        listen(this.exhibitViewOpener_, EventType.CLICK, this.changeExhibitView_, false, this);
        listen(this.exhibitViewCloser_, EventType.CLICK, this.changeExhibitView_, false, this);
        listen(this.exhibitFrameOpener_, EventType.CLICK, this.showExhibitFrame_, false, this);

        listen(this.layerProvider_, LayerEventType.SHOW_LAYER, this.handleLayerShow_, false, this);
        listen(this.layerProvider_, LayerEventType.HIDE_LAYER, this.handleLayerHide_, false, this);

        listen(this.videoProvider_, EventType.PLAY, this.handleVideoPlay_, false, this);
        listen(this.videoProvider_, EventType.PAUSE, this.handleVideoPause_, false, this);

        listen(this.renderer_.domElement, EventType.CLICK, this.handleCanvasClick_, false, this);

        this.visitor_.physicSensor.addEventListener('collide', this.handleCollisionStart_.bind(this));
        this.physicWorld_.addEventListener('endContact', this.handleCollisionEnd_.bind(this));
        this.physicWorld_.addEventListener("preStep", this.handlePhysicPreStep_.bind(this));

        this.worldProvider_.physicIsEnabled = true;
    }

    /**
     * Shows the different navigation hints ({@Link HintBox}) for the user one after the other.
     * @return {Promise}
     * @private
     */
    showNavigationHints_()
    {
        if(window['GAMEPAD_MODE'] == 'on')
            return Promise.resolve();

        if(!this.hintNavigationPromise_) {
            this.hintNavigationPromise_ = this.hintBox_.show(HintBoxContentType.NAVIGATION_MOVEMENT).then(() => {
                return this.hintBox_.show(HintBoxContentType.NAVIGATION_PERSPECTIVE).then(() => {
                    if(!this.worldProvider_.isMobileDevice)
                        return this.hintBox_.show(HintBoxContentType.NAVIGATION_TOPVIEW).catch((error) => {console.warn(error);});
                }, (error) => {console.warn(error);});
            }, (error) => {console.warn(error);});
        }
        return this.hintNavigationPromise_;
    }

    /**
     * Initializes the opening of the layer for the exhibit infos.
     * @private
     */
    openExhibitInfo_()
    {
        if(this.resizeProvider_.getViewportSize().width < 1024)
            this.layerProvider_.hide('visitor');

        this.layerProvider_.show('exhibit', {'id': this.worldProvider_.activeExhibit.id, 'elementId': this.worldProvider_.activeExhibit.elementId});
    }

    /**
     * Analyzes the click on the canvas when the exhibition is in top view and checks by raycasting
     * if an object in the list of {@Link Worldprovider#intersectables} has been hit.
     * The position of the visitor changes to the clicked position.
     * A minimum distance to the next artwork is kept so that the user does not move too far away from all objects in the room.
     * If an intersectable (exhibit or scenographic element) is hit, the next possible free position to the object is calculated.
     * @param {Event} event
     * @private
     */
    handleCanvasClick_(event)
    {
        // topview
        if(this.worldProvider_.isTopView && !this.worldProvider_.isSwitchingView)
        {
            let point = getBrowserPoints(event)[0];

            let viewSize = this.resizeProvider_.getViewportSize();
            let mouseVector = new THREE.Vector2(
                (point.x / viewSize.width) * 2 - 1,
                -(point.y / viewSize.height) * 2 + 1
            );

            this.raycaster_.setFromCamera(mouseVector, this.camera_);
            this.raycaster_.far = Infinity;
            let intersects = this.raycaster_.intersectObjects(this.worldProvider_.intersectables);
            if(intersects[0])
            {
                let pos = null;
                if(intersects[0]['object']['name'] == 'ground')
                {
                    pos = /** @type {THREE.Vector3} */ (intersects[0]['point']);
                }
                else if(intersects[0]['object']['name'].substr(0,14) == 'boundingObject')
                {
                    let nameObj = intersects[0]['object']['name'].split('::');
                    let exhibit = this.exhibits_[parseInt(nameObj[1], 10)];
                    let elementId = nameObj[2] != '' ? parseInt(nameObj[2], 10) : exhibit.model.artworks.length == 1 ? exhibit.model.artworks[0] : '';
                    if(exhibit && elementId)
                        pos = exhibit.getWorldPositionOf('interactionObject::'+exhibit.id.toString()+'::'+elementId.toString());
                }

                if(pos != null)
                {
                    let closestExhibit = null;
                    let distanceVisitorToClosestExhibit = Infinity;
                    this.exhibits_.forEach((exhibit) => {
                        let distance = /** @type {THREE.Vector3} */ (pos).distanceTo(exhibit.object3D.position);
                        if(distance < distanceVisitorToClosestExhibit)
                        {
                            closestExhibit = exhibit;
                            distanceVisitorToClosestExhibit = distance;
                        }
                    });

                    // check if new pos is outside the visitor max distance to any exhibit
                    if(closestExhibit && distanceVisitorToClosestExhibit > this.visitor_.maxDistanceToExhibit)
                    {
                        const angle = closestExhibit.object3D.position.angleTo(pos);
                        pos.x = Math.cos(angle) * this.visitor_.maxDistanceToExhibit + closestExhibit.object3D.position.x;
                        pos.z = Math.sin(angle) * this.visitor_.maxDistanceToExhibit + closestExhibit.object3D.position.z;
                    }

                    let distance = Math.sqrt(Math.pow(this.cameraTopviewWrapper_.position.x - pos.x,2) + Math.pow(this.cameraTopviewWrapper_.position.z - pos.z,2));
                    gsap.to(this.cameraTopviewWrapper_.position, {'x': pos.x, 'z': pos.z, 'duration': Math.min(1, distance), 'ease': 'power2.inOut', 'onComplete': () => {
                        this.worldProvider_.switchWorldView();
                    }});
                }
            }
        }
    }

    /**
     * Performs the animated switch from visitor view to top view and vice versa.
     * @private
     */
    handleSwitchView_()
    {
        this.worldProvider_.isSwitchingView = true;
        this.autoResetProvider_.start();

        let pos = this.visitor_.camera.getWorldPosition(new THREE.Vector3());
        this.visitor_.enableTopViewPlane(this.worldProvider_.isTopView, this.worldProvider_.isTopView ? .6 : 0);

        // switches from visitor view to top view
        if(this.worldProvider_.isTopView)
        {
            const rotateToGlobaleAxis = false; //set to true to have a correct compass view
            gsap.killTweensOf(this.cameraTopviewWrapper_.position);
            let tweenObject = {
                'wy': pos.y,
                'wry': this.visitor_.object3D.rotation.y,
                'fd': this.scene_.fog.density,
                'crx': 0,
                'hli': this.lights_.get('hemisphereLight').intensity,
            };
            let targetRotation = rotateToGlobaleAxis ? 0 : tweenObject['wry'];
            this.cameraTopviewWrapper_.position.set(pos.x, pos.y, pos.z);
            this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
            this.changeCamera_(this.cameraTopview_);
            let timeline = gsap.timeline({'onComplete': () => {
                this.worldProvider_.isSwitchingView = false;
            }});
            timeline.to(tweenObject, {'wy': 300, 'wry': targetRotation, 'fd': 0, 'hli': this.worldProvider_.settings.lights['hemiLightIntensityTopview'], 'duration': 1, 'ease': 'power2.inOut',
                'onUpdate': () => {
                    this.cameraTopviewWrapper_.position.y = tweenObject['wy'];
                    this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
                    this.scene_.fog.density = tweenObject['fd'];
                    this.lights_.get('hemisphereLight').intensity = tweenObject['hli'];
                }
            }, 0);
            timeline.to(tweenObject, {'crx': -90 * Math.PI / 180, 'duration': .75, 'ease': 'power2.inOut',
                'onUpdate': () => {
                    this.cameraTopview_.rotation.x = tweenObject['crx'];
                }
            }, 0);
        }
        // switches from top view to visitor view
        else
        {
            let tweenObject = {
                'wy': this.cameraTopviewWrapper_.position.y,
                'wry': this.cameraTopviewWrapper_.rotation.y,
                'fd': this.scene_.fog.density,
                'crx': this.cameraTopview_.rotation.x,
                'hli': this.lights_.get('hemisphereLight').intensity,
            };
            let timeline = gsap.timeline({'onComplete': () => {
                this.worldProvider_.isSwitchingView = false;
                this.visitor_.physicBody.position.x = this.cameraTopviewWrapper_.position.x;
                this.visitor_.physicBody.position.z = this.cameraTopviewWrapper_.position.z;
                this.changeCamera_(this.visitor_.camera);
            }});
            timeline.to(tweenObject, {'wy': pos.y, 'wry': this.visitor_.object3D.rotation.y, 'fd': this.worldProvider_.settings.fog['density'], 'hli': this.worldProvider_.settings.lights['hemiLightIntensity'], 'duration': 1, 'ease': 'power2.inOut',
                'onUpdate': () => {
                    this.cameraTopviewWrapper_.position.y = tweenObject['wy'];
                    this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
                    this.scene_.fog.density = tweenObject['fd'];
                    this.lights_.get('hemisphereLight').intensity = tweenObject['hli'];
                },
            }, 0);
            timeline.to(tweenObject, {'crx': 0, 'duration': .75, 'delay': .25, 'ease': 'power2.inOut',
                'onUpdate': () => {
                    this.cameraTopview_.rotation.x = tweenObject['crx'];
                }
            }, 0);
        }
    }

    /**
     * Triggers the change from the visitor view to the top view and vice versa.
     * For the period of the change the interaction possibility of the user is deactivated.
     * @private
     */
    changeExhibitView_()
    {
        this.moveController_.stopInteraction();
        this.worldProvider_.switchExhibitView();
    }

    /**
     * Initializes the switch from visitor view to exhibit orbit view and vice versa.
     * @private
     */
    handleSwitchExhibitView_()
    {
        let open = this.worldProvider_.isExhibitView === true;
        clearTimeout(this.viewSwitchTimeout_);
        classlist.remove(open ? this.exhibitViewOpener_ : this.exhibitViewCloser_, 'is-visible');

        let exhibit = this.exhibits_[this.worldProvider_.activeExhibit.id];
        let elementId = this.worldProvider_.activeExhibit.elementId;
        let orbitView = exhibit.getOrbitViewOfElement(elementId);
        let hasHighresObject = exhibit.model.getElement(elementId).fileHighres3D.trim() != '';

        // switches from visitor view to exhibit orbit view if an orbit model exists
        if(open)
        {
            if(orbitView)
            {
                let artwork3D = exhibit.getArtwork3D(elementId);
                this.layerProvider_.hide('exhibit');
                let isLoadingHighres = hasHighresObject;
                if(isLoadingHighres)
                    exhibit.loadHighresObject(elementId).then(() => {
                        if(exhibit.getHightResolutionState(elementId))
                        {
                            exhibit.switchObjectResolution(elementId, true);
                            this.hintBox_.hide(HintBoxContentType.ARTWORK_LOADING_HIGHRES);
                        }
                        isLoadingHighres = false;
                    });
                this.initOrbitView_(artwork3D, orbitView).then(() => {
                    classlist.add(this.exhibitViewCloser_, 'is-visible');
                    if(isLoadingHighres)
                        this.hintBox_.show(HintBoxContentType.ARTWORK_LOADING_HIGHRES, 0).catch((error) => {console.warn(error);})
                });
            }
            else
            {
                this.viewSwitchTimeout_ = setTimeout(() => {
                    classlist.add(this.exhibitViewCloser_, 'is-visible');
                }, 400);
            }
        }

        // switches from exhibit orbit view to visitor view if orbit controls were initialized
        else
        {
            if(this.orbitControls_)
            {
                this.layerProvider_.hide('exhibit');
                this.orbitControls_.enabled = false;
                if(hasHighresObject)
                {
                    exhibit.switchObjectResolution(elementId, false);
                    this.hintBox_.hide(HintBoxContentType.ARTWORK_LOADING_HIGHRES);
                }
                this.leaveOrbitView_().then(() => {
                    classlist.add(this.exhibitViewOpener_, 'is-visible');
                    if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
                        this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
                });
            }
            else
            {
                this.viewSwitchTimeout_ = setTimeout(() => {
                    classlist.add(this.exhibitViewOpener_, 'is-visible');
                }, 400);
            }
        }
    }

    /**
     * Starts the animated change from the visitor view to the exhibit orbit view
     * @param {THREE.Object3D} artwork3D Artwork which should be the center of the orbit view.
     * @param {ExhibitionElementOrbitViewModel} orbitView
     * @return {Promise}
     * @private
     */
    initOrbitView_(artwork3D, orbitView)
    {
        this.autoResetProvider_.cancel();
        this.visitor_.controlsAreEnabled = false;

        let orbitCenter = artwork3D.getWorldPosition(new THREE.Vector3());
        orbitCenter.add(orbitView.center);
        let vX = this.visitor_.object3D.position.x - orbitCenter.x;
        let vZ = this.visitor_.object3D.position.z - orbitCenter.z;
        let magV = Math.sqrt(vX*vX + vZ*vZ);
        let closestOrbitPoint = new THREE.Vector3(
            orbitCenter.x + vX / magV * orbitView.distance,
            0,
            orbitCenter.z + vZ / magV * orbitView.distance
        );

        if(!this.cameraOribitview_)
        {
            this.cameraOribitview_ = /** @type {THREE.Camera} */ (this.camera_.clone());
            this.cameraOribitview_.name = 'orbitCamera';
            this.scene_.add(this.cameraOribitview_);
        }
        this.cameraOribitview_.position.x = closestOrbitPoint.x;
        this.cameraOribitview_.position.y = orbitCenter.y;
        this.cameraOribitview_.position.z = closestOrbitPoint.z;
        this.cameraOribitview_.rotation.y = 0;
        this.cameraOribitview_.lookAt(orbitCenter);

        return this.animateToClosestOrbitPoint_(closestOrbitPoint, orbitCenter.y, 0, 0).then(() => {
            this.changeCamera_(this.cameraOribitview_);

            this.orbitControls_ = new THREE.OrbitControls(this.camera_, this.renderer_.domElement);
            this.orbitControls_.target.set(orbitCenter.x, orbitCenter.y, orbitCenter.z);
            this.orbitControls_.minDistance = orbitView.limitDistance ? orbitView.limitDistance.start : .5;
            this.orbitControls_.maxDistance = orbitView.limitDistance ? orbitView.limitDistance.end : 8;
            this.orbitControls_.minAzimuthAngle = orbitView.limitAzimuth ? orbitView.limitAzimuth.start * Math.PI / 180 : Infinity;
            this.orbitControls_.minAzimuthAngle = orbitView.limitAzimuth ? orbitView.limitAzimuth.end * Math.PI / 180 : Infinity;
            this.orbitControls_.minPolarAngle = (orbitView.limitPolar ? orbitView.limitPolar.start : 30) * Math.PI / 180;
            this.orbitControls_.maxPolarAngle = (orbitView.limitPolar ? orbitView.limitPolar.end : 95) * Math.PI / 180;
            this.orbitControls_.enablePan = true;
            this.orbitControls_.enableDamping = true;

            this.orbitControls_.update();
            this.savedProperties_.set('orbitZoomLevel', this.orbitControls_.getZoomLevel());
            this.savedProperties_.set('orbitCenterX', orbitCenter.x);
            this.savedProperties_.set('orbitCenterY', orbitCenter.y);
            this.savedProperties_.set('orbitCenterZ', orbitCenter.z);

            this.orbit_.azimuthalAngle = this.orbitControls_.getAzimuthalAngle();
            this.orbit_.polarAngle = this.orbitControls_.getPolarAngle();
            this.orbit_.zoom = this.orbitControls_.getZoomLevel();
            this.autoResetProvider_.start();
        });
    }

    /**
     * Starts the animated change from the visitor view to the exhibit orbit view
     * @param {THREE.Vector3} closestOrbitPoint Closest vector form the visitor position to the default obit view radius (zoom).
     * @param {number} targetCameraY
     * @param {number} targetCameraZ
     * @param {number} targetCameraRotationX
     * @return {Promise}
     * @private
     */
    animateToClosestOrbitPoint_(closestOrbitPoint, targetCameraY, targetCameraZ, targetCameraRotationX)
    {
        let completer = new Completer();

        let startQuaternion = this.visitor_.object3D.quaternion;
        let targetQuaternion = this.cameraOribitview_.quaternion;
        let tweenObject = {
            't': 0,
            'x': this.visitor_.physicBody.position.x,
            'z': this.visitor_.physicBody.position.z,
            'cy': this.visitor_.camera.position.y,
            'cz': this.visitor_.camera.position.z,
            'crx': this.visitor_.camera.rotation.x,
        };

        let distancePoint = Math.sqrt(Math.pow(tweenObject.x - closestOrbitPoint.x,2) + Math.pow(tweenObject.z - closestOrbitPoint.z,2));
        let distanceCamera = Math.max(Math.abs(this.visitor_.camera.position.y - targetCameraY), Math.abs(this.visitor_.camera.position.z - targetCameraZ));

        gsap.to(tweenObject,{
            't': 1,
            'x': closestOrbitPoint.x,
            'z': closestOrbitPoint.z,
            'cy': targetCameraY,
            'cz': targetCameraZ,
            'crx': targetCameraRotationX,
            'duration': Math.max(distancePoint, distanceCamera),
            'ease': 'power2.inOut',
            'onUpdate': () => {
                //position
                this.visitor_.physicBody.position.x = tweenObject['x'];
                this.visitor_.physicBody.position.z = tweenObject['z'];

                //camera
                this.visitor_.camera.position.y = tweenObject['cy'];
                this.visitor_.camera.position.z = tweenObject['cz'];
                this.visitor_.camera.rotation.x = tweenObject['crx'];

                //rotation
                let qm = startQuaternion.clone();
                qm.slerp(targetQuaternion, tweenObject['t']);
                this.visitor_.object3D.rotation.setFromQuaternion(qm);
            },
            'onComplete': () => {
                completer.resolve();
            }
        });

        return completer.getPromise();
    }

    /**
     * Starts the animated change from the exhibit orbit view back to the visitor view
     * @return {Promise}
     * @private
     */
    leaveOrbitView_()
    {
        let completer = new Completer();
        this.orbitControls_.enabled = false;
        this.autoResetProvider_.cancel();

        let tweenObject1 = {
            'azimuthAngle': this.orbitControls_.getAzimuthalAngle(),
            'polarAngle': this.orbitControls_.getPolarAngle(),
            'zoom': this.orbitControls_.getZoomLevel(),
        };
        let tweenObject2 = {
            'y': this.visitor_.camera.position.y
        };

        let distancePolarAngle = Math.abs((90 * Math.PI / 180) - tweenObject1['polarAngle']);
        let timeline = gsap.timeline({'ease': 'power2.inOut', 'onComplete': () => {completer.resolve();}});
        timeline.to(tweenObject1,{
            'polarAngle': 90 * Math.PI / 180,
            'zoom': this.savedProperties_.get('orbitZoomLevel'),
            'duration': distancePolarAngle,
            'ease': 'none',
            'onUpdate': () => {
                this.orbitControls_.updateToPoint(tweenObject1['azimuthAngle'], tweenObject1['polarAngle'], tweenObject1['zoom'])
            },
            'onComplete': () => {
                this.visitor_.physicBody.position.x = this.cameraOribitview_.position.x;
                this.visitor_.physicBody.position.z = this.cameraOribitview_.position.z;
                this.visitor_.object3D.rotation.y = this.orbitControls_.getAzimuthalAngle();

                this.changeCamera_(this.visitor_.camera);
                this.orbitControls_.dispose();
                this.orbitControls_ = null;
            }
        });
        if(this.visitor_.camera.position.y != this.visitor_.cameraNormalY)
        {
            let distanceCameraY = Math.abs(this.visitor_.camera.position.y - this.visitor_.cameraNormalY);
            timeline.to(tweenObject2, {
                'y': this.visitor_.cameraNormalY,
                'duration': distanceCameraY * .5,
                'ease': 'none',
                'onUpdate': () => {
                    this.visitor_.camera.position.y = tweenObject2['y'];
                }
            });
        }

        return completer.getPromise().then(() => {
            if(!this.isFirstActivation_) this.visitor_.controlsAreEnabled = true;
            this.autoResetProvider_.start();
        })
    }

    /**
     * Initializes the display of the layer for the external installation of an exhibit.
     * Rendering is paused during display to save performance.
     * @private
     */
    showExhibitFrame_()
    {
        this.autoResetProvider_.cancel();
        this.visitor_.controlsAreEnabled = false;
        this.worldProvider_.pauseRendering();

        this.videoProvider_.pauseAllPlayers();

        let elementModel = this.exhibitionProvider_.items[this.worldProvider_.activeExhibit.id].getElement(this.worldProvider_.activeExhibit.elementId);

        this.layerProvider_.show('installation', {'url': elementModel.externUrl});
    }

    /**
     * Disables the background sound when a video is started.
     * @private
     */
    handleVideoPlay_()
    {
        this.soundProvider_.enableBackgroundSound = false;
    }

    /**
     * Enables the background sound when a video is paused/stopped.
     * @private
     */
    handleVideoPause_()
    {
        if(window['SOUND_MODE'] != 'generic-platform')
            this.soundProvider_.enableBackgroundSound = true;
    }

    /**
     * Monitors whether a layer is opened and performs different actions depending on the layer.
     * @param {LayerEvent} event
     * @private
     */
    handleLayerShow_(event)
    {
        switch(event.layer.type)
        {
            case LayerType.VISITOR:
                // hides navigation elements on the left viewport side (under the visitor layer)
                if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
                clearTimeout(this.viewSwitchTimeout_);
                classlist.remove(this.exhibitViewOpener_, 'is-visible');
                classlist.remove(this.exhibitViewCloser_, 'is-visible');
                classlist.remove(this.exhibitFrameOpener_, 'is-visible');
                classlist.remove(this.exhibitInteractiveHint_, 'is-visible');
                break;
            case LayerType.EXHIBIT:
                // hides navigation elements on the right viewport side (under the exhibit layer)
                if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
                classlist.remove(this.exhibitInfoOpener_, 'is-visible');
                break;
            case LayerType.MEDIA:
                // hides the visitor layer which would be overlapping the current media layer
                if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
                this.layerProvider_.hide('visitor');
                this.visitor_.controlsAreEnabled = false;
                break;
            case LayerType.INSTALLATION:
                // disables the background sound when a the installation frame is opened
                this.soundProvider_.enableBackgroundSound = false;
                break;
        }
    }

    /**
     * Monitors whether a layer is closed and performs different actions depending on the layer.
     * @param {LayerEvent} event
     * @private
     */
    handleLayerHide_(event)
    {
        switch(event.layer.type)
        {
            case LayerType.VISITOR:
                // depending of the current view make the orbit view close, the orbit view opener or the external installation button visible
                if(this.worldProvider_.activeExhibit != null)
                {
                    let open = this.worldProvider_.isExhibitView === true;
                    let elementModel = this.exhibitionProvider_.items[this.worldProvider_.activeExhibit.id].getElement(this.worldProvider_.activeExhibit.elementId);
                    if(elementModel)
                    {
                        if(elementModel.orbitView)
                            classlist.add(open ? this.exhibitViewCloser_ : this.exhibitViewOpener_, 'is-visible');
                        else if(elementModel.externUrl)
                            classlist.add(this.exhibitFrameOpener_, 'is-visible');
                    }
                }
                break;
            case LayerType.EXHIBIT:
                // when no installation frame is open and the hit box for the artwork info wasn't shown yet display it
                if(!this.worldProvider_.isExhibitView)
                {
                    if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
                        this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
                }
                // show the exhibit info button
                if(this.worldProvider_.activeExhibit)
                    classlist.add(this.exhibitInfoOpener_, 'is-visible');
                break;
            case LayerType.MEDIA:
                // activate the visitor move interaction if the view is not the top view or the exhibit orbit view and the reshuffeling is over
                if(!this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.isFirstActivation_)
                    this.visitor_.controlsAreEnabled = true;
                break;
            case LayerType.INSTALLATION:
                // activate the visitor move interaction if the view is not the top view or the exhibit orbit view and the reshuffeling is over
                if(!this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.isFirstActivation_)
                    this.visitor_.controlsAreEnabled = true;
                this.autoResetProvider_.start();
                // restarts the rendering of the scene
                this.startRenderer_();
                break;
        }

        // enables the background sound when the visitor, media, exhibit and installation layer are closed
        if (!this.layerProvider_.isOpen('visitor') && !this.layerProvider_.isOpen('exhibit') &&
            !this.layerProvider_.isOpen('media') && !this.layerProvider_.isOpen('installation')) {
            this.soundProvider_.enableBackgroundSound = true;
        }
    }

    /**
     * Changes the camera to the passed one and updates the matrix to the viewport/render size.
     * @param {THREE.Camera} camera
     * @private
     */
    changeCamera_(camera)
    {
        this.camera_ = camera;
        this.updateCameraMatrix_();
    }

    /**
     * Activates the attraction of the exhibits, so that the physical calculation of Cannon affects their mass and thus the position.
     * @param {boolean} enable
     * @private
     */
    enableGravity_(enable)
    {
        if(!enable)
        {
            this.exhibits_.forEach((exhibit) => {
                exhibit.attraction = 0;
            });
        }
    }

    /**
     * Monitors the Cannon sensors and their interaction start. Checks if the visitor interacts with exhibit elements.
     * If it is an activity zone, it is activated and triggers further events (rearrangement of other artworks,
     * user profile update, display of exhibit view and info buttons).
     * @param {Object} event
     * @private
     */
    handleCollisionStart_(event)
    {
        if((!this.visitor_.controlsAreEnabled && this.allMassBodiesAreScalledUp_) || this.worldProvider_.isExhibitView)
            return;

        let name = this.exhibitionProvider_.physicBodyReferences.get(event['body']['id']);
        let activeExhibit = null;
        if(name)
        {
            let nameSplitObj = name.split('::');
            if(nameSplitObj[0] == 'mass')
            {
                let exhibitId = parseInt(nameSplitObj[1], 10);
                this.exhibits_[exhibitId].enteredByVisitor = true;
            }
            else if(nameSplitObj[0] == 'sensor')
            {
                let exhibitId = parseInt(nameSplitObj[1], 10);
                let elementId = parseInt(nameSplitObj[2], 10);
                if(this.collidedSensors_.indexOf(exhibitId+"::"+elementId) == -1)
                {
                    this.collidedSensors_.push(exhibitId + "::" + elementId);
                    //console.log("ACTIVATE exhibit:" + exhibitId + " element:" + elementId);
                    if (activeExhibit)
                    {
                        let distanceActiveExhibit = this.exhibits_[activeExhibit.id].getInteractionDistance(activeExhibit.elementId, this.visitor_.object3D.position);
                        let distanceCurrentExhibit = this.exhibits_[exhibitId].getInteractionDistance(elementId, this.visitor_.object3D.position);
                        if (distanceCurrentExhibit < distanceActiveExhibit)
                        {
                            activeExhibit = {id: exhibitId, elementId: elementId};
                        }
                    }
                    else
                    {
                        activeExhibit = {id: exhibitId, elementId: elementId};
                    }
                }
            }
        }

        if(activeExhibit)
        {
            if(this.activationCountdown_ > 0 || this.allMassBodiesAreScalledUp_ == false)
                this.virtualActiveSensor_ = {exhibitId: activeExhibit.id, elementId: activeExhibit.elementId};
            else
                this.activateSensor_(activeExhibit.id, activeExhibit.elementId);
        }
    }

    /**
     * Monitors the Cannon sensors and their interaction end. When the user leaves an activity zone,
     * further actions are triggered (end of movement of other artwork, exhibit view and info buttons are hidden).
     * @param {Object} event
     * @private
     */
    handleCollisionEnd_(event)
    {
        if((!this.visitor_.controlsAreEnabled && this.allMassBodiesAreScalledUp_) || this.worldProvider_.isExhibitView || !event['bodyA'] || !event['bodyB'])
            return;

        let nameA = this.exhibitionProvider_.physicBodyReferences.get(event['bodyA']['id']);
        let nameB = this.exhibitionProvider_.physicBodyReferences.get(event['bodyB']['id']);

        if(nameA && nameB && (nameA == 'visitor' || nameB == 'visitor'))
        {
            let name = nameA == 'visitor' ? nameB : nameA;
            let nameSplitObj = name.split('::');

            if(nameSplitObj[0] == 'mass')
            {
                let exhibitId = parseInt(nameSplitObj[1], 10);
                this.exhibits_[exhibitId].enteredByVisitor = false;
            }
            else if(nameSplitObj[0] == 'sensor')
            {
                let exhibitId = parseInt(nameSplitObj[1], 10);
                let elementId = parseInt(nameSplitObj[2], 10);
                this.collidedSensors_.splice(this.collidedSensors_.indexOf(exhibitId+"::"+elementId), 1);
                //console.log("DEACTIVATE exhibit:"+exhibitId+" element:"+elementId, this.collidedSensors_);
            }
        }

        if(this.collidedSensors_.length > 0)
        {
            let activeExhibit = null;
            for(let i=0; i<this.collidedSensors_.length; i++)
            {
                let collidedSensorObj = this.collidedSensors_[i].split('::');
                let exhibitId = parseInt(collidedSensorObj[0], 10);
                let elementId = parseInt(collidedSensorObj[1], 10);
                if (activeExhibit)
                {
                    let distanceActiveExhibit = this.exhibits_[activeExhibit.id].getInteractionDistance(activeExhibit.elementId, this.visitor_.object3D.position);
                    let distanceCurrentExhibit = this.exhibits_[exhibitId].getInteractionDistance(elementId, this.visitor_.object3D.position);
                    if (distanceCurrentExhibit < distanceActiveExhibit)
                        activeExhibit = {id: exhibitId, elementId: elementId};
                }
                else
                    activeExhibit = {id: exhibitId, elementId: elementId};
            }
            if(this.activationCountdown_ > 0 || this.allMassBodiesAreScalledUp_ == false)
                this.virtualActiveSensor_ = {exhibitId: activeExhibit.id, elementId: activeExhibit.elementId};
            else
                this.activateSensor_(activeExhibit.id, activeExhibit.elementId);
        }
        else
        {
            this.virtualActiveSensor_ = null;
            this.deactivateSensor_();
        }
    }

    /**
     * Entering an activity zone is registered.
     * @param {number} exhibitId
     * @param {number} elementId
     * @private
     */
    activateSensor_(exhibitId, elementId)
    {
        if(!this.worldProvider_.activeExhibit || (this.worldProvider_.activeExhibit.id != exhibitId && this.worldProvider_.activeExhibit.elementId != elementId))
        {
            let elementModel = this.exhibitionProvider_.items[exhibitId].getElement(elementId);

            if(elementModel.orbitView)
                classlist.add(this.exhibitViewOpener_, 'is-visible');
            else if(elementModel.externUrl)
                classlist.add(this.exhibitFrameOpener_, 'is-visible');

            classlist.enable(this.exhibitInteractiveHint_, 'is-second-info', elementModel.orbitView != null || elementModel.externUrl != '');

            this.exhibits_[exhibitId].activatedByVisitor = true;

            // updates the visitor preferences
            this.exhibitionProvider_.addPreference(exhibitId, elementId);
            this.worldProvider_.activateExhibit(exhibitId, elementId);
            classlist.add(document.documentElement, 'activated-exhibit');
            classlist.add(this.exhibitInfoOpener_, 'is-visible');
            this.layerProvider_.hide('visitor');

            setTimeout(() => {
                this.showNavigationHints_().then(() => {
                    if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
                        this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
                    else if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_SINGLE_VIEW) && elementModel.orbitView)
                        this.hintBox_.show(HintBoxContentType.ARTWORK_SINGLE_VIEW).catch((error) => {console.warn(error);});
                }, (error) => {console.warn(error);});
            }, 500);

            if(this.activationCountdown_ == 0 && this.allMassBodiesAreScalledUp_ == true)
                this.activateGravity_();
        }
    }

    /**
     * Activates the attraction force when the user is in an exhibit activation zone. The other exhibits in the vicinity
     * of the activated exhibit are rearranged according to their attraction force based on the tags.
     * @private
     */
    activateGravity_()
    {
        if(this.isFirstActivation_) {
            this.isFirstActivation_ = false;
            this.checkExhibits_ = true;
        }

        if(this.worldProvider_.activeExhibit) {
            this.exhibits_[this.worldProvider_.activeExhibit.id].setImmovable(true);
        }
        this.enableGravity_(true);

        dataset.set(this.activationHint_, 'state', 'profile');
        classlist.enable(this.activationHint_, 'is-visible', true);
    }

    /**
     * Leaving an activity zone is registered.
     * @private
     */
    deactivateSensor_()
    {
        if(this.worldProvider_.activeExhibit != null)
        {
            classlist.remove(this.exhibitViewOpener_, 'is-visible');
            classlist.remove(this.exhibitFrameOpener_, 'is-visible');
            classlist.remove(this.exhibitInteractiveHint_, 'is-visible');

            this.exhibitionProvider_.visitor.completeCurrentPreference();
            this.exhibits_[this.worldProvider_.activeExhibit.id].activatedByVisitor = false;

            if(this.activationCountdown_ == 0 && this.allMassBodiesAreScalledUp_ == true)
            {
                this.enableGravity_(false);
                this.exhibits_[this.worldProvider_.activeExhibit.id].setImmovable(false);
                classlist.remove(this.activationHint_, 'is-visible');
            }

            this.worldProvider_.deactivateExhibit();
            classlist.remove(document.documentElement, 'activated-exhibit');
            classlist.remove(this.exhibitInfoOpener_, 'is-visible');
            this.layerProvider_.hide('exhibit');
        }
    }

    /**
     * Monitor the step before the new powers are applied to physical objects. Influences the forces that occur.
     * @private
     */
    handlePhysicPreStep_()
    {
        let allMassBodiesAreScalledUp = this.activationCountdown_ == 0;
        this.exhibits_.forEach((exhibit) => {
            if (exhibit.attraction != 0 && exhibit.physicMassBodyScale == 1)
            {
                if(!exhibit.activatedByVisitor)
                {
                    let forceDirection = new THREE.Vector3();
                    forceDirection.subVectors(this.visitor_.object3D.position, exhibit.object3D.position).normalize();
                    let force = new CANNON.Vec3(
                        forceDirection.x * this.worldProvider_.settings.physic['attractionSpeed'],
                        0,
                        forceDirection.z * this.worldProvider_.settings.physic['attractionSpeed']
                    );

                    if(exhibit.physicMassBody)
                        exhibit.physicMassBody.force.set(force.x, force.y, force.z);
                }
            }
            const condition = this.activationCountdown_ == 0;
            if(condition && exhibit.physicMassBodyScale < 1)
            {
                exhibit.updateMassBodyScale(Math.min(exhibit.physicMassBodyScale + this.worldProvider_.settings.physic['scaleSpeed'], 1));
                allMassBodiesAreScalledUp = false;
            }
        });
        const activationTime = 5;

        // Runs once when all exhibits have been fully scaled up. Checks if a saved virtual sensor should be activated now or it starts displaying the navigation hints.
        if(this.allMassBodiesAreScalledUp_ == false && allMassBodiesAreScalledUp == true)
        {
            setTimeout(() => {
                let timeline = gsap.timeline({'onComplete': () => {
                    this.enableDebugRendering_ = window['PHYSIC_MODE'] === 'on';
                    if(this.enableDebugRendering_ === false)
                    {
                        /** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
                            mesh.parent.remove(mesh);
                        });
                    }
                }});
                /** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
                    timeline.add(gsap.to(mesh.material, {'opacity': window['PHYSIC_MODE'] === 'on' ? 1 : 0, 'duration': 1, 'ease': 'power2.out'}), 0);
                });
            }, activationTime * 1000);

            this.visitor_.controlsAreEnabled = true;
            classlist.remove(this.activationHint_, 'is-visible');
            this.hintBox_.hide();

            if(this.virtualActiveSensor_)
            {
                this.activateSensor_(this.virtualActiveSensor_.exhibitId, this.virtualActiveSensor_.elementId);
                this.activateGravity_();
                this.virtualActiveSensor_ = null;
            } else {
                setTimeout(() => { this.showNavigationHints_().catch((error) => {console.warn(error);}); }, 500);
            }

            // Activates the frame check (only for mobile devices) for the performance monitoring.
            if(this.worldProvider_.isMobileDevice)
                setTimeout(() => {this.checkFrames_ = true;}, activationTime * 1000);
        }
        this.allMassBodiesAreScalledUp_ = allMassBodiesAreScalledUp;
    }

    /**
     * Main call for 3D scene updates in the render process. Updates all relevant 3D and Cannon Objects influenced
     * by the visitor movement, the artwork activation and the physical attraction.
     * @inheritDoc
     */
    updateScene_(delta)
    {
        super.updateScene_(delta);

        this.moveController_.checkGamepad(null);

        // updating the visitor by the interaction detected in the move controller
        let velocityMoveSide = this.moveController_.getAcceleration('x') * delta;
        let velocityMoveStraight = this.moveController_.getAcceleration('z') * delta;

        if(this.visitor_.controlsAreEnabled && this.moveController_.isMoving)
            this.autoResetProvider_.start();

        if (this.visitor_.controlsAreEnabled && !this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.layerProvider_.isOpen('guiding'))
        {
            // visitor movement
            const isTouchZooming = this.moveController_.isZoomingIn == MoveType.TOUCH_ZOOM_IN || this.moveController_.isZoomingOut == MoveType.TOUCH_ZOOM_OUT;
            const couldMovePerTouchLeft = false;
            const couldMovePerTouchRight = false;
            const couldMoveLeft = this.moveController_.isMovingLeft && (this.moveController_.isMovingLeft != MoveType.TOUCH_LEFT || couldMovePerTouchLeft);
            const couldMoveRight = this.moveController_.isMovingRight && (this.moveController_.isMovingRight != MoveType.TOUCH_RIGHT || couldMovePerTouchRight);
            if (couldMoveLeft && !isTouchZooming && this.moveController_.isMovingLeft != MoveType.MOUSE_LEFT && this.moveController_.isMovingLeft != MoveType.PAD_LEFT)
                this.moveController_.velocity.x += velocityMoveSide;
            if (couldMoveRight && !isTouchZooming && this.moveController_.isMovingRight != MoveType.MOUSE_RIGHT && this.moveController_.isMovingRight != MoveType.PAD_RIGHT)
                this.moveController_.velocity.x -= velocityMoveSide;
            if (this.moveController_.isMovingUp && !isTouchZooming && this.moveController_.isMovingUp != MoveType.MOUSE_UP && this.moveController_.isMovingUp != MoveType.PAD_UP)
                this.moveController_.velocity.z += velocityMoveStraight;
            if (this.moveController_.isMovingDown && !isTouchZooming && this.moveController_.isMovingDown != MoveType.MOUSE_DOWN && this.moveController_.isMovingDown != MoveType.PAD_DOWN)
                this.moveController_.velocity.z -= velocityMoveStraight;

            // visitor rotation
            let rh = 0;
            if(window['GAMEPAD_MODE'] === 'on' && this.moveController_.gamepadIsEnabled)
            {
                let horizontalAngle = this.moveController_.gamepadAxes.get(MoveType.PAD_RIGHT_HORIZONTAL);
                horizontalAngle = Math.abs(Math.round(horizontalAngle * 10)) > 1 ? horizontalAngle : 0;
                let verticalAngle = this.moveController_.gamepadAxes.get(MoveType.PAD_RIGHT_VERTICAL);
                verticalAngle = Math.abs(Math.round(verticalAngle * 10)) > 1 ? verticalAngle : 0;
                this.moveController_.velocity['rh'] += -horizontalAngle * this.moveController_.getAcceleration('rh') / 1000 * delta;
                this.moveController_.velocity['rv'] += -verticalAngle * this.moveController_.getAcceleration('rv') / 1000 * delta;
                rh = this.visitor_.object3D.rotation.y + this.moveController_.velocity['rh'];
            }
            else
            {
                if(this.moveController_.interactionMovePoints)
                {
                    if(this.moveController_.isMouseMoved)
                    {
                        this.moveController_.velocity['rh'] = -this.moveController_.interactionMoveDistances[0].x * this.moveController_.getAcceleration('rh') / 1000 * delta;
                        this.moveController_.velocity['rv'] = -this.moveController_.interactionMoveDistances[0].y * this.moveController_.getAcceleration('rv') / 1000 * delta;
                    }
                    else
                    {
                        if(!isTouchZooming){
                            if(this.moveController_.isMovingLeft == MoveType.TOUCH_LEFT)
                                this.moveController_.velocity['rh'] = Math.min(7, this.moveController_.touchStartMoveDistance.x / 10) * this.moveController_.getAcceleration('rh') / 1000 * delta;
                            else if(this.moveController_.isMovingRight == MoveType.TOUCH_RIGHT)
                                this.moveController_.velocity['rh'] = -Math.min(7, this.moveController_.touchStartMoveDistance.x / 10) * this.moveController_.getAcceleration('rh') / 1000 * delta;
                            else
                                this.moveController_.velocity['rh'] = -this.moveController_.interactionMoveDistances[0].x * this.moveController_.getAcceleration('rh') / 1000 * delta;
                        } else {
                            this.moveController_.velocity['rv'] = this.moveController_.interactionZoomDistance * this.moveController_.getAcceleration('rv') / 1000 * delta;
                        }
                    }
                    this.moveController_.interactionMoveDistances[0] = new Coordinate(0, 0);
                }
                rh = this.visitor_.object3D.rotation.y + this.moveController_.velocity['rh'];
            }

            const max = this.visitor_.cameraPolarAngleRange.start > this.visitor_.cameraPolarAngleRange.end ? this.visitor_.cameraPolarAngleRange.end : this.visitor_.cameraPolarAngleRange.start;
            const min = this.visitor_.cameraPolarAngleRange.start > this.visitor_.cameraPolarAngleRange.end ? this.visitor_.cameraPolarAngleRange.start : this.visitor_.cameraPolarAngleRange.end;
            this.visitor_.camera.rotation.x = Math.max(max, Math.min(min, this.visitor_.camera.rotation.x + this.moveController_.velocity['rv']));

            this.visitor_.object3D.rotation.y = rh;
        }

        // depending camera x and z position on current camera x rotation
        if(this.visitor_.controlsAreEnabled && !this.worldProvider_.isExhibitView && !this.layerProvider_.isOpen('guiding'))
        {
            this.visitor_.camera.position.y = ((this.visitor_.camera.rotation.x - this.visitor_.cameraPolarAngleRange.start) / (this.visitor_.cameraPolarAngleRange.end - this.visitor_.cameraPolarAngleRange.start)) * (this.visitor_.cameraRangeY.end - this.visitor_.cameraRangeY.start) + this.visitor_.cameraRangeY.start;
            this.visitor_.camera.position.z = parseFloat(Math.min(0, this.visitor_.camera.rotation.x) * -10);
        }

        let moveDirectionFrontal = new THREE.Vector3();
        this.visitor_.camera.getWorldDirection(moveDirectionFrontal);
        let moveDirectionSide = moveDirectionFrontal.clone().applyAxisAngle(new THREE.Vector3(0,1,0), 90 * Math.PI / 180);
        moveDirectionFrontal.multiplyScalar(this.moveController_.velocity.z * delta);
        moveDirectionSide.multiplyScalar(this.moveController_.velocity.x * delta);
        let moveDirection = moveDirectionFrontal.clone().add(moveDirectionSide);

        // checks collision via raycaster
        let collidedInNextStep = false;
        let height = 1;
        let visitorPosition = new THREE.Vector3(
            this.visitor_.physicBody.position.x + Math.round(moveDirection.x * 100) / 100,
            height,
            this.visitor_.physicBody.position.z + Math.round(moveDirection.z * 100) / 100
        );
        let nextVisitorPosition = new THREE.Vector3(
            visitorPosition.x + Math.round(moveDirection.x * 100) / 100,
            height,
            visitorPosition.z + Math.round(moveDirection.z * 100) / 100
        );
        let exhibitColliders = [];
        let minDistanceVisitorToExhibit = this.visitor_.maxDistanceToExhibit;
        this.exhibits_.forEach((exhibit) => {
            minDistanceVisitorToExhibit = Math.min(minDistanceVisitorToExhibit, nextVisitorPosition.distanceTo(exhibit.object3D.position));

            if(this.allMassBodiesAreScalledUp_)
            {
                if(exhibit.isActive) {
                    if(exhibit.shouldTestColliding([visitorPosition, nextVisitorPosition]))
                        exhibitColliders = exhibitColliders.concat(exhibit.collisionObjects);
                } else {
                    exhibit.enableInteraction();
                }
            }
        });

        // checks if new pos is outside the visitor max distance to any exhibit
        if(minDistanceVisitorToExhibit >= this.visitor_.maxDistanceToExhibit)
        {
            collidedInNextStep = true;
            nextVisitorPosition.x = this.visitor_.physicBody.position.x;
            nextVisitorPosition.z = this.visitor_.physicBody.position.z;
        }

        if(!(nextVisitorPosition.x == this.visitor_.physicBody.position.x && nextVisitorPosition.z == this.visitor_.physicBody.position.z))
        {
            let direction = new THREE.Vector3();
            let distance = visitorPosition.distanceTo(nextVisitorPosition);
            direction.subVectors(nextVisitorPosition, visitorPosition).normalize();
            this.raycaster_.set(visitorPosition.clone(), direction.clone());
            this.raycaster_.far = distance + this.visitor_.radius;
            let objects = this.allMassBodiesAreScalledUp_ ? exhibitColliders : this.allExhibitColliders_;
            if(objects.length > 0)
            {
                let intersects = this.raycaster_.intersectObjects(objects);
                collidedInNextStep = intersects.length > 0;
            }
        }

        if(!collidedInNextStep)
        {
            this.visitor_.physicBody.position.x += moveDirection.x;
            this.visitor_.physicBody.position.z += moveDirection.z;
        }
        else
        {
            this.moveController_.velocity.x = 0;
            this.moveController_.velocity.z = 0;
        }

        // apply visitor friction (smooth movement)
        let frictionalImpulse = -this.moveController_.velocity.x * this.moveController_.getFriction('x');
        this.moveController_.velocity.x += frictionalImpulse * delta;
        frictionalImpulse = -this.moveController_.velocity.z * this.moveController_.getFriction('z');
        this.moveController_.velocity.z += frictionalImpulse * delta;

        if(!this.moveController_.interactionMovePoints)
        {
            frictionalImpulse = -this.moveController_.velocity['rh'] * this.moveController_.getFriction('rh');
            this.moveController_.velocity['rh'] += frictionalImpulse * delta;
            frictionalImpulse = -this.moveController_.velocity['rv'] * this.moveController_.getFriction('rv');
            this.moveController_.velocity['rv'] += frictionalImpulse * delta;
        }

        // update the physical world
        let physicWorldDelta = Math.min(delta, 0.1);
        this.visitor_.physicBody.position.y = 0;
        if(this.worldProvider_.physicIsEnabled)
            this.physicWorld_.step(this.worldProvider_.settings.physic['fixedTimeStep'], physicWorldDelta, this.worldProvider_.settings.physic['maxSubSteps']);

        // checks once if in the exhibits certain 3D elements can be removed
        if(this.checkExhibits_)
        {
            this.checkExhibits_ = false;
            this.exhibits_.forEach((exhibit) => {
                exhibit.checkObjectsAfterFirstInteraction();
            });
        }

        // update physical instance of the visitor
        this.visitor_.physicBody.position.y = 0;

        // update user profil
        this.exhibitionProvider_.visitor.updateProfile(delta, /** @type {number} */ (this.worldProvider_.settings.profile['changeMultiplier']));
        let equalityList = [];

        // optical match of the optical THREE elements to the Cannon physical objects
        this.visitor_.object3D.position.x = this.visitor_.physicBody.position.x;
        this.visitor_.object3D.position.z = this.visitor_.physicBody.position.z;
        this.visitor_.update();
        this.exhibits_.forEach((exhibit) => {
            exhibit.update(this.clock_);
            // update attraction
            if(this.worldProvider_.activeExhibit != null)
            {
                equalityList.push({'id': exhibit.id,'value': this.exhibitionProvider_.visitor.caluculateEqualityFactor(exhibit.model.tags)});
            }
        });

        // apply attraction
        if(equalityList.length > 0)
        {
            equalityList.sort((a, b) => {return b['value'] - a['value']});
            for (let i = 0; i < equalityList.length; i++){
                let attraction = Math.pow(0.5, (1 / this.worldProvider_.settings.physic['attractionSpeed']) * i);
                this.exhibits_[equalityList[i]['id']].attraction = attraction;
            }
        }

        // update orbit controls if the exhibit orbit view is active
        if(this.orbitControls_)
        {
            this.orbitControls_.update();
            let newAzimuthalAngle = this.orbitControls_.getAzimuthalAngle();
            let newPolarAngle = this.orbitControls_.getPolarAngle();
            let newZoom = this.orbitControls_.getZoomLevel();
            let userIsInactive = this.orbit_.azimuthalAngle == newAzimuthalAngle && this.orbit_.polarAngle == newPolarAngle && this.orbit_.zoom == newZoom;
            if(!userIsInactive)
                this.autoResetProvider_.start();
            this.orbit_.azimuthalAngle = newAzimuthalAngle;
            this.orbit_.polarAngle = newPolarAngle;
            this.orbit_.zoom = newZoom;
        }

        // update ground
        this.ground_.position.x = this.visitor_.object3D.position.x;
        this.ground_.position.z = this.visitor_.object3D.position.z;

        // update activation counter
        if(this.countdownIsRunning_)
        {
            this.activationCountdown_ = Math.max(this.activationCountdown_ - delta, 0);
            /** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = Math.round(this.activationCountdown_).toString();
            if(this.activationCountdown_ == 0)
            {
                setTimeout(() => {
                    /** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = '';
                }, 1000);
                let timeline = gsap.timeline();
                /** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
                    timeline.add(gsap.to(mesh.material, {'opacity': 1, 'duration': 1, 'ease': 'power2.out'}), 0);
                });
                this.exhibits_.forEach((exhibit) => {
                    exhibit.updateVisibilities();
                });
                this.enableDebugRendering_ = true;
                this.countdownIsRunning_ = false;
            }
        }

        // update Cannon physic debugger
        if(this.enableDebugRendering_)
            this.physicDebugger_.update();
        this.renderer_.render(this.scene_, this.camera_);
    }
}

exports = Exhibition;