<commit_before>'use strict';

// @ngInject
function GameboardController(CardshifterServerAPI, $scope, $timeout, $rootScope, $location, $modal) {
    var playerInfos = {
        user: {
            index: null,
            id: null,
            name: null,
            animations: {},
            properties: {},
            zones: {}
        },
        opponent: {
            index: null,
            id: null,
            name: null,
            animations: {},
            properties: {},
            zones: {}
        }
    };

    $scope.modName = currentUser.game.mod.toLowerCase().replace(' ', '-');

    $scope.cardZones = {}; // contains information about what card is where.
    $scope.actions = [];
    $scope.doingAction = false;
    $scope.playerInfos = playerInfos;
    $scope.targets = [];
    $scope.selected = [];
    $scope.currentAction;

    var commandMap = {
        "resetActions": resetActions,
        "useable": addUsableAction,
        "player": storePlayerInfo,
        "zone": setZone,
        "entityRemoved": removeEntity,
        "card": storeCard,
        "zoneChange": moveCard,
        "targets": setTargets,
        "update": updateProperties,
        "elimination": displayWinner
    };

    CardshifterServerAPI.setMessageListener(function(message) {
        commandMap[message.command](message);
        $scope.$apply();
    }, ["resetActions", "useable", "player", "zone", "card", "zoneChange", "targets", "update", "entityRemoved", "elimination"]);

    $scope.startAction = function(action) {
        if(!action.targetRequired) { // No targets? No confirmation. Do we understand each other?
            $scope.currentAction = action;
			var doAbility = new CardshifterServerAPI.messageTypes.UseAbilityMessage(currentUser.game.id,
																				$scope.currentAction.id,
																				$scope.currentAction.action);

			CardshifterServerAPI.sendMessage(doAbility);
			$scope.cancelAction();
            return;
        }

		// if a target is required, request targets
		var getTargets = new CardshifterServerAPI.messageTypes.RequestTargetsMessage(currentUser.game.id,
				action.id, action.action);
		CardshifterServerAPI.sendMessage(getTargets);

        $scope.currentAction = action;
        $scope.doingAction = true;
    };

    $scope.cancelAction = function() {
        $scope.doingAction = false;
        $scope.targets = [];
        for (var i = 0; i < $scope.selected.length; i++) {
            $scope.selected[i].selected = false;
        }
        $scope.selected = [];
    };

    $scope.performAction = function() {
		var action = $scope.currentAction;
		var selected = $scope.selected;
		var minTargets = $scope.targetsMessage.min;
		var maxTargets = $scope.targetsMessage.max;
		if (selected.length < minTargets || selected.length > maxTargets) {
			console.log("target(s) required: " + minTargets + " - " + maxTargets + " but chosen " + selected.length);
			return;
		}

        var doAbility = null;

		var selectedIDs = [];
		for(var i = 0, length = $scope.selected.length; i < length; i++) {
			selectedIDs.push($scope.selected[i].id);
		}

		var doAbility = new CardshifterServerAPI.messageTypes.UseAbilityMessage(currentUser.game.id,
																				$scope.currentAction.id,
																				$scope.currentAction.action,
																				selectedIDs);

        CardshifterServerAPI.sendMessage(doAbility);
        $scope.cancelAction();
    };

    $scope.selectEntity = function(entity) {
        if (!$scope.doingAction) {
            return;
        }
        var selected = $scope.selected;
        var index = selected.indexOf(entity);

        if(index === -1) {      // select
            selected.push(entity);
            entity.selected = true;
        } else {                // de-select
            selected.splice(index, 1);
            entity.selected = false;
        }

        // if action requires exactly one target, perform action when target is chosen
        if ($scope.targetsMessage.min === 1 && $scope.targetsMessage.max === 1) {
            $scope.performAction();
        }
    };

    /**
    * Resets all the available actions that the user has.
    */
    function resetActions() {
        $scope.actions = [];
    };

    /**
    * Adds another possible action to the possible actions
    * that this user can complete on their turn.
    *
    * @param action:UsableActionMessage -- The action to add
    *
    * This will only add the action to $scope.actions if there
    * is not another action with the same name in there.
    */
    function addUsableAction(action) {
        var actions = $scope.actions;

        if(findPlayer(action.id)) { // ID is not target
            action.isPlayer = true;
            var notDuplicate = true;

            for(var i = 0, length = actions.length; i < length; i++) {
                if(actions[i].action === action.action) { // not a duplicate
                    notDuplicate = false;
                    break;
                }
            }

            if(notDuplicate) {
                actions.push(action);
            }
        } else { // ID is target
            action.isPlayer = false;
            actions.push(action);
        }
    };

    /**
    * Stores the information in player into either
    * playerInfos.user if this user is being
    * described in the message, or playerInfos.opponent
    * if the opponent is being described in the message.
    *
    * @param player:PlayerMessage -- The player info to store
    *
    */
    function storePlayerInfo(player) {
        var playerInfo;

        if(player.index === currentUser.game.playerIndex) {
            playerInfo = playerInfos.user;
        } else {
            playerInfo = playerInfos.opponent;
        }

        playerInfo.index = player.index;
        playerInfo.id = player.id;
        playerInfo.name = player.name;
        playerInfo.properties = player.properties;
    };

    /**
    * Stores the information about a zone by the
    * zone's name in
    * playerInfos.<player>.zones.<zone_name>
    *
    * @param zone:ZoneMessage -- The zone to add
    *
    * This will skip all "Cards" messages.
    */
    function setZone(zone) {
        if(zone.name === "Cards") { // "Not currently used as it is too meta."
            return;
        }

        for(var player in playerInfos) {
            if(playerInfos.hasOwnProperty(player)) {
                if(playerInfos[player].id === zone.owner) {
                    var newEntities = {};
                    for(var i = 0, length = zone.entities.length; i < length; i++) {
                        var entityId = zone.entities[i];
                        newEntities[entityId] = {}; // setup each ID to be an key holding an object to store card info
                        $scope.cardZones[entityId] = zone.id;
                    }
                    zone.entities = newEntities;
					zone.length = function () {
						return Object.keys(this.entities).length;
					}

                    playerInfos[player].zones[zone.name] = zone;
                    break;
                }
            }
        }
    };

    /**
    * Stores a CardInfoMessage in the appropriate zone,
    * which is going to be in either the user's zones, or
    * the opponent's zones.
    *
    * @param card:CardInfoMessage -- The card to store.
    *
    * This function will not store the CardInfoMessage if the
    * zone is not ".known".
    */
    function storeCard(card) {
        var destinationZone = findZone(card.zone);
        card.animations = {};

        try {
            if(destinationZone.known) {
                destinationZone.entities[card.id] = card;
            }
        } catch(e) {
            /* Do nothing. The reason why an error
            * might occur probably has something to
            * do with how the server is sending messages.
            *
            * For Mythos, 10 cards are sent: two groups of
            * 5. Both groups are identical. However, the
            * first group will always fail.
            *
            * The reason for this is because Mythos will send
            * CardInfoMessages before it has sent the
            * ZoneMessages, which means that this function
            * won't know where to put the cards. However,
            * since the initial ZoneChangeMessages and
            * CardInfoMessages in Mythos don't matter, it is
            * safe to ignore this error.
            */
        }
    };

    /**
    * Moves a single card from one zone to another
    *
    * @param message:ZoneChangeMessage -- The zone change information
    *
    */
    function moveCard(message) {
        try {
            var src = findZone(message.sourceZone);
            var dest = findZone(message.destinationZone);

            var card = src.entities[message.entity];
            delete src.entities[message.entity];
            $scope.cardZones[message.entity] = message.destinationZone;
            dest.entities[message.entity] = card;
        } catch(e) {
            /*
            * See the try/catch in storeCard.
            */
        }
    };

    /**
    * Removes a card from the zone it is in
    *
    * @param message:EntityRemoveMessage -- Remove information
    */
	function removeEntity(message) {
		var entityId = message.entity;
        var zoneId = $scope.cardZones[entityId];
        var zone = findZone(zoneId);
        delete zone.entities[entityId];
        delete $scope.cardZones[entityId];
	};

    /**
    * Sets the $scope.targets to all the available
    * targets for the current action.
    *
    * @param targets:AvailableTargetsMessage -- The available targets
    *
    * The HTML, depending on this $scope.targets value, will turn
    * a card name into a link for the user to select.
    */
    function setTargets(targets) {
        $scope.targets = targets.targets;
		$scope.targetsMessage = targets;
    };

    /**
    * Updates properties based on the message received.
    *
    * @param toUpdate:UpdateMessage -- The information on what to update
    *
    */
    function updateProperties(toUpdate) {
        var entity = findEntity(toUpdate.id);
        if (!entity) {
            console.log('entity not found: ' + toUpdate.id);
            return;
        }
        var oldValue = entity.properties[toUpdate.key];
        entity.properties[toUpdate.key] = toUpdate.value;
        if (typeof toUpdate.value === 'number') {
            var diff = toUpdate.value - oldValue;
            var anim = entity.animations[toUpdate.key];
            var animObject = { diff: diff };
            if (anim) {
                anim.push(animObject);
            } else {
                entity.animations[toUpdate.key] = [ animObject ];
            }
        }
    };

    /**
    * Displays the winner to the user and then navigates back
    * to the lobby.
    *
    * @param elimination:PlayerEliminatedMessage -- The elimination information
    *
    */
    function displayWinner(elimination) {
        var id = elimination.id;
        var winner = elimination.winner;
        var results = "You ";

        if (findPlayer(id) !== playerInfos.user) {
            return; // avoid showing modal twice
        }

        if (winner) {
            results += "win";
        } else {
            results += "lose";
        }

        var modalInstance = $modal.open({
            animation: true,
            backdrop: 'static',
            template: require('../game_results/game_results.html'),
            controller: 'GameOverMessageController',
            size: 'sm',
            resolve: {
                message: function () {
                    return results;
                }
            }
        });

        modalInstance.result.then(function () {
            $location.path("/lobby");
        });

    };

    /**
    * Return the zone of the passed in ID.
    *
    * @param id:number -- The ID of the zone.
    * @return Object -- The zone
    *                -- null, if a zone with id doesn't exist
    */
    function findZone(id) {
        var zoneGroups = [playerInfos.user.zones, playerInfos.opponent.zones];

        for(var i = 0, length = zoneGroups.length; i < length; i++) {
            for(var zone in zoneGroups[i]) {
                if(zoneGroups[i].hasOwnProperty(zone)) {
                    if(zoneGroups[i][zone].id === id) {
                        return zoneGroups[i][zone];
                    }
                }
            }
        }
        return null;
    };
    /**
    * Finds and returns a player based on an ID
    *
    * @param id:number -- The ID of the player
    * @param Object -- playerInfos.user
    *               -- playerInfos.opponent
    *               -- null, if the ID does not belong to either player
    */
    function findPlayer(id) {
        if(id === playerInfos.user.id) {
            return playerInfos.user;
        } else if(id === playerInfos.opponent.id) {
            return playerInfos.opponent;
        }
        return null;
    };

    /**
    * Finds and returns an entity based on an ID
    *
    * @param id:number -- The ID of the entity
    * @param Object -- playerInfos.user or playerInfos.opponent
    *               -- a card object
    *               -- null, if no entity with that ID was found
    */
    function findEntity(id) {
        var player = findPlayer(id);
        if (player) {
            return player;
        } else {
            var zoneId = $scope.cardZones[id];
            var zone = findZone(zoneId);
            if (zone.entities[id]) {
                return zone.entities[id];
            }
        }
        return null;
    };
}

module.exports = GameboardController;<commit_msg>fix a problem in `moveCard` (ZoneChangeMessage handler) when source zone doesn't exist<commit_after>'use strict';

// @ngInject
function GameboardController(CardshifterServerAPI, $scope, $timeout, $rootScope, $location, $modal) {
    var playerInfos = {
        user: {
            index: null,
            id: null,
            name: null,
            animations: {},
            properties: {},
            zones: {}
        },
        opponent: {
            index: null,
            id: null,
            name: null,
            animations: {},
            properties: {},
            zones: {}
        }
    };

    $scope.modName = currentUser.game.mod.toLowerCase().replace(' ', '-');

    $scope.cardZones = {}; // contains information about what card is where.
    $scope.actions = [];
    $scope.doingAction = false;
    $scope.playerInfos = playerInfos;
    $scope.targets = [];
    $scope.selected = [];
    $scope.currentAction;

    var commandMap = {
        "resetActions": resetActions,
        "useable": addUsableAction,
        "player": storePlayerInfo,
        "zone": setZone,
        "entityRemoved": removeEntity,
        "card": storeCard,
        "zoneChange": moveCard,
        "targets": setTargets,
        "update": updateProperties,
        "elimination": displayWinner
    };

    CardshifterServerAPI.setMessageListener(function(message) {
        commandMap[message.command](message);
        $scope.$apply();
    }, ["resetActions", "useable", "player", "zone", "card", "zoneChange", "targets", "update", "entityRemoved", "elimination"]);

    $scope.startAction = function(action) {
        if(!action.targetRequired) { // No targets? No confirmation. Do we understand each other?
            $scope.currentAction = action;
			var doAbility = new CardshifterServerAPI.messageTypes.UseAbilityMessage(currentUser.game.id,
																				$scope.currentAction.id,
																				$scope.currentAction.action);

			CardshifterServerAPI.sendMessage(doAbility);
			$scope.cancelAction();
            return;
        }

		// if a target is required, request targets
		var getTargets = new CardshifterServerAPI.messageTypes.RequestTargetsMessage(currentUser.game.id,
				action.id, action.action);
		CardshifterServerAPI.sendMessage(getTargets);

        $scope.currentAction = action;
        $scope.doingAction = true;
    };

    $scope.cancelAction = function() {
        $scope.doingAction = false;
        $scope.targets = [];
        for (var i = 0; i < $scope.selected.length; i++) {
            $scope.selected[i].selected = false;
        }
        $scope.selected = [];
    };

    $scope.performAction = function() {
		var action = $scope.currentAction;
		var selected = $scope.selected;
		var minTargets = $scope.targetsMessage.min;
		var maxTargets = $scope.targetsMessage.max;
		if (selected.length < minTargets || selected.length > maxTargets) {
			console.log("target(s) required: " + minTargets + " - " + maxTargets + " but chosen " + selected.length);
			return;
		}

        var doAbility = null;

		var selectedIDs = [];
		for(var i = 0, length = $scope.selected.length; i < length; i++) {
			selectedIDs.push($scope.selected[i].id);
		}

		var doAbility = new CardshifterServerAPI.messageTypes.UseAbilityMessage(currentUser.game.id,
																				$scope.currentAction.id,
																				$scope.currentAction.action,
																				selectedIDs);

        CardshifterServerAPI.sendMessage(doAbility);
        $scope.cancelAction();
    };

    $scope.selectEntity = function(entity) {
        if (!$scope.doingAction) {
            return;
        }
        var selected = $scope.selected;
        var index = selected.indexOf(entity);

        if(index === -1) {      // select
            selected.push(entity);
            entity.selected = true;
        } else {                // de-select
            selected.splice(index, 1);
            entity.selected = false;
        }

        // if action requires exactly one target, perform action when target is chosen
        if ($scope.targetsMessage.min === 1 && $scope.targetsMessage.max === 1) {
            $scope.performAction();
        }
    };

    /**
    * Resets all the available actions that the user has.
    */
    function resetActions() {
        $scope.actions = [];
    };

    /**
    * Adds another possible action to the possible actions
    * that this user can complete on their turn.
    *
    * @param action:UsableActionMessage -- The action to add
    *
    * This will only add the action to $scope.actions if there
    * is not another action with the same name in there.
    */
    function addUsableAction(action) {
        var actions = $scope.actions;

        if(findPlayer(action.id)) { // ID is not target
            action.isPlayer = true;
            var notDuplicate = true;

            for(var i = 0, length = actions.length; i < length; i++) {
                if(actions[i].action === action.action) { // not a duplicate
                    notDuplicate = false;
                    break;
                }
            }

            if(notDuplicate) {
                actions.push(action);
            }
        } else { // ID is target
            action.isPlayer = false;
            actions.push(action);
        }
    };

    /**
    * Stores the information in player into either
    * playerInfos.user if this user is being
    * described in the message, or playerInfos.opponent
    * if the opponent is being described in the message.
    *
    * @param player:PlayerMessage -- The player info to store
    *
    */
    function storePlayerInfo(player) {
        var playerInfo;

        if(player.index === currentUser.game.playerIndex) {
            playerInfo = playerInfos.user;
        } else {
            playerInfo = playerInfos.opponent;
        }

        playerInfo.index = player.index;
        playerInfo.id = player.id;
        playerInfo.name = player.name;
        playerInfo.properties = player.properties;
    };

    /**
    * Stores the information about a zone by the
    * zone's name in
    * playerInfos.<player>.zones.<zone_name>
    *
    * @param zone:ZoneMessage -- The zone to add
    *
    * This will skip all "Cards" messages.
    */
    function setZone(zone) {
        if(zone.name === "Cards") { // "Not currently used as it is too meta."
            return;
        }

        for(var player in playerInfos) {
            if(playerInfos.hasOwnProperty(player)) {
                if(playerInfos[player].id === zone.owner) {
                    var newEntities = {};
                    for(var i = 0, length = zone.entities.length; i < length; i++) {
                        var entityId = zone.entities[i];
                        newEntities[entityId] = {}; // setup each ID to be an key holding an object to store card info
                        $scope.cardZones[entityId] = zone.id;
                    }
                    zone.entities = newEntities;
					zone.length = function () {
						return Object.keys(this.entities).length;
					}

                    playerInfos[player].zones[zone.name] = zone;
                    break;
                }
            }
        }
    };

    /**
    * Stores a CardInfoMessage in the appropriate zone,
    * which is going to be in either the user's zones, or
    * the opponent's zones.
    *
    * @param card:CardInfoMessage -- The card to store.
    *
    * This function will not store the CardInfoMessage if the
    * zone is not ".known".
    */
    function storeCard(card) {
        var destinationZone = findZone(card.zone);
        card.animations = {};

        try {
            if(destinationZone.known) {
                destinationZone.entities[card.id] = card;
            }
        } catch(e) {
            /* Do nothing. The reason why an error
            * might occur probably has something to
            * do with how the server is sending messages.
            *
            * For Mythos, 10 cards are sent: two groups of
            * 5. Both groups are identical. However, the
            * first group will always fail.
            *
            * The reason for this is because Mythos will send
            * CardInfoMessages before it has sent the
            * ZoneMessages, which means that this function
            * won't know where to put the cards. However,
            * since the initial ZoneChangeMessages and
            * CardInfoMessages in Mythos don't matter, it is
            * safe to ignore this error.
            */
        }
    };

    /**
    * Moves a single card from one zone to another
    *
    * @param message:ZoneChangeMessage -- The zone change information
    *
    */
    function moveCard(message) {
        try {
            var src = findZone(message.sourceZone);
            var dest = findZone(message.destinationZone);
            var card = null;
            // when a card is suddenly summoned, sourceZone is -1, which doesn't exist
            if (src) {
                card = src.entities[message.entity];
                delete src.entities[message.entity];
            }
            $scope.cardZones[message.entity] = message.destinationZone;
            dest.entities[message.entity] = card;
        } catch(e) {
            /*
            * See the try/catch in storeCard.
            */
        }
    };

    /**
    * Removes a card from the zone it is in
    *
    * @param message:EntityRemoveMessage -- Remove information
    */
	function removeEntity(message) {
		var entityId = message.entity;
        var zoneId = $scope.cardZones[entityId];
        var zone = findZone(zoneId);
        delete zone.entities[entityId];
        delete $scope.cardZones[entityId];
	};

    /**
    * Sets the $scope.targets to all the available
    * targets for the current action.
    *
    * @param targets:AvailableTargetsMessage -- The available targets
    *
    * The HTML, depending on this $scope.targets value, will turn
    * a card name into a link for the user to select.
    */
    function setTargets(targets) {
        $scope.targets = targets.targets;
		$scope.targetsMessage = targets;
    };

    /**
    * Updates properties based on the message received.
    *
    * @param toUpdate:UpdateMessage -- The information on what to update
    *
    */
    function updateProperties(toUpdate) {
        var entity = findEntity(toUpdate.id);
        if (!entity) {
            console.log('entity not found: ' + toUpdate.id);
            return;
        }
        var oldValue = entity.properties[toUpdate.key];
        entity.properties[toUpdate.key] = toUpdate.value;
        if (typeof toUpdate.value === 'number') {
            var diff = toUpdate.value - oldValue;
            var anim = entity.animations[toUpdate.key];
            var animObject = { diff: diff };
            if (anim) {
                anim.push(animObject);
            } else {
                entity.animations[toUpdate.key] = [ animObject ];
            }
        }
    };

    /**
    * Displays the winner to the user and then navigates back
    * to the lobby.
    *
    * @param elimination:PlayerEliminatedMessage -- The elimination information
    *
    */
    function displayWinner(elimination) {
        var id = elimination.id;
        var winner = elimination.winner;
        var results = "You ";

        if (findPlayer(id) !== playerInfos.user) {
            return; // avoid showing modal twice
        }

        if (winner) {
            results += "win";
        } else {
            results += "lose";
        }

        var modalInstance = $modal.open({
            animation: true,
            backdrop: 'static',
            template: require('../game_results/game_results.html'),
            controller: 'GameOverMessageController',
            size: 'sm',
            resolve: {
                message: function () {
                    return results;
                }
            }
        });

        modalInstance.result.then(function () {
            $location.path("/lobby");
        });

    };

    /**
    * Return the zone of the passed in ID.
    *
    * @param id:number -- The ID of the zone.
    * @return Object -- The zone
    *                -- null, if a zone with id doesn't exist
    */
    function findZone(id) {
        var zoneGroups = [playerInfos.user.zones, playerInfos.opponent.zones];

        for(var i = 0, length = zoneGroups.length; i < length; i++) {
            for(var zone in zoneGroups[i]) {
                if(zoneGroups[i].hasOwnProperty(zone)) {
                    if(zoneGroups[i][zone].id === id) {
                        return zoneGroups[i][zone];
                    }
                }
            }
        }
        return null;
    };
    /**
    * Finds and returns a player based on an ID
    *
    * @param id:number -- The ID of the player
    * @param Object -- playerInfos.user
    *               -- playerInfos.opponent
    *               -- null, if the ID does not belong to either player
    */
    function findPlayer(id) {
        if(id === playerInfos.user.id) {
            return playerInfos.user;
        } else if(id === playerInfos.opponent.id) {
            return playerInfos.opponent;
        }
        return null;
    };

    /**
    * Finds and returns an entity based on an ID
    *
    * @param id:number -- The ID of the entity
    * @param Object -- playerInfos.user or playerInfos.opponent
    *               -- a card object
    *               -- null, if no entity with that ID was found
    */
    function findEntity(id) {
        var player = findPlayer(id);
        if (player) {
            return player;
        } else {
            var zoneId = $scope.cardZones[id];
            var zone = findZone(zoneId);
            if (zone.entities[id]) {
                return zone.entities[id];
            }
        }
        return null;
    };
}

module.exports = GameboardController;<|endoftext|>


<commit_before>define(['albumlist', 'browser', 'flipper', 'i18n', 'jquery', 'viewer'], function(AlbumList, Browser, Flipper, I18N, $, Viewer) {

var _ = I18N.G;

var App = {
	start: function() {
		var app = this;

		console.log('Starting application');

		app.albumList = new AlbumList();
		app.browser = new Browser(app);
		app.listMapFlipper = new Flipper('#browser-content-flipper');
		app.viewer = new Viewer(app);

		app.loadAlbums();

		app.setUiMode('album-list');

		I18N.setLocale(window.navigator.language);

		console.log('Application started');

		window.addEventListener('hashchange', function() {
			app.dispatchHash();
		});

		app.dispatchHash();
	},

	getUiMode: function() {
		var mode = null;

		$.each(document.body.className.split(' '), function(idx, val) {
			if (val.indexOf('ui-mode-') === 0) {
				mode = val.slice('ui-mode-'.length);
				return;
			}
		});

		return mode;
	},

	setUiMode: function(mode) {
		var $body = $(document.body);
		var modeName = 'ui-mode-' + mode;
		var currentMode = this.getUiMode();

		if (currentMode) {
			if (currentMode === mode) {
				return;
			}

			$body.removeClass('ui-mode-' + currentMode);
		}

		$body.addClass(modeName);
	},

	hashRoutes: {
		'browse': 'browseAlbum',
		'view': 'viewPicture'
	},

	dispatchHash: function() {
		var hash = document.location.hash;

		if (this.navigateSilent) {
			this.navigateSilent = false;
			return;
		}

		if (hash === '') {
			this.setUiMode('album-list');
			return;
		}

		hash = hash.slice(1); // remove the leading #

		var actionName;
		var actionParam;
		var actionOptions = {};

		var colIdx = hash.indexOf(':');

		if (colIdx == -1) {
			actionName = hash;
		} else {
			var tokens = hash.slice(0, colIdx).split(',');
			actionName = tokens.shift();

			$.each(tokens, function(idx, val) {
				actionOptions[val] = true;
			});

			actionParam = hash.slice(1 + colIdx);
		}

		for (x in this.hashRoutes) {
			if (x !== actionName) {
				continue;
			}

			var f = this[this.hashRoutes[x]];

			if (f === undefined) {
				console.log('Undefined route function: ' + this.hashRoutes[x]);
				return;
			}

			if (this.route) {
				this.previousRoute = this.route;
			}

			this.route = {
				action: actionName,
				param : actionParam,
				options: actionOptions
			};

			f.call(this, actionParam, actionOptions);
		}
	},

	navigate: function(route, silent) {
		this.navigateSilent = !!silent;

		document.location.hash = route;
	},

	loadAlbums: function() {
		var app = this;
		var albumList = $('#album-list');

		$.getJSON('/albums/', function(data) {
			data.sort(app.albumCompareFunc);
			app.albumList.update(data);
		});
	},

	albumCompareFunc: function(a, b) {
		return a.name.localeCompare(b.name);
	},

	sortPicturesByDate: function(pictures) {
		// For each picture, parse the EXIF date (if any)
		$.each(pictures, function(idx, pic) {
			if (!pic.metadata) {
				return;
			}

			var exifDate = pic.metadata['Exif.Photo.DateTimeOriginal'];

			if (!exifDate) {
				return;
			}

			// Format of the date is 2013:11:13 11:14:12

			if (exifDate.length != 19) {
				console.log('Cannot parse EXIF date ' + exifDate + ': invalid length');
				return;
			}

			var yearStr = exifDate.slice(0, 4);
			var monthStr = exifDate.slice(5, 7);
			var dayStr = exifDate.slice(8, 10);
			var hourStr = exifDate.slice(11, 13);
			var minuteStr = exifDate.slice(14, 16);
			var secondStr = exifDate.slice(17, 19);

			date = new Date(yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);

			if (isNaN(date.getTime())) {
				console.log('Cannot parse EXIF date ' + exifDate);
				return;
			}

			pic.date = date;
		});

		pictures.sort(function (a, b) {
			// If we have no date at all, fallback on filename lexical sort
			if (!a.date && !b.date) {
				return (a.path === b.path ? 0 : (a.path < b.path ? -1 : 1));
			}

			if (a.date && !b.date) {
				return -1;
			}

			if (!a.date && b.date) {
				return 1;
			}

			var ta = a.date.getTime();
			var tb = b.date.getTime();

			return (ta === tb ? 0 : (ta < tb ? -1 : 1));
		});
	},

	parseGpsMetadata: function(pictures) {
		var parseRational = function(data) {
			var idx = data.indexOf('/');

			if (idx === -1) {
				return null;
			}

			var a = Number(data.slice(0, idx));
			var b = Number(data.slice(1 + idx));

			if (isNaN(a) || isNaN(b)) {
				return null;
			}

			return a/b;
		};

		var parseCoord = function(data) {
			if (!data) {
				return null;
			}

			var tokens = data.split(' ');

			if (tokens.length !== 3) {
				return null;
			}

			var deg = parseRational(tokens[0]);
			var min = parseRational(tokens[1]);
			var sec = parseRational(tokens[2]); // we'll ignore this for now

			if (deg === null || min === null || sec === null) {
				return null;
			}

			return deg + min / 60 + sec / 3600;
		};

		var parse = function(data, keyName) {
			var coord = parseCoord(data[keyName]);

			if (coord === null) {
				return null;
			}

			var ref = data[keyName + 'Ref'];

			switch (ref) {
			case 'N':
			case 'E':
				return coord;
			case 'S':
			case 'W':
				return -coord;
			}

			return null;
		};

		$.each(pictures, function(idx, pic) {

			if (!pic.metadata) {
				return;
			}

			var lat = parse(pic.metadata, 'Exif.GPSInfo.GPSLatitude');
			var lon = parse(pic.metadata, 'Exif.GPSInfo.GPSLongitude');

			if (!lat || !lon) {
				return;
			}

			pic.gpsCoords = [lat, lon];
		});
	},

	loadAlbum: function(name, cb) {
		var app = this;

		// FIXME: The decoding of the hash token seems to behave differently
		// in Chrome and Firefox (Firefox does the decoding already). As
		// long as we don't have any % in the album name, doing an extra
		// decode is harmless.
		if (this.album && this.album.name === decodeURIComponent(name)) {
			cb(this.album);
			return;
		}

		$.getJSON('/albums/' + name, function(data) {
			app.sortPicturesByDate(data.pictures);
			app.parseGpsMetadata(data.pictures);

			app.album = data;

			cb(data);
		});
	},

	browseAlbum: function(album, options) {
		var app = this;
		var $content_flipper = $('#browser-content-flipper');

		app.setUiMode('browser');

		$content_flipper.toggleClass('browser-no-album', !album);

		app.browser.browse(null);

		if (album) {
			app.loadAlbum(album, function(data) {
				app.browser.browse(data);
			});

			var $toggleViewButton = $('#browser-map-button');

			if (options.map) {
				app.listMapFlipper.flip(true);
				$toggleViewButton.attr('value', _('List view'));
			} else {
				app.listMapFlipper.flip(false);
				$toggleViewButton.attr('value', _('Map view'));
			}
		} else {
			app.browser.browse(null);
		}
	},

	viewPicture: function(path) {
		var app = this;

		var wasInViewerMode = (this.getUiMode() === 'viewer');

		this.setUiMode('viewer');

		var idx = path.lastIndexOf('/');
		var album = path.slice(0, idx);
		var filename = path.slice(1+idx);

		app.loadAlbum(album, function(data) {
			app.viewer.view(data, filename);

			if (!wasInViewerMode) {
				$('#viewer-toolbar-inner').removeClass('viewer-toolbar-autohide');

				setTimeout(function() {
					$('#viewer-toolbar-inner').toggleClass('viewer-toolbar-autohide', true);
					app.hideViewerToolbarTimeout = undefined;
				}, 1000);
			}
		});
	},

	buildHash: function(route) {
		var hash = route.action;

		if (route.options) {
			for (o in route.options) {
				hash += ',';
				hash += o;
			}
		}

		hash += ':';
		hash += route.param;

		return hash;
	}
};

return App;

}); // define
<commit_msg>ui: Don't reset browser album all the time<commit_after>define(['albumlist', 'browser', 'flipper', 'i18n', 'jquery', 'viewer'], function(AlbumList, Browser, Flipper, I18N, $, Viewer) {

var _ = I18N.G;

var App = {
	start: function() {
		var app = this;

		console.log('Starting application');

		app.albumList = new AlbumList();
		app.browser = new Browser(app);
		app.listMapFlipper = new Flipper('#browser-content-flipper');
		app.viewer = new Viewer(app);

		app.loadAlbums();

		app.setUiMode('album-list');

		I18N.setLocale(window.navigator.language);

		console.log('Application started');

		window.addEventListener('hashchange', function() {
			app.dispatchHash();
		});

		app.dispatchHash();
	},

	getUiMode: function() {
		var mode = null;

		$.each(document.body.className.split(' '), function(idx, val) {
			if (val.indexOf('ui-mode-') === 0) {
				mode = val.slice('ui-mode-'.length);
				return;
			}
		});

		return mode;
	},

	setUiMode: function(mode) {
		var $body = $(document.body);
		var modeName = 'ui-mode-' + mode;
		var currentMode = this.getUiMode();

		if (currentMode) {
			if (currentMode === mode) {
				return;
			}

			$body.removeClass('ui-mode-' + currentMode);
		}

		$body.addClass(modeName);
	},

	hashRoutes: {
		'browse': 'browseAlbum',
		'view': 'viewPicture'
	},

	dispatchHash: function() {
		var hash = document.location.hash;

		if (this.navigateSilent) {
			this.navigateSilent = false;
			return;
		}

		if (hash === '') {
			this.setUiMode('album-list');
			return;
		}

		hash = hash.slice(1); // remove the leading #

		var actionName;
		var actionParam;
		var actionOptions = {};

		var colIdx = hash.indexOf(':');

		if (colIdx == -1) {
			actionName = hash;
		} else {
			var tokens = hash.slice(0, colIdx).split(',');
			actionName = tokens.shift();

			$.each(tokens, function(idx, val) {
				actionOptions[val] = true;
			});

			actionParam = hash.slice(1 + colIdx);
		}

		for (x in this.hashRoutes) {
			if (x !== actionName) {
				continue;
			}

			var f = this[this.hashRoutes[x]];

			if (f === undefined) {
				console.log('Undefined route function: ' + this.hashRoutes[x]);
				return;
			}

			if (this.route) {
				this.previousRoute = this.route;
			}

			this.route = {
				action: actionName,
				param : actionParam,
				options: actionOptions
			};

			f.call(this, actionParam, actionOptions);
		}
	},

	navigate: function(route, silent) {
		this.navigateSilent = !!silent;

		document.location.hash = route;
	},

	loadAlbums: function() {
		var app = this;
		var albumList = $('#album-list');

		$.getJSON('/albums/', function(data) {
			data.sort(app.albumCompareFunc);
			app.albumList.update(data);
		});
	},

	albumCompareFunc: function(a, b) {
		return a.name.localeCompare(b.name);
	},

	sortPicturesByDate: function(pictures) {
		// For each picture, parse the EXIF date (if any)
		$.each(pictures, function(idx, pic) {
			if (!pic.metadata) {
				return;
			}

			var exifDate = pic.metadata['Exif.Photo.DateTimeOriginal'];

			if (!exifDate) {
				return;
			}

			// Format of the date is 2013:11:13 11:14:12

			if (exifDate.length != 19) {
				console.log('Cannot parse EXIF date ' + exifDate + ': invalid length');
				return;
			}

			var yearStr = exifDate.slice(0, 4);
			var monthStr = exifDate.slice(5, 7);
			var dayStr = exifDate.slice(8, 10);
			var hourStr = exifDate.slice(11, 13);
			var minuteStr = exifDate.slice(14, 16);
			var secondStr = exifDate.slice(17, 19);

			date = new Date(yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);

			if (isNaN(date.getTime())) {
				console.log('Cannot parse EXIF date ' + exifDate);
				return;
			}

			pic.date = date;
		});

		pictures.sort(function (a, b) {
			// If we have no date at all, fallback on filename lexical sort
			if (!a.date && !b.date) {
				return (a.path === b.path ? 0 : (a.path < b.path ? -1 : 1));
			}

			if (a.date && !b.date) {
				return -1;
			}

			if (!a.date && b.date) {
				return 1;
			}

			var ta = a.date.getTime();
			var tb = b.date.getTime();

			return (ta === tb ? 0 : (ta < tb ? -1 : 1));
		});
	},

	parseGpsMetadata: function(pictures) {
		var parseRational = function(data) {
			var idx = data.indexOf('/');

			if (idx === -1) {
				return null;
			}

			var a = Number(data.slice(0, idx));
			var b = Number(data.slice(1 + idx));

			if (isNaN(a) || isNaN(b)) {
				return null;
			}

			return a/b;
		};

		var parseCoord = function(data) {
			if (!data) {
				return null;
			}

			var tokens = data.split(' ');

			if (tokens.length !== 3) {
				return null;
			}

			var deg = parseRational(tokens[0]);
			var min = parseRational(tokens[1]);
			var sec = parseRational(tokens[2]); // we'll ignore this for now

			if (deg === null || min === null || sec === null) {
				return null;
			}

			return deg + min / 60 + sec / 3600;
		};

		var parse = function(data, keyName) {
			var coord = parseCoord(data[keyName]);

			if (coord === null) {
				return null;
			}

			var ref = data[keyName + 'Ref'];

			switch (ref) {
			case 'N':
			case 'E':
				return coord;
			case 'S':
			case 'W':
				return -coord;
			}

			return null;
		};

		$.each(pictures, function(idx, pic) {

			if (!pic.metadata) {
				return;
			}

			var lat = parse(pic.metadata, 'Exif.GPSInfo.GPSLatitude');
			var lon = parse(pic.metadata, 'Exif.GPSInfo.GPSLongitude');

			if (!lat || !lon) {
				return;
			}

			pic.gpsCoords = [lat, lon];
		});
	},

	loadAlbum: function(name, cb) {
		var app = this;

		// FIXME: The decoding of the hash token seems to behave differently
		// in Chrome and Firefox (Firefox does the decoding already). As
		// long as we don't have any % in the album name, doing an extra
		// decode is harmless.
		if (this.album && this.album.name === decodeURIComponent(name)) {
			cb(this.album);
			return;
		}

		$.getJSON('/albums/' + name, function(data) {
			app.sortPicturesByDate(data.pictures);
			app.parseGpsMetadata(data.pictures);

			app.album = data;

			cb(data);
		});
	},

	browseAlbum: function(album, options) {
		var app = this;
		var $content_flipper = $('#browser-content-flipper');

		app.setUiMode('browser');

		$content_flipper.toggleClass('browser-no-album', !album);

		if (album && app.browser.album && app.browser.album.name !== album) {
			app.browser.browse(null);
		}

		if (album) {
			app.loadAlbum(album, function(data) {
				app.browser.browse(data);
			});

			var $toggleViewButton = $('#browser-map-button');

			if (options.map) {
				app.listMapFlipper.flip(true);
				$toggleViewButton.attr('value', _('List view'));
			} else {
				app.listMapFlipper.flip(false);
				$toggleViewButton.attr('value', _('Map view'));
			}
		} else {
			app.browser.browse(null);
		}
	},

	viewPicture: function(path) {
		var app = this;

		var wasInViewerMode = (this.getUiMode() === 'viewer');

		this.setUiMode('viewer');

		var idx = path.lastIndexOf('/');
		var album = path.slice(0, idx);
		var filename = path.slice(1+idx);

		app.loadAlbum(album, function(data) {
			app.viewer.view(data, filename);

			if (!wasInViewerMode) {
				$('#viewer-toolbar-inner').removeClass('viewer-toolbar-autohide');

				setTimeout(function() {
					$('#viewer-toolbar-inner').toggleClass('viewer-toolbar-autohide', true);
					app.hideViewerToolbarTimeout = undefined;
				}, 1000);
			}
		});
	},

	buildHash: function(route) {
		var hash = route.action;

		if (route.options) {
			for (o in route.options) {
				hash += ',';
				hash += o;
			}
		}

		hash += ':';
		hash += route.param;

		return hash;
	}
};

return App;

}); // define
<|endoftext|>


<commit_before>define([
  'gui/GuiTR',
  'editor/Sculpt'
], function (TR, Sculpt) {

  'use strict';

  var GuiSculptingTools = {};

  GuiSculptingTools.hide = function (toolKey) {
    for (var i = 0, ctrls = GuiSculptingTools[toolKey].ctrls_, nbCtrl = ctrls.length; i < nbCtrl; ++i)
      ctrls[i].setVisibility(false);
  };

  GuiSculptingTools.show = function (toolKey) {
    for (var i = 0, ctrls = GuiSculptingTools[toolKey].ctrls_, nbCtrl = ctrls.length; i < nbCtrl; ++i)
      ctrls[i].setVisibility(true);
  };

  var setOnChange = function (key, val, factor) {
    this[key] = factor ? val * factor : val;
  };

  // some helper functions
  var addCtrlIntensity = function (tool, fold) {
    return fold.addSlider(TR('sculptIntensity'), tool.intensity_ * 100, setOnChange.bind(tool, 'intensity_', 0.01), 0, 100, 1);
  };
  var addCtrlCulling = function (tool, fold) {
    return fold.addCheckbox(TR('sculptCulling'), tool, 'culling_');
  };
  var addCtrlNegative = function (tool, fold, self) {
    var ctrl = fold.addCheckbox(TR('sculptNegative'), tool, 'negative_');
    if (self) {
      self.toggleNegative = function () {
        ctrl.setValue(!ctrl.getValue());
      };
    }
    return ctrl;
  };

  GuiSculptingTools[Sculpt.tool.BRUSH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(fold.addCheckbox(TR('sculptClay'), tool, 'clay_'));
      this.ctrls_.push(fold.addCheckbox(TR('sculptAccumulate'), tool, 'accumulate_'));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.CREASE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.DRAG] = {
    ctrls_: [],
    init: function () {}
  };

  GuiSculptingTools[Sculpt.tool.FLATTEN] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.INFLATE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.PAINT] = {
    ctrls_: [],
    onMaterialChanged: function (main, tool, materials) {
      tool.color_ = materials[0].getValue();
      tool.roughness_ = materials[1].getValue();
      tool.metallic_ = materials[2].getValue();
      var mesh = main.getMesh();
      if (mesh) {
        mesh.setAlbedo(tool.color_);
        mesh.setRoughness(tool.roughness_);
        mesh.setMetallic(tool.metallic_);
        main.render();
      }
    },
    resetMaterialOverride: function (main) {
      var mesh = main.getMesh();
      if (mesh) {
        mesh.getAlbedo()[0] = -1.0;
        mesh.setRoughness(-0.18);
        mesh.setMetallic(-0.78);
        main.render();
      }
    },
    init: function (tool, fold, main) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlCulling(tool, fold));

      var materials = [];
      var ctrlColor = fold.addColor(TR('sculptColor'), tool.color_, this.onMaterialChanged.bind(this, main, tool, materials));
      var ctrlRoughness = fold.addSlider(TR('sculptRoughness'), tool.roughness_, this.onMaterialChanged.bind(this, main, tool, materials), 0.0, 1.0, 0.01);
      var ctrlMetallic = fold.addSlider(TR('sculptMetallic'), tool.metallic_, this.onMaterialChanged.bind(this, main, tool, materials), 0.0, 1.0, 0.01);
      materials.push(ctrlColor, ctrlRoughness, ctrlMetallic);
      window.addEventListener('mouseup', this.resetMaterialOverride.bind(this, main));

      tool.setPickCallback(function (color, roughness, metallic) {
        ctrlColor.setValue(color);
        ctrlRoughness.setValue(roughness);
        ctrlMetallic.setValue(metallic);
      });

      this.ctrls_.push(ctrlColor, ctrlRoughness, ctrlMetallic);
      this.ctrls_.push(fold.addCheckbox(TR('sculptPickColor'), tool, 'pickColor_'));
    }
  };

  GuiSculptingTools[Sculpt.tool.PINCH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.TWIST] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.SCALE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.SMOOTH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(fold.addCheckbox(TR('sculptTangentialSmoothing'), tool, 'tangent_'));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.TRANSLATE] = {
    ctrls_: [],
    init: function () {}
  };

  GuiSculptingTools[Sculpt.tool.ROTATE] = {
    ctrls_: [],
    init: function () {}
  };

  return GuiSculptingTools;
});<commit_msg>Fixes color picking event<commit_after>define([
  'gui/GuiTR',
  'editor/Sculpt'
], function (TR, Sculpt) {

  'use strict';

  var GuiSculptingTools = {};

  GuiSculptingTools.hide = function (toolKey) {
    for (var i = 0, ctrls = GuiSculptingTools[toolKey].ctrls_, nbCtrl = ctrls.length; i < nbCtrl; ++i)
      ctrls[i].setVisibility(false);
  };

  GuiSculptingTools.show = function (toolKey) {
    for (var i = 0, ctrls = GuiSculptingTools[toolKey].ctrls_, nbCtrl = ctrls.length; i < nbCtrl; ++i)
      ctrls[i].setVisibility(true);
  };

  var setOnChange = function (key, val, factor) {
    this[key] = factor ? val * factor : val;
  };

  // some helper functions
  var addCtrlIntensity = function (tool, fold) {
    return fold.addSlider(TR('sculptIntensity'), tool.intensity_ * 100, setOnChange.bind(tool, 'intensity_', 0.01), 0, 100, 1);
  };
  var addCtrlCulling = function (tool, fold) {
    return fold.addCheckbox(TR('sculptCulling'), tool, 'culling_');
  };
  var addCtrlNegative = function (tool, fold, self) {
    var ctrl = fold.addCheckbox(TR('sculptNegative'), tool, 'negative_');
    if (self) {
      self.toggleNegative = function () {
        ctrl.setValue(!ctrl.getValue());
      };
    }
    return ctrl;
  };

  GuiSculptingTools[Sculpt.tool.BRUSH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(fold.addCheckbox(TR('sculptClay'), tool, 'clay_'));
      this.ctrls_.push(fold.addCheckbox(TR('sculptAccumulate'), tool, 'accumulate_'));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.CREASE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.DRAG] = {
    ctrls_: [],
    init: function () {}
  };

  GuiSculptingTools[Sculpt.tool.FLATTEN] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.INFLATE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.PAINT] = {
    ctrls_: [],
    onMaterialChanged: function (main, tool, materials) {
      tool.color_ = materials[0].getValue();
      tool.roughness_ = materials[1].getValue();
      tool.metallic_ = materials[2].getValue();
      var mesh = main.getMesh();
      if (mesh) {
        mesh.setAlbedo(tool.color_);
        mesh.setRoughness(tool.roughness_);
        mesh.setMetallic(tool.metallic_);
        main.render();
      }
    },
    resetMaterialOverride: function (main) {
      var mesh = main.getMesh();
      if (mesh) {
        mesh.getAlbedo()[0] = -1.0;
        mesh.setRoughness(-0.18);
        mesh.setMetallic(-0.78);
        main.render();
      }
    },
    onPickedMaterial: function (materials, tool, color, roughness, metallic) {
      materials[0].setValue(color, true);
      materials[1].setValue(roughness, true);
      materials[2].setValue(metallic, true);
      tool.color_ = color;
      tool.roughness_ = roughness;
      tool.metallic_ = metallic;
    },
    init: function (tool, fold, main) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlCulling(tool, fold));

      var materials = [];
      var cbMatChanged = this.onMaterialChanged.bind(this, main, tool, materials);
      var ctrlColor = fold.addColor(TR('sculptColor'), tool.color_, cbMatChanged);
      var ctrlRoughness = fold.addSlider(TR('sculptRoughness'), tool.roughness_, cbMatChanged, 0.0, 1.0, 0.01);
      var ctrlMetallic = fold.addSlider(TR('sculptMetallic'), tool.metallic_, cbMatChanged, 0.0, 1.0, 0.01);
      materials.push(ctrlColor, ctrlRoughness, ctrlMetallic);
      window.addEventListener('mouseup', this.resetMaterialOverride.bind(this, main));

      tool.setPickCallback(this.onPickedMaterial.bind(this, materials, tool));

      this.ctrls_.push(ctrlColor, ctrlRoughness, ctrlMetallic);
      this.ctrls_.push(fold.addCheckbox(TR('sculptPickColor'), tool, 'pickColor_'));
    }
  };

  GuiSculptingTools[Sculpt.tool.PINCH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(addCtrlNegative(tool, fold, this));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.TWIST] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.SCALE] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.SMOOTH] = {
    ctrls_: [],
    init: function (tool, fold) {
      this.ctrls_.push(addCtrlIntensity(tool, fold));
      this.ctrls_.push(fold.addCheckbox(TR('sculptTangentialSmoothing'), tool, 'tangent_'));
      this.ctrls_.push(addCtrlCulling(tool, fold));
    }
  };

  GuiSculptingTools[Sculpt.tool.TRANSLATE] = {
    ctrls_: [],
    init: function () {}
  };

  GuiSculptingTools[Sculpt.tool.ROTATE] = {
    ctrls_: [],
    init: function () {}
  };

  return GuiSculptingTools;
});<|endoftext|>


<commit_before>var Transaction = require('./transaction')
var request = require('../lib/transport')
var logger = require('../lib/logger')

var Instrumentation = function () {
  this._queue = []
  this.scheduler = setInterval(this._dispatch.bind(this), 10000)
}

Instrumentation.prototype.add = function (transaction) {
  this._queue.push(transaction)
}

Instrumentation.prototype.startTransaction = function (name, type) {
  return new Transaction(this, name, type)
}

Instrumentation.prototype._flush = function () {
  this._queue = []
}

Instrumentation.prototype._dispatch = function() {

  logger.log('Instrumentation.scheduler._dispatch', this._queue.length)

  if(!this._queue.length) {
    return
  }

  var transactions = this._formatTransactions()
  var flush = this._flush.bind(this)

  request.sendTransaction(transactions).then(flush)
}

Instrumentation.prototype._formatTransactions = function () {
  var transactions = groupTransactions(this._queue)

  var traces = [].concat.apply([], this._queue.map(function (trans) {
    return trans.traces
  }))

  var groupedTraces = groupTraces(traces)
  var groupedTracesTimings = getRawGroupedTracesTimings(traces, groupedTraces);

  return {
    transactions: transactions,
    traces: {
      groups: groupedTraces,
      raw: groupedTracesTimings
    }
  }
}

function groupTransactions (transactions) {
  var groups = grouper(transactions, transactionGroupingKey)

  return Object.keys(groups).map(function (key) {
    var trans = groups[key][0]
    var durations = groups[key].map(function (trans) {
      return trans.duration()
    })
    return {
      transaction: trans.name,
      result: trans.result,
      kind: trans.type,
      timestamp: groupingTs(trans._startStamp).toISOString(),
      durations: durations
    }
  })
}

function getRawGroupedTracesTimings (traces, groupedTraces) {

  var getTraceGroupIndex = function (col, item) {
    var index = 0
    var targetGroup = traceGroupingKey(item)

    col.forEach(function (item, i) {
      if(item._group === targetGroup) {
        index = i
      }
    })

    return index
  }

  var groupedByTransaction = grouper(traces, function(trace) {
    return trace.transaction.name
  })

  return Object.keys(groupedByTransaction).map(function (key) {
    var traces = groupedByTransaction[key]
    var transaction = traces[0].transaction

    var data = [transaction.duration()]

    traces.forEach(function(trace) {
      var groupIndex = getTraceGroupIndex(groupedTraces, trace)
      data.push([groupIndex, trace._start - transaction._start, trace.duration()])
    })

    return data;
  });

}

function groupTraces (traces) {
  var groupedByMinute = grouper(traces, traceGroupingKey)

  return Object.keys(groupedByMinute).map(function (key) {
    var trace = groupedByMinute[key][0]

    var startTime = trace._start
    if(trace.transaction) {
      startTime = startTime - trace.transaction._start
    } else {
      startTime = 0;
    }

    return {
      transaction: trace.transaction.name,
      signature: trace.signature,
      kind: trace.type,
      timestamp: trace._startStamp.toISOString(),
      parents: trace.ancestors(),
      extra: {
        _frames: trace.frames
      },
      _group: key
    }
  }).sort(function(a, b) {
    return a.start_time - b.start_time
  })

}

function grouper (arr, func) {
  var groups = {}

  arr.forEach(function (obj) {
    var key = func(obj)
    if (key in groups){
      groups[key].push(obj)
    } else {
      groups[key] = [obj]
    }

    obj._traceGroup = key
  })

  return groups
}

function groupingTs (ts) {
  return new Date(ts.getFullYear(), ts.getMonth(), ts.getDate(), ts.getHours(), ts.getMinutes())
}

function transactionGroupingKey (trans) {
  return groupingTs(trans._startStamp).getTime() + '|' + trans.name + '|' + trans.result + '|' + trans.type
}

function traceGroupingKey (trace) {
  var ancestors = trace.ancestors().map(function (trace) {
    return trace.signature
  }).join(',')

  return groupingTs(trace._startStamp).getTime() + '|' + trace.transaction.name + '|' + ancestors + '|' + trace.signature + '|' + trace.type
}

module.exports = Instrumentation
<commit_msg>Use arrays to construct groups<commit_after>var Transaction = require('./transaction')
var request = require('../lib/transport')
var logger = require('../lib/logger')

var Instrumentation = function () {
  this._queue = []
  this.scheduler = setInterval(this._dispatch.bind(this), 10000)
}

Instrumentation.prototype.add = function (transaction) {
  this._queue.push(transaction)
}

Instrumentation.prototype.startTransaction = function (name, type) {
  return new Transaction(this, name, type)
}

Instrumentation.prototype._flush = function () {
  this._queue = []
}

Instrumentation.prototype._dispatch = function() {
  logger.log('Instrumentation.scheduler._dispatch', this._queue.length)

  if(!this._queue.length) {
    return
  }

  var transactions = this._formatTransactions()
  var flush = this._flush.bind(this)

  request.sendTransaction(transactions).then(flush)
}

Instrumentation.prototype._formatTransactions = function () {
  var transactions = groupTransactions(this._queue)

  var traces = [].concat.apply([], this._queue.map(function (trans) {
    return trans.traces
  }))

  var groupedTraces = groupTraces(traces)
  var groupedTracesTimings = getRawGroupedTracesTimings(traces, groupedTraces);

  return {
    transactions: transactions,
    traces: {
      groups: groupedTraces,
      raw: groupedTracesTimings
    }
  }
}

function groupTransactions (transactions) {
  var groups = grouper(transactions, transactionGroupingKey)

  return Object.keys(groups).map(function (key) {
    var trans = groups[key][0]
    var durations = groups[key].map(function (trans) {
      return trans.duration()
    })
    return {
      transaction: trans.name,
      result: trans.result,
      kind: trans.type,
      timestamp: groupingTs(trans._startStamp).toISOString(),
      durations: durations
    }
  })
}

function getRawGroupedTracesTimings (traces, groupedTraces) {

  var getTraceGroupIndex = function (col, item) {
    var index = 0
    var targetGroup = traceGroupingKey(item)

    col.forEach(function (item, i) {
      if(item._group === targetGroup) {
        index = i
      }
    })

    return index
  }

  var groupedByTransaction = grouper(traces, function(trace) {
    return trace.transaction.name
  })

  return Object.keys(groupedByTransaction).map(function (key) {
    var traces = groupedByTransaction[key]
    var transaction = traces[0].transaction

    var data = [transaction.duration()]

    traces.forEach(function(trace) {
      var groupIndex = getTraceGroupIndex(groupedTraces, trace)
      data.push([groupIndex, trace._start - transaction._start, trace.duration()])
    })

    return data;
  });

}

function groupTraces (traces) {
  var groupedByMinute = grouper(traces, traceGroupingKey)

  return Object.keys(groupedByMinute).map(function (key) {
    var trace = groupedByMinute[key][0]

    var startTime = trace._start
    if(trace.transaction) {
      startTime = startTime - trace.transaction._start
    } else {
      startTime = 0;
    }

    return {
      transaction: trace.transaction.name,
      signature: trace.signature,
      kind: trace.type,
      timestamp: trace._startStamp.toISOString(),
      parents: trace.ancestors(),
      extra: {
        _frames: trace.frames
      },
      _group: key
    }
  }).sort(function(a, b) {
    return a.start_time - b.start_time
  })

}

function grouper (arr, func) {
  var groups = {}

  arr.forEach(function (obj) {
    var key = func(obj)
    if (key in groups){
      groups[key].push(obj)
    } else {
      groups[key] = [obj]
    }

    obj._traceGroup = key
  })

  return groups
}

function groupingTs (ts) {
  return new Date(ts.getFullYear(), ts.getMonth(), ts.getDate(), ts.getHours(), ts.getMinutes())
}

function transactionGroupingKey (trans) {
  return [
    groupingTs(trans._startStamp).getTime(),
    trans.name,
    trans.result,
    trans.type
  ].join('-')
}

function traceGroupingKey (trace) {
  var ancestors = trace.ancestors().map(function (trace) {
    return trace.signature
  }).join(',')

  return [
    groupingTs(trace._startStamp).getTime(),
    trace.transaction.name,
    ancestors,
    trace.signature,
    trace.type
  ].join('-')
}

module.exports = Instrumentation
<|endoftext|>


<commit_before>/**
 * @fileoverview Resizer component used to watch an element and fire
 * an event when the object's width or height changes
 * @author lakenen
 */

/**
 * resizer component definition
 */
Crocodoc.addComponent('resizer', function (scope) {

    'use strict';

    var support = scope.getUtility('support');

    // shorter way of defining
    // 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange'
    var FULLSCREENCHANGE_EVENT = ['', ' webkit', ' moz', ' ']
        .join('fullscreenchange') +
        // @NOTE: IE 11 uses upper-camel-case for this, which is apparently necessary
        'MSFullscreenChange';

    var $window = $(window),
        $document = $(document),
        element,
        frameWidth = 0,
        currentClientWidth,
        currentClientHeight,
        currentOffsetWidth,
        currentOffsetHeight,
        resizeFrameID;

    /**
     * Fire the resize event with the proper data
     * @returns {void}
     * @private
     */
    function broadcast() {
        scope.broadcast('resize', {
            // shortcuts for offsetWidth/height
            width: currentOffsetWidth,
            height: currentOffsetHeight,
            // client width is width of the inner, visible area
            clientWidth: currentClientWidth,
            clientHeight: currentClientHeight,
            // offset width is the width of the element, including border,
            // padding, and scrollbars
            offsetWidth: currentOffsetWidth,
            offsetHeight: currentOffsetHeight
        });
    }

    /**
     * Check if the element has resized every animation frame
     * @returns {void}
     * @private
     */
    function loop() {
        support.cancelAnimationFrame(resizeFrameID);
        checkResize();
        resizeFrameID = support.requestAnimationFrame(loop, element);
    }

    /**
     * Check if the element has resized, and broadcast the resize event if so
     * @returns {void}
     * @private
     */
    function checkResize () {
        var newOffsetHeight = element.offsetHeight,
            newOffsetWidth = element.offsetWidth;

        // check if we're in a frame
        if (window.frameElement) {
            // firefox has an issue where styles aren't calculated in hidden iframes
            // if the iframe was hidden and is now visible, broadcast a
            // layoutchange event
            if (frameWidth === 0 && window.frameElement.offsetWidth !== 0) {
                frameWidth = window.frameElement.offsetWidth;
                scope.broadcast('layoutchange');
                return;
            }
        }

        //on touch devices, the offset height is sometimes zero as content is loaded
        if (newOffsetHeight) {
            if (newOffsetHeight !== currentOffsetHeight || newOffsetWidth !== currentOffsetWidth) {
                currentOffsetHeight = newOffsetHeight;
                currentOffsetWidth = newOffsetWidth;
                currentClientHeight = element.clientHeight;
                currentClientWidth = element.clientWidth;
                broadcast();
            }
        }
    }

    return {

        messages: [
            'layoutchange'
        ],

        /**
         * Handle framework messages
         * @returns {void}
         */
        onmessage: function () {
            // force trigger resize when layout changes
            // @NOTE: we do this because the clientWidth/Height
            // could be different based on the layout (and whether
            // or not the new layout changes scrollbars)
            currentOffsetHeight = null;
            checkResize();
        },

        /**
         * Initialize the Resizer component with an element to watch
         * @param  {HTMLElement} el The element to watch
         * @returns {void}
         */
        init: function (el) {
            element = $(el).get(0);

            // use the documentElement for viewport dimensions
            // if we are using the window as the viewport
            if (element === window) {
                element = document.documentElement;
                $window.on('resize', checkResize);
                // @NOTE: we don't need to loop with
                // requestAnimationFrame in this case,
                // because we can rely on window.resize
                // events if the window is our viewport
                checkResize();
            } else {
                loop();
            }
           $document.on(FULLSCREENCHANGE_EVENT, checkResize);
        },

        /**
         * Destroy the Resizer component
         * @returns {void}
         */
        destroy: function () {
            $document.off(FULLSCREENCHANGE_EVENT, checkResize);
            $window.off('resize', checkResize);
            support.cancelAnimationFrame(resizeFrameID);
        }
    };
});
<commit_msg>Fix #110 - check for hidden iframe throws for cross-domain frame<commit_after>/**
 * @fileoverview Resizer component used to watch an element and fire
 * an event when the object's width or height changes
 * @author lakenen
 */

/**
 * resizer component definition
 */
Crocodoc.addComponent('resizer', function (scope) {

    'use strict';

    var support = scope.getUtility('support');

    // shorter way of defining
    // 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange'
    var FULLSCREENCHANGE_EVENT = ['', ' webkit', ' moz', ' ']
        .join('fullscreenchange') +
        // @NOTE: IE 11 uses upper-camel-case for this, which is apparently necessary
        'MSFullscreenChange';

    var $window = $(window),
        $document = $(document),
        element,
        frameWidth = 0,
        currentClientWidth,
        currentClientHeight,
        currentOffsetWidth,
        currentOffsetHeight,
        resizeFrameID,
        inIframe = (function () {
            try {
                return window.self !== window.top;
            } catch (e) {
                return true;
            }
        })();

    /**
     * Fire the resize event with the proper data
     * @returns {void}
     * @private
     */
    function broadcast() {
        scope.broadcast('resize', {
            // shortcuts for offsetWidth/height
            width: currentOffsetWidth,
            height: currentOffsetHeight,
            // client width is width of the inner, visible area
            clientWidth: currentClientWidth,
            clientHeight: currentClientHeight,
            // offset width is the width of the element, including border,
            // padding, and scrollbars
            offsetWidth: currentOffsetWidth,
            offsetHeight: currentOffsetHeight
        });
    }

    /**
     * Check if the element has resized every animation frame
     * @returns {void}
     * @private
     */
    function loop() {
        support.cancelAnimationFrame(resizeFrameID);
        checkResize();
        resizeFrameID = support.requestAnimationFrame(loop, element);
    }

    /**
     * Check if the element has resized, and broadcast the resize event if so
     * @returns {void}
     * @private
     */
    function checkResize () {
        var newOffsetHeight = element.offsetHeight,
            newOffsetWidth = element.offsetWidth;

        // check if we're in a frame
        if (inIframe) {
            // firefox has an issue where styles aren't calculated in hidden iframes
            // if the iframe was hidden and is now visible, broadcast a
            // layoutchange event
            if (frameWidth === 0 && window.innerWidth !== 0) {
                frameWidth = window.innerWidth;
                scope.broadcast('layoutchange');
                return;
            }
        }

        //on touch devices, the offset height is sometimes zero as content is loaded
        if (newOffsetHeight) {
            if (newOffsetHeight !== currentOffsetHeight || newOffsetWidth !== currentOffsetWidth) {
                currentOffsetHeight = newOffsetHeight;
                currentOffsetWidth = newOffsetWidth;
                currentClientHeight = element.clientHeight;
                currentClientWidth = element.clientWidth;
                broadcast();
            }
        }
    }

    return {

        messages: [
            'layoutchange'
        ],

        /**
         * Handle framework messages
         * @returns {void}
         */
        onmessage: function () {
            // force trigger resize when layout changes
            // @NOTE: we do this because the clientWidth/Height
            // could be different based on the layout (and whether
            // or not the new layout changes scrollbars)
            currentOffsetHeight = null;
            checkResize();
        },

        /**
         * Initialize the Resizer component with an element to watch
         * @param  {HTMLElement} el The element to watch
         * @returns {void}
         */
        init: function (el) {
            element = $(el).get(0);

            // use the documentElement for viewport dimensions
            // if we are using the window as the viewport
            if (element === window) {
                element = document.documentElement;
                $window.on('resize', checkResize);
                // @NOTE: we don't need to loop with
                // requestAnimationFrame in this case,
                // because we can rely on window.resize
                // events if the window is our viewport
                checkResize();
            } else {
                loop();
            }
           $document.on(FULLSCREENCHANGE_EVENT, checkResize);
        },

        /**
         * Destroy the Resizer component
         * @returns {void}
         */
        destroy: function () {
            $document.off(FULLSCREENCHANGE_EVENT, checkResize);
            $window.off('resize', checkResize);
            support.cancelAnimationFrame(resizeFrameID);
        }
    };
});
<|endoftext|>


<commit_before>
import { Cfg, replaceExtension } from '../core/lib';
import { Svx3dHandler } from './svx3dHandler';
import { loxHandler } from './loxHandler';
import { pltHandler } from './pltHandler';
import { FileLoader, EventDispatcher } from '../Three';

function CaveLoader ( callback ) {

	if ( ! callback ) {

		alert( 'No callback specified' );

	}

	this.callback = callback;
	this.dataResponse = null;
	this.metadataResponse = null;
	this.requests = [];

	this.reset();

}

CaveLoader.prototype = Object.create( EventDispatcher.prototype );

CaveLoader.prototype.constructor = CaveLoader;

CaveLoader.prototype.reset = function () {

	this.files = null;
	this.extention = null;
	this.handler = null;
	this.section = null;

	this.requests.forEach( function ( request ) { request.abort(); } );
	this.requests = [];

};

CaveLoader.prototype.setHandler = function ( fileName ) {

	const extention = fileName.split( '.' ).reverse().shift().toLowerCase();

	if ( this.extention !== null && extention !== this.extention ) {

		alert( 'CaveView: mismatched file extension for [' + fileName + ']' );
		return false;

	}

	if ( this.handler !== null ) return true;

	this.extention = extention;

	switch ( this.extention ) {

	case '3d':

		this.handler = new Svx3dHandler( fileName );

		break;

	case 'lox':

		this.handler = new loxHandler( fileName );

		break;

	case 'plt':

		this.handler = new pltHandler( fileName );

		break;

	default:

		console.warn( 'CaveView: unknown file extension [', this.extention, ']' );
		return false;

	}

	return true;

};

CaveLoader.prototype.loadFile = function ( file, section ) {

	if ( file instanceof File ) {

		this.loadLocalFile( file, section );

	} else {

		this.loadURL( file, section );

	}

};

CaveLoader.prototype.loadFiles = function ( files ) {

	this.files = files;
	this.loadFile( files.pop() );

};

CaveLoader.prototype.loadURL = function ( fileName, section ) {

	this.dispatchEvent( { type: 'progress', name: 'start' } );

	if ( section !== undefined ) this.section = section;

	const self = this;
	const prefix = Cfg.value( 'surveyDirectory', '' );

	// setup file handler
	if ( ! this.setHandler( fileName ) ) return false;

	const handler = this.handler;
	const taskCount = 2;

	var doneCount = 0;

	const loader = new FileLoader().setPath( prefix );

	loader.setResponseType( 'json' );

	this.requests.push( loader.load( replaceExtension( fileName, 'json' ), _metadataLoaded, undefined, _metadataError ) );

	loader.setResponseType( handler.type );

	this.requests.push( loader.load( fileName, _dataLoaded, _progress, _dataError ) );

	return true;

	function _dataLoaded ( result ) {

		self.dataResponse = result;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _metadataLoaded ( result ) {

		self.metadataResponse = result;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _progress ( event ) {

		self.dispatchEvent( { type: 'progress', name: 'set', progress: Math.round( 100 * event.loaded / event.total ) } );

	}

	function _dataError ( event ) {

		if ( event.type === 'abort' ) return;

		console.warn( 'error event', event );

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _metadataError ( /* event */ ) {

		if ( event.type === 'abort' ) return;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

};

CaveLoader.prototype.loadLocalFile = function ( file, section ) {

	this.dispatchEvent( { type: 'progress', name: 'start' } );

	if ( section !== undefined ) this.section = section;

	const self = this;
	const fileName = file.name;

	if ( ! this.setHandler( fileName ) ) return false;

	const fLoader = new FileReader();

	fLoader.addEventListener( 'load', _loaded );
	fLoader.addEventListener( 'progress', _progress );

	switch ( this.handler.type ) {

	case 'arraybuffer':

		fLoader.readAsArrayBuffer( file );

		break;

	case 'text':

		fLoader.readAsText( file );

		break;

	default:

		alert( 'unknown file data type' );
		return false;

	}

	return true;

	function _loaded () {

		self.dataResponse = fLoader.result;
		self.callHandler();

		fLoader.removeEventListener( 'load', _loaded );
		fLoader.removeEventListener( 'progress', _progress );

	}

	function _progress ( e ) {

		self.dispatchEvent( { type: 'progress', name: 'set', progress: Math.round( 100 * e.loaded / e.total ) } );

	}

};

CaveLoader.prototype.callHandler = function () {

	if ( this.dataResponse === null ) {

		this.callback( false );
		this.dispatchEvent( { type: 'progress', name: 'stop' } );

		return;

	}

	const data = this.dataResponse;
	const metadata = this.metadataResponse;
	const section = this.section;
	const files = this.files;

	this.dataResponse = null;
	this.metadataResponse = null;

	const moreFiles = files !== null && files.length > 0;

	// start the next download to overlap parsing previous file

	if ( moreFiles ) this.loadFile( files.pop() );

	this.handler.parse( data, metadata, section );

	if ( ! moreFiles ) {

		this.callback( this.handler.end() );
		this.dispatchEvent( { type: 'progress', name: 'end' } );

	}

};

export { CaveLoader };

// EOF<commit_msg>make metadata file loading optional<commit_after>
import { Cfg, replaceExtension } from '../core/lib';
import { Svx3dHandler } from './svx3dHandler';
import { loxHandler } from './loxHandler';
import { pltHandler } from './pltHandler';
import { FileLoader, EventDispatcher } from '../Three';

function CaveLoader ( callback ) {

	if ( ! callback ) {

		alert( 'No callback specified' );

	}

	this.callback = callback;
	this.dataResponse = null;
	this.metadataResponse = null;
	this.requests = [];

	this.reset();

}

CaveLoader.prototype = Object.create( EventDispatcher.prototype );

CaveLoader.prototype.constructor = CaveLoader;

CaveLoader.prototype.reset = function () {

	this.files = null;
	this.extention = null;
	this.handler = null;
	this.section = null;

	this.requests.forEach( function ( request ) { request.abort(); } );
	this.requests = [];

};

CaveLoader.prototype.setHandler = function ( fileName ) {

	const extention = fileName.split( '.' ).reverse().shift().toLowerCase();

	if ( this.extention !== null && extention !== this.extention ) {

		alert( 'CaveView: mismatched file extension for [' + fileName + ']' );
		return false;

	}

	if ( this.handler !== null ) return true;

	this.extention = extention;

	switch ( this.extention ) {

	case '3d':

		this.handler = new Svx3dHandler( fileName );

		break;

	case 'lox':

		this.handler = new loxHandler( fileName );

		break;

	case 'plt':

		this.handler = new pltHandler( fileName );

		break;

	default:

		console.warn( 'CaveView: unknown file extension [', this.extention, ']' );
		return false;

	}

	return true;

};

CaveLoader.prototype.loadFile = function ( file, section ) {

	if ( file instanceof File ) {

		this.loadLocalFile( file, section );

	} else {

		this.loadURL( file, section );

	}

};

CaveLoader.prototype.loadFiles = function ( files ) {

	this.files = files;
	this.loadFile( files.pop() );

};

CaveLoader.prototype.loadURL = function ( fileName, section ) {

	this.dispatchEvent( { type: 'progress', name: 'start' } );

	if ( section !== undefined ) this.section = section;

	const self = this;
	const prefix = Cfg.value( 'surveyDirectory', '' );
	const loadMetadata = Cfg.value( 'loadMetadata', false );

	// setup file handler
	if ( ! this.setHandler( fileName ) ) return false;

	const handler = this.handler;
	const taskCount = loadMetadata ? 2 : 1;

	var doneCount = 0;

	const loader = new FileLoader().setPath( prefix );

	if ( loadMetadata ) {

		loader.setResponseType( 'json' );

		this.requests.push( loader.load( replaceExtension( fileName, 'json' ), _metadataLoaded, undefined, _metadataError ) );

	}

	loader.setResponseType( handler.type );

	this.requests.push( loader.load( fileName, _dataLoaded, _progress, _dataError ) );

	return true;

	function _dataLoaded ( result ) {

		self.dataResponse = result;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _metadataLoaded ( result ) {

		self.metadataResponse = result;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _progress ( event ) {

		self.dispatchEvent( { type: 'progress', name: 'set', progress: Math.round( 100 * event.loaded / event.total ) } );

	}

	function _dataError ( event ) {

		if ( event.type === 'abort' ) return;

		console.warn( 'error event', event );

		if ( ++doneCount === taskCount ) self.callHandler();

	}

	function _metadataError ( /* event */ ) {

		if ( event.type === 'abort' ) return;

		if ( ++doneCount === taskCount ) self.callHandler();

	}

};

CaveLoader.prototype.loadLocalFile = function ( file, section ) {

	this.dispatchEvent( { type: 'progress', name: 'start' } );

	if ( section !== undefined ) this.section = section;

	const self = this;
	const fileName = file.name;

	if ( ! this.setHandler( fileName ) ) return false;

	const fLoader = new FileReader();

	fLoader.addEventListener( 'load', _loaded );
	fLoader.addEventListener( 'progress', _progress );

	switch ( this.handler.type ) {

	case 'arraybuffer':

		fLoader.readAsArrayBuffer( file );

		break;

	case 'text':

		fLoader.readAsText( file );

		break;

	default:

		alert( 'unknown file data type' );
		return false;

	}

	return true;

	function _loaded () {

		self.dataResponse = fLoader.result;
		self.callHandler();

		fLoader.removeEventListener( 'load', _loaded );
		fLoader.removeEventListener( 'progress', _progress );

	}

	function _progress ( e ) {

		self.dispatchEvent( { type: 'progress', name: 'set', progress: Math.round( 100 * e.loaded / e.total ) } );

	}

};

CaveLoader.prototype.callHandler = function () {

	if ( this.dataResponse === null ) {

		this.callback( false );
		this.dispatchEvent( { type: 'progress', name: 'stop' } );

		return;

	}

	const data = this.dataResponse;
	const metadata = this.metadataResponse;
	const section = this.section;
	const files = this.files;

	this.dataResponse = null;
	this.metadataResponse = null;

	const moreFiles = files !== null && files.length > 0;

	// start the next download to overlap parsing previous file

	if ( moreFiles ) this.loadFile( files.pop() );

	this.handler.parse( data, metadata, section );

	if ( ! moreFiles ) {

		this.callback( this.handler.end() );
		this.dispatchEvent( { type: 'progress', name: 'end' } );

	}

};

export { CaveLoader };

// EOF<|endoftext|>