ds/impl/SessionState.js

/**
 * @fileoverview This file contains internal definitions used in DsClient
 *
 * @author anders.rejdebrant@spark-vision.com (Anders Rejdebrant)
 */

goog.provide('spv.ds.impl.SessionState');

goog.require('spv.ds.ImageSerieInfo');
goog.require('spv.ds.UrlSettings');
goog.require('spv.ds.impl.Bump');
goog.require('spv.ds.impl.BumpSerie');




/**
 * Manages all client side session data and provides functions for lookup and
 * mutation of that data.
 *
 * The only client side session data not handled by this class is the session id
 * used by the current implementation of the server protocol. Management of
 * session id the responsibility of the protocol adaptor.
 *
 * @struct
 * @constructor
 * @param {spv.ds.impl.SessionInitData} init_data
 */
spv.ds.impl.SessionState = function(init_data)
{
	/**
	 * @private
	 * @type {spv.ds.impl.SessionInitData}
	 */
	this._init_data = init_data;
	/**
	 * @private
	 * @type {null|string}
	 */
	this._category_id = null;
	/**
	* @private
	* @type {Object.<string, string>}
	*/
	this._category_short_texts = {};
	/**
	* @private
	* @type {Object.<string, string>}
	*/
	this._category_medium_texts = {};
	/**
	* @private
	* @type {Object.<string, string>}
	*/
	this._category_long_texts = {};
	/**
	* @private
	* @type {Object.<string, InternalTextMapCollection>}
	*/
	this._all_texts = convertToInternalTextMapCollections(init_data);
	/**
	 * @private
	 * @type {null|string}
	 */
	this._model_id = null;
	/**
	 * @private
	 * @type {null|spv.ds.SessionStateUpdate}
	 */
	this._state_update = null;
	/**
	 * @private
	 * @type {spv.ds.impl.AuxData}
	 */
	this._aux_data = new spv.ds.impl.AuxData();
};




/**
 * Test if category and model has been set, throw on failure
 */
spv.ds.impl.SessionState.prototype.assertHasLoadedConfig = function()
{
	this.getCategoryId();
	this.getModelId();
};



/**
 * Get the auxiliary data that came with the last bop.
 *
 * @return {spv.ds.impl.AuxData}
 */
spv.ds.impl.SessionState.prototype.getAuxiliaryData = function()
{
	return this._aux_data;
};





/**
 * Get the image serie information for the current configuration
 *
 * @return {Array.<spv.ds.ImageSerieInfo>}
 */
spv.ds.impl.SessionState.prototype.getImageSerieInfos = function()
{
	var bump = getBump(this._init_data, this.getCategoryId());
	var model_series = spv.ds.impl.SessionState.getBumpSeries(bump, this.getModelId());
	/** @type {Array.<spv.ds.ImageSerieInfo>} */
	var image_serie_infos = [];
	for( var i=0; i < model_series.length; ++i )
		image_serie_infos.push( convertToImageSerieInfo( model_series[i] ) );

	return image_serie_infos;
};




/**
 * Get the root menues from bump data
 *
 * @return {Array.<spv.ds.MenuItem>}
 */
spv.ds.impl.SessionState.prototype.getRootMenuItems = function()
{
	var bump = getBump(this._init_data, this.getCategoryId());
	return bump.menus;
};




/**
 * Update the client side session state
 *
 * @param {spv.ds.SessionStateUpdate} state_update
 * @param {spv.ds.impl.AuxData} aux_data
 */
spv.ds.impl.SessionState.prototype.update = function(state_update, aux_data)
{
	this.setCategoryId(state_update);
	this.setModelId(state_update);
	this._state_update = state_update;
	this._aux_data = aux_data;
	this.updateTexts( state_update );
};




/**
 * Updates the text database.
 *
 * @param {spv.ds.SessionStateUpdate} state_update
 */
spv.ds.impl.SessionState.prototype.updateTexts = function( state_update )
{
	for( var i=0, ii=state_update.menu_items.length; i<ii; ++i )
	{
		var menu_item = state_update.menu_items[i];
		var id = menu_item.id;
		var texts = this._all_texts[ menu_item.category ];
		texts.short_texts[ id ] = menu_item.short_text;
		texts.medium_texts[ id ] = menu_item.medium_text;
		texts.long_texts[ id ] = menu_item.long_text;
	}
};




/**
 * Get the last session state update
 *
 * @return {spv.ds.SessionStateUpdate}
 */
spv.ds.impl.SessionState.prototype.getUpdate = function()
{
	if (this._state_update)
	{
		return this._state_update;
	}
	throw new Error('SessionState.prototype.getUpdate failed, no state found.');
};




/**
 * Get the category id for the current configuration or throw if no config
 *
 * @private
 * @return {string}
 */
spv.ds.impl.SessionState.prototype.getCategoryId = function()
{
	if (this._category_id)
	{
		return this._category_id;
	}
	throw new Error('category_id is not set, no configuration loaded');
};




/**
 * Get 'short_text' text for item_id or empty string on failure
 *
 * @param {string} item_id
 * @return {string}
 */
spv.ds.impl.SessionState.prototype.getShortText = function(item_id)
{
	var text = this._category_short_texts[item_id];
	if (text)
	{
		return text;
	}
	return '';
};




/**
 * Get 'medium_text' text for item_id or empty string on failure
 *
 * @param {string} item_id
 * @return {string}
 */
spv.ds.impl.SessionState.prototype.getMediumText = function(item_id)
{
	var text = this._category_medium_texts[item_id];
	if (text)
	{
		return text;
	}
	return '';
};




/**
 * Get 'long_text' text for item_id or empty string on failure
 *
 * @param {string} item_id
 * @return {string}
 */
spv.ds.impl.SessionState.prototype.getLongText = function(item_id)
{
	var text = this._category_long_texts[item_id];
	if (text)
	{
		return text;
	}
	return '';
};




/**
 * Get base urls needed for most GET operations on images etc.
 *
 * @return {spv.ds.UrlSettings}
 */
spv.ds.impl.SessionState.prototype.getUrlSettings = function()
{
	var cat_ids = getCategoryIds(this._init_data);
	if (cat_ids.length < 1)
	{
		throw new Error('No categories found in session init data.');
	}
	var category_id = cat_ids[0];
	var bump = getBump(this._init_data, category_id);
	var result = new spv.ds.UrlSettings(
		bump.gui_url,
		bump.icon_url,
		bump.composed_url,
		bump.readmore_content_url,
		bump.summary_content_url);
	return result;
};




/**
 * Sets the current category id
 *
 * @private
 * @param {spv.ds.SessionStateUpdate} state_update
 */
spv.ds.impl.SessionState.prototype.setCategoryId = function(state_update)
{
	var cat_id = state_update.category;
	if (!cat_id)
	{
		throw new Error('The category id is invalid: ' + cat_id);
	}
	var cat_ids = getCategoryIds(this._init_data);
	if (!arrayContains(cat_ids, cat_id))
	{
		throw new Error('The category id is missing in init data: ' + cat_id);
	}
	this._category_id = cat_id;
	this.rebindTextLookupObjects();
};




/**
 * Updates the text lookup object references for category texts.
 * Intended as a speculative performance optimization as well as a
 * simplification of text lookup code.
 *
 * @private
 */
spv.ds.impl.SessionState.prototype.rebindTextLookupObjects = function()
{
	var cat_id = this.getCategoryId();
	var cat_texts = this._all_texts[cat_id];
	if (!cat_texts)
	{
		cat_texts = new InternalTextMapCollection({}, {}, {});
	}
	this._category_short_texts = cat_texts.short_texts;
	this._category_medium_texts = cat_texts.medium_texts;
	this._category_long_texts = cat_texts.long_texts;
};




/**
 * Get the model id for the current configuration or throw if no config
 *
 * @private
 * @return {string}
 */
spv.ds.impl.SessionState.prototype.getModelId = function()
{
	if (this._model_id)
	{
		return this._model_id;
	}
	throw new Error('model_id is not set, no configuration loaded');
};




/**
 * Sets the current model id
 *
 * @private
 * @param {spv.ds.SessionStateUpdate} state_update
 */
spv.ds.impl.SessionState.prototype.setModelId = function(state_update)
{
	var model_id = state_update.model_id;
	if (!model_id)
	{
		throw new Error('The new model_id is invalid: ' + model_id);
	}
	this._model_id = model_id;
};








/* -----------------------------------------------------------------------------
 * Private utility functions.
 *
 * This looks like a members of global scope but it is not as long as you
 * compile with an outer function wrapper. /AR
 */


/**
 * Lookup the BumpSerie for a model
 *
 * @private
 * @param {spv.ds.impl.Bump} bump
 * @param {string} model_id
 * @return {Array.<spv.ds.impl.BumpSerie>}
 */
// TODO: Put all private functions into the SessionState class.
spv.ds.impl.SessionState.getBumpSeries = function(bump, model_id)
{
	var bump_series = bump.series;
	/** @type {Array.<spv.ds.impl.BumpSerie>} */
	var series_of_model = [];
	for (var i = 0; i < bump_series.length; i++)
	{
		var serie = bump_series[i];
		if (model_id == serie.model_id)
		{
			series_of_model.push(serie);
		}
	}

	if( series_of_model.length == 0 )
		throw new Error('Could not find image serie for model_id: ' + model_id);
	else
		return series_of_model;
};




/**
 * @param {spv.ds.impl.SessionInitData} sid
 * @param {string} category_id
 * @return {spv.ds.impl.Bump}
 */
var getBump = function(sid, category_id)
{
	var bumps = sid.bumps;
	for (var i = 0; i < bumps.length; i++)
	{
		var bump = bumps[i];
		if (category_id == bump.category)
		{
			return bump;
		}
	}
	throw new Error('No bump found for category: ' + category_id);
};




/**
 * Convert internal representation of image serie to the exposed type
 *
 * @param {spv.ds.impl.BumpSerie} bump_serie
 * @return {spv.ds.ImageSerieInfo}
 */
var convertToImageSerieInfo = function(bump_serie)
{
	return new spv.ds.ImageSerieInfo(
			bump_serie.id,
			bump_serie.model_id,
			bump_serie.index,
			bump_serie.first_frame,
			bump_serie.last_frame,
			bump_serie.frames_order,
			bump_serie.image_width,
			bump_serie.image_output_width,
			bump_serie.image_height,
			bump_serie.image_output_height);
};




/**
 * Convert init data to collection of faster text lookup objects
 *
 * @param {spv.ds.impl.SessionInitData} init_data
 * @return {Object.<string, InternalTextMapCollection>}
 */
var convertToInternalTextMapCollections = function(init_data)
{
	/**
	 * @type {Object.<string, InternalTextMapCollection>}
	 */
	var result = {};
	var cat_ids = getCategoryIds(init_data);
	for (var i = 0; i < cat_ids.length; i++)
	{
		var cat_id = cat_ids[i];
		var bump = getBump(init_data, cat_id);
		var text_dict = convertToInternalTextMapCollection(bump);
		result[cat_id] = text_dict;
	}
	return result;
};




/**
 * @param {spv.ds.impl.SessionInitData} sid
 * @return {Array.<string>}
 */
var getCategoryIds = function(sid)
{
	if (!sid)
	{
		throw new Error('Parameter sid (SessionInitData) is not defined');
	}
	var bumps = sid.bumps;
	if (!bumps)
	{
		throw new Error('sid.bumps (Array.<Bump>) is not defined');
	}
	/** @type {Array.<string>} */
	var result = [];
	for (var i = 0; i < bumps.length; i++)
	{
		result.push(bumps[i].category);
	}
	return result;
};




/**
 * @param {spv.ds.impl.Bump} bump
 * @return {InternalTextMapCollection}
 */
var convertToInternalTextMapCollection = function(bump)
{
	/**
	 * @type {Object.<string, string>}
	 */
	var short_texts = {};
	/**
	 * @type {Object.<string, string>}
	 */
	var medium_texts = {};
	/**
	 * @type {Object.<string, string>}
	 */
	var long_texts = {};
	var bump_items = bump.items;
	for (var i = 0; i < bump_items.length; i++)
	{
		var bump_item = bump_items[i];
		var item_id = bump_item.id;
		short_texts[item_id] = bump_item.short_text;
		medium_texts[item_id] = bump_item.medium_text;
		long_texts[item_id] = bump_item.long_text;
	}
	var bump_menus = bump.menus;
	for (var j = 0; j < bump_menus.length; j++)
	{
		var bump_menu = bump_menus[j];
		var menu_id = bump_menu.id;
		short_texts[menu_id] = bump_menu.short_text;
		medium_texts[menu_id] = bump_menu.medium_text;
	}
	var result = new InternalTextMapCollection(
			short_texts,
			medium_texts,
			long_texts);
	return result;
};




/**
 * Helper for browser compatibility.
 * TODO: test indexOf and see what closure compiler translates it too and if it
 * augments the Array prototype if the function is missing. (run tests in IE6)
 *
 * @param {Array.<string>} arr
 * @param {string} elem
 */
var arrayContains = function(arr, elem)
{
	for (var i = 0; i < arr.length; i++)
	{
		if (arr[i] == elem)
		{
			return true;
		}
	}
	return false;
};




/**
 * Internal collection of text lookup objects
 *
 * @private
 * @struct
 * @constructor
 * @param {Object.<string, string>} short_texts
 * @param {Object.<string, string>} medium_texts
 * @param {Object.<string, string>} long_texts
 */
var InternalTextMapCollection = function(
		short_texts,
		medium_texts,
		long_texts)
{
	/**
	* @type {Object.<string, string>}
	*/
	this.short_texts = short_texts;
	/**
	* @type {Object.<string, string>}
	*/
	this.medium_texts = medium_texts;
	/**
	* @type {Object.<string, string>}
	*/
	this.long_texts = long_texts;
};