/* This file created by JSCacher. Last modified: Fri Feb 17 11:02:17 EST 2012 */

Ext.namespace('Voyeur.Localization');

/**
 * @class Voyeur.Localization
 */
Voyeur.Localization = function(lang) {	
	this.lang = lang ? lang : 'en';
	this.bundle = new Ext.util.MixedCollection();
	this.langCodesStore = new Ext.data.JsonStore({
		root: 'languages',
		fields: ['lang', 'code'],
		idProperty: 'code',
		data: {languages: [
		    {lang: 'Afrikaans', code: 'af'},
			{lang: 'Albanian', code: 'sq'},
			{lang: 'Arabic', code: 'ar'},
			{lang: 'Belarusian', code: 'be'},
			{lang: 'Bulgarian', code: 'bg'},
			{lang: 'Catalan', code: 'ca'},
			{lang: 'Chinese Simplified', code: 'zh-CN'},
			{lang: 'Chinese Traditional', code: 'zh-TW'},
			{lang: 'Croatian', code: 'hr'},
			{lang: 'Czech', code: 'cs'},
			{lang: 'Danish', code: 'da'},
			{lang: 'Dutch', code: 'nl'},
			{lang: 'English', code: 'en'},
			{lang: 'Estonian', code: 'et'},
			{lang: 'Filipino', code: 'tl'},
			{lang: 'Finnish', code: 'fi'},
			{lang: 'French', code: 'fr'},
			{lang: 'Galician', code: 'gl'},
			{lang: 'German', code: 'de'},
			{lang: 'Greek', code: 'el'},
			{lang: 'Haitian Creole', code: 'ht'},
			{lang: 'Hebrew', code: 'iw'},
			{lang: 'Hindi', code: 'hi'},
			{lang: 'Hungarian', code: 'hu'},
			{lang: 'Icelandic', code: 'is'},
			{lang: 'Indonesian', code: 'id'},
			{lang: 'Irish', code: 'ga'},
			{lang: 'Italian', code: 'it'},
			{lang: 'Japanese', code: 'ja'},
			{lang: 'Latvian', code: 'lv'},
			{lang: 'Lithuanian', code: 'lt'},
			{lang: 'Macedonian', code: 'mk'},
			{lang: 'Malay', code: 'ms'},
			{lang: 'Maltese', code: 'mt'},
			{lang: 'Norwegian', code: 'no'},
			{lang: 'Persian', code: 'fa'},
			{lang: 'Polish', code: 'pl'},
			{lang: 'Portuguese', code: 'pt'},
			{lang: 'Romanian', code: 'ro'},
			{lang: 'Russian', code: 'ru'},
			{lang: 'Serbian', code: 'sr'},
			{lang: 'Slovak', code: 'sk'},
			{lang: 'Slovenian', code: 'sl'},
			{lang: 'Spanish', code: 'es'},
			{lang: 'Swahili', code: 'sw'},
			{lang: 'Swedish', code: 'sv'},
			{lang: 'Thai', code: 'th'},
			{lang: 'Turkish', code: 'tr'},
			{lang: 'Ukrainian', code: 'uk'},
			{lang: 'Vietnamese', code: 'vi'},
			{lang: 'Welsh', code: 'cy'},
			{lang: 'Yiddish', code: 'yi'}
		]}
	});
}

Voyeur.Localization.prototype = {
	
	/**
	 * Load the values from the passed object into the resource bundle.
	 * @param {Object} json
	 */
	load : function(json) {
		this.bundle.addAll(json);
	},
	
	/**
	 * Retrieve the localized object from the specified key and language (if no
	 * lang parameter is specified, the default one for this instance is used).
	 * @param {String} key the key
	 * @param {String} lang (optional) override the default language
	 */
	get : function(key,lang) {
		lang = lang ? lang : this.lang;
		var vals = this.bundle.get(key);
		if (!vals) {return key;} // key value doesn't exist, return key
		if (vals.lang) {return vals.lang;} // key value returns for lang, return it
		if (vals.en) {return vals.en;} // key value returns for English, return it
		for (var k in vals) {return vals[k]} // return the first value that exists
	}
	
	/**
	 * Get the current two-letter language code.
	 */
	,getLang: function() {
		return this.lang;
	}
	
	/**
	 * Get the language name for a code.
	 * @params {String} code The lang code. If unspecified, the current lang code is used.
	 */
	,getLangName: function(code) {
		code = code || this.lang;
		var name = '';
		var entry = this.langCodesStore.getById(code);
		if (entry) name = entry.get('lang');
		return name;
	}
}

Voyeur.localization = new Voyeur.Localization();

Ext.namespace('Voyeur');

// stop windows from stealing focus when Voyeur is in an iframe
Ext.override(Ext.Window, {
	toFront : function(e){
    	if (window.parent == window) {
	        if(this.manager.bringToFront(this)){
	            if(!e || !e.getTarget().focus){
	                this.focus();
	            }
	        }
    	}
        return this;
    }
});

// use thousands separator for paging toolbar display info
Ext.override(Ext.PagingToolbar, {
	updateInfo: function() {
        if(this.displayItem){
            var count = this.store.getCount();
            var msg = count == 0 ?
                this.emptyMsg :
                String.format(
                    this.displayMsg,
                    Ext.util.Format.number(this.cursor+1, "0,000"),
                    Ext.util.Format.number(this.cursor+count, "0,000"),
                    Ext.util.Format.number(this.store.getTotalCount(), "0,000")
                );
            this.displayItem.setText(msg);
        }
    }
});

/**
 * @class Voyeur.Application The parent class for Voyeur.  Handles event notification and keeps track of tools and corpus.
 * @extends_ext Ext.util.Observable
 * @param {Object} config configuration object
 * @constructor Create a new Voyeur application.
 */
Voyeur.Application = Ext.extend(Ext.util.Observable, {
	constructor : function(config) {
		this.query = Ext.urlDecode(window.location.search.substring(1));
		this.version = config.version ? config.version : '1.0';
		this.build = config.build ? config.build : '?';
		this.setBaseUrl(document.location.protocol+'//'+document.location.host+document.location.pathname);
		this.corpus = new Voyeur.Corpus(this.query.corpus ? {'@id': this.query.corpus} : null);
		this.tools = new Ext.util.MixedCollection();
		this.iframe = window.parent != window;
		Voyeur.Application.superclass.constructor.call(this, config);
		this.events = [];
		this.addListener('appReady', function(src, data) {
			// make sure to load corpus
			var query = Ext.urlDecode(window.location.search.substring(1));
			if (this.query.corpus || this.query.input) {
				if (query.input) {
					Ext.applyIf(query, {corpusCreateIfNotExists: true})
					if (query.inputFormat && query.inputFormat=="ZIP") {
						query.inputFormat='zip'; // workaround because of
					}
				}
				this.update({
					title : this.localize('app.loading')
					,params : query
					,tool : 'CorpusSummary'
				})
			}
			
		});
		
		this.addListener('resultLoaded', function(src, data) {
			if (data.corpus && data.corpus['@lastModified'] > this.corpus.get('lastModified')) {
				this.corpus = new Voyeur.Corpus(data.corpus)
				var id = this.corpus.getId();
				this.tools.each(function(tool) {
					tool.setApiParams({corpus: id});
				})
				/*
				var corpus = data.corpus;
				this.corpus = new Voyeur.Corpus({
					id: corpus['@id']
					,lastModified : corpus['@lastModified']
				});
				for (var i=0;i<corpus.documents.length;i++) {
					var doc = new Ext.util.MixedCollection();
					doc.addAll({
						index : parseInt(corpus.documents[i]['@index'])
						,id : corpus.documents[i]['@id']
						,title : corpus.documents[i]['@title']
						,author : corpus.documents[i]['@author']
						,timeInMillis : parseInt(corpus.documents[i]['@time'])
					})
					this.corpus.add(doc)
				}
				*/				
			}
		},this);

		this.addListener('toolLoaded', function(src, data) {
			this.tools.each(function(tool) {
				if (src.xtype==tool.xtype) {
					this.setApiParams({corpus: this.getCorpus().getId()});
				}
			})
		}, this);

	},
    init: function(){
        this.initTips();

		this.stopListsStore = new Ext.data.ArrayStore({
            fields: ['id', 'label', 'description'],
            data : [
                    ['stop.en.taporware.txt','Taporware (English)','']
                    ,['stop.fr.veronis.txt','Véronis (French)','']
                    ,['stop.de.german.txt','German','']
                    ,['stop.es.spanish.txt','Spanish','']
                    ,['stop.hu.hungarian.txt','Hungarian','']
                    ,['stop.it.italian.txt','Italian','']
                    ,['stop.no.norwegian.txt','Norwegian','']
            ]
        });

		this.corporaStore = new Ext.data.ArrayStore({
            fields: ['id', 'label', 'description'],
            data : [
                    ['humanist','Humanist Listserv Archives','']
                    ,['shakespeare','Shakespeare\'s Plays (Moby)','']
            ]
        });
        
        this.initPage();
		this.dispatchEvent('appReady', this)
    },
    initTips: function(){
        Ext.QuickTips.init();
        // Apply a set of config properties to the singleton
        Ext.apply(Ext.QuickTips.getQuickTip(), {
            //minWidth: 1000,
            dismissDelay: 0,
            qclass: 'qtip'
        });
        
    },
    
	/*
    initPage: function(){
        var params = window.location.search.split(/\?|&/);
		var query = {}
        if (params.length > 0) {
            for (var i = 0, length = params.length; i < length; i++) {
                var param = params[i].split('=');
                if (param[1] != null) {query[param[0]] = param[1];}
            }
        }
		
		// if a corpus has been specified, load it
		if (query.corpus) {
			this.corpus = new Voyeur.Corpus({id : query.corpus});
			this.corpusRetrievedHandler();
//			this.update({
//				title : this.localization.get('progress.initializing_corpus'),
//				tool : 'CorpusTool',
//				handler : this.corpusRetrievedHandler,
//				scope : this
//			});
		}        
    },*/
	
    /**
     * Initiates an Ajax call to Trombone with progress bar.
     * @param {Object} args configuration options that may contain the following properties:<ul>
     * <li><b>progressWin</b> : Ext.ProgressBar<div class="sub-desc">(Optional) A {@link Ext.ProgressBar} to show progress (if no progressWin is specified, one will be created).</div></li>
     * <li><b>renderTo</b>: Mixed<div class="sub-desc">(Optional) A DOM id, DOM node or {@link Ext.Element} (same arguments as {@link Ext.get}. If this property is not provided then the document body will be used.</div></li>
     * <li><b>title</b>: String<div class="sub-desc">(Optional) The progress bar's title.</div></li>
     * <li><b>params</b>: Mixed<div class="sub-desc">(Optional) Parameters to be passed to the Ajax request, either as an object or as a URL encoded string (name=value&amp;name=value...)</div></li>
     * </ul>
     */
	update : function(args, fromTool) {

		var progressWin = args.progressWin;
		if (!progressWin) {
			var renderTo = args && args.renderTo ? Ext.get(args.renderTo) : Ext.getBody();
			var wid = renderTo.getWidth(true)-40;
			wid = wid>600 ? 600 : wid;
			var progressBar = new Ext.ProgressBar({
				width: wid
			});
			progressWin = new Ext.Window({
	            title: args.title,
	            layout: 'fit',
	            width: wid,
	            autoHeight: true,
	            closeAction: 'close',
	            modal: true,
	            plain: true,
				renderTo : args && args.renderTo ? args.renderTo : document.body,
	            items: [progressBar]
	            ,listeners: {
					beforedestroy: {
						fn: function(win) {
							// IE isn't happy unless the bar is reset
							var bar = win.items.get(0).reset();
						}
					}
				}
        	});
			progressWin.show();
			progressBar.wait({
				interval : 200,
				text: this.localize('app.loading')
			});
		}

		if (!args.params) {args.params = {};}
		if (typeof args.params == 'string') {
			args.params = Ext.urlDecode(args.params);
		}
		args.params.tool = args.tool;

		if (!args.params.corpus && !this.getCorpus().isEmpty()) {args.params.corpus=this.getCorpus().getId();}
		Ext.Ajax.request({
			url : this.getBaseUrl()+"trombone/",
			method : 'post',
			params : args.params,
			form : args.form,
			callback : function(options,success,response) {
				var msg = response.statusText;
				if (success) {
					var r = Ext.util.JSON.decode(response.responseText);
					if (r.error) {
						msg = Voyeur.localization.get(r.code ? r.code : r.error);
						if (msg==r.code) {msg=r.error;}
					}
					else {
						if (r.message) {
							msg = Voyeur.localization.get(r.code ? r.code : r.message);
							if (msg==r.code) {msg=r.message;}
							progressWin.items.get(0).updateText(msg);
						}
						else if (r.result) {
							progressWin.items.get(0).updateText(this.localize('app.retrieving'))
						}
						if (r.process || r.result) {
				            var me = this;
				            setTimeout(function(){
				                me.update({
									params : r,
									progressWin : progressWin,
									handler : args.handler,
									scope : args.scope,
									tool: args.tool,
									title : args.title,
									event : args.event
								}, fromTool)
				            }, 500);
						}
						else {
							progressWin.destroy();
							if (typeof args.tool == "string") {args.tool = [args.tool]}
							for (var i=0;i<args.tool.length;i++) {
								this.dispatchEvent(args.event ? args.event : args.tool[i] + 'ResultLoaded', fromTool ? fromTool : this, r);
							}	
						}
						return;
					}
				} else {
					var paramsString = '<ul>';
					for (var k in options.params) {
						paramsString += '<li>'+k+': '+options.params[k]+'</li>';
					}
					paramsString += '</ul>';
					msg += paramsString
					if (response.responseText) {
						var detailsButton = '<input type="button" value="'+this.localize('app.toggleDetails')+
						'" onClick="Ext.get(\'serverErrorDetails\').setVisibilityMode(Ext.Element.DISPLAY).toggle();" />';
						var errorString = response.responseText.replace('<pre', detailsButton+'<pre');
						msg += errorString;
					}
				}
				
				progressWin.destroy();
				Ext.Msg.show({
					title : this.localize('app.error'),
					msg : msg,
					buttons : Ext.Msg.OK,
					icon : Ext.Msg.ERROR,
					width: 600,
					height: 400
				});
			},
			scope : this
		});
	},
	
	/**
	 * Gets the corpus.
	 * @return {Voyeur.Corpus}
	 */
	getCorpus : function() {return this.corpus;},
	
	/**
	 * Adds a tool.
	 * @param {Object} tool The tool object.
	 */
	addTool : function(tool) {
		this.tools.add(tool.id, tool);
		this.dispatchEvent('toolLoaded', tool, tool);
	},
	
	/**
	 * Dispatches an event to all tools.
	 * @param {String} name The event name.
	 * @param {Object} source The source (tool) of the event.
	 * @param {Object} data The data associated with the event.
	 * @param {Object} filter <ul>
	 * <li><b>tools</b> : String|Array<div class="sub-desc">One or more tools (xtypes) to apply the filter to.</div></li>
	 * <li><b>type</b> : String<div class="sub-desc">Possible values: 'whitelist' (event will be sent exclusively to these tools), 'blacklist' (event will be sent to all tools but these)</div></li>
	 * </ul>
	 */
	dispatchEvent : function () {
		var ev = arguments[0];
		var args = arguments;
		
		if (window.console && console.info) {console.info(arguments[0],arguments[1],arguments);}
		if (ev.indexOf("ResultLoaded")>0) {this.fireEvent('resultLoaded', arguments[1], arguments[2])}
		this.fireEvent.apply(this,arguments);
		
		var hasListener = false;
		var evLower = ev.toLowerCase();
		var filter = arguments[3];
		if (filter != null) {
			if (typeof filter.tools == 'string') filter.tools = [filter.tools];
		}
		
		this.tools.each(function(tool) {
			if (filter != null) {
				var match = filter.tools.indexOf(tool.xtype) != -1;
				if (filter.type == 'whitelist' && !match) return true;
				else if (filter.type == 'blacklist' && match) return true;
			}
			if (!tool.disabled && tool.hasListener(evLower)) {
				var listened = tool.fireEvent.apply(tool, args);
				if (!hasListener && listened) {hasListener=true;}
			}
		});
		if (!hasListener && ev!='toolLoaded' && ev!='appReady' && ev!='unhandledEvent') {
			if (window.console && console.info) {console.info('Unhandled event: ', arguments[0],arguments[1],arguments);}
			args = ['unhandledevent', arguments[1]];
			for (var i=0;i<arguments.length;i++) {args.push(arguments[i])}
			this.fireEvent.apply(this, args);
		}
			
	},

	/**
	 * Applies param(s) to all tools.
	 * @param {Object} params An object containing the params to set.
	 */
	applyParamsGlobally : function(params, reload) {
		this.tools.each(function(tool) {
			tool.setApiParams(params);
		});
		if (reload) {
			this.update({tool: 'CorpusSummary'});
		}
	},
	
	/**
	 * The following two methods are called from Mandala when the user
	 * drags a dot outside the confines of the Mandala workspace to show
	 * that the cursor is holding a Mandala dot and may drop it on to another
	 * tool for some purpose.
	 * @private
	 */
	changeCursorToDrag : function() {
		document.body.style.cursor = 'crosshair';
	},
	
	changeCursorToDefault : function() {
		document.body.style.cursor = 'default';
	},
	
	/**
	 * Displayes a "growl" style message.
	 * @param {String} title The message title.
	 * @param {String} msg The message body.
	 * @param {Mixed} renderTo The Element or Element id to render the message to. 
	 */
    growl : function(title, msg, renderTo) {
		renderTo = renderTo ? renderTo : document.body;
        var msgCt = Ext.DomHelper.append(renderTo, {id:'msg-div'}, true);
        msgCt.alignTo(renderTo, 'c-c', [-10,-10]);        /*<-- pos in bottom-right corner*/
        
        var msg = String.format.apply(String, Array.prototype.slice.call(arguments, 1));
        var m = Ext.DomHelper.append(msgCt, {html:this.createBox(title, msg)}, true);
        m.slideIn('b').pause(3).ghost("b", {remove:true});  /*<--move from bottom*/
    },
	createBox : function(t, s){
        return ['<div class="msg">',
                '<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>',
                '<div class="x-box-ml"><div class="x-box-mr"><div class="x-box-mc"><h3>', t, '</h3>', s, '</div></div></div>',
                '<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>',
                '</div>'].join('');
    }
	
    /**
     * Localizes a string.
     * @param {String} key The key of the string to localize.
     */
	,localize : function(key) {return Voyeur.localization.get(key)}
	
	/**
	 * Loads a localization.
	 * @param {Object} loc An object with localized strings as values.
	 * @param {String} prefix A prefix to add to the keys in the loc param.
	 */
	,loadLocalization : function(loc, prefix) {
		if (prefix) {
			var prefixedLocalization = {}
			for (key in loc) {
				prefixedLocalization[prefix+key] = loc[key]
			}
			this.getLocalization().load(prefixedLocalization);
		}
		else {this.getLocalization().load(loc);}
	}
	
	/**
	 * Gets the current localization.
	 * @return {Voyeur.Localization}
	 */
	,getLocalization : function() {return Voyeur.localization;}
	
	,setBaseUrl : function(baseUrl) {
		this.baseUrl=baseUrl;
		Ext.BLANK_IMAGE_URL = this.baseUrl + 'resources/themes/default/images/s.gif';
	}
	
	/**
	 * Gets the base URL for the Voyeur application.
	 * @return {String}
	 */
	,getBaseUrl : function() {return this.baseUrl;}
	
	/**
	 * Gets the Trombone URL.
	 * @return {String}
	 */
	,getTromboneUrl : function() {return this.getBaseUrl()+"trombone";}
	
	/**
	 * Gets the application version.
	 * @return {String}
	 */
	,getVersion : function() {return this.version;}
	
	/**
	 * Gets the build.
	 * @return {String}
	 */
	,getBuild : function() {return this.build;}
	
	/**
	 * Gets the stop lists store.
	 * @return {Ext.data.ArrayStore}
	 */
	,getStopListsStore : function() {return this.stopListsStore;}
	
	/**
	 * Gets the corpora store.
	 * @return {Ext.data.ArrayStore}
	 */
	,getCorporaStore : function() {return this.corporaStore;}
	
	/**
	 * Gets a query param by name.
	 * @param {String} name The name of the query param.
	 * @return {String}
	 */
	,getQueryParam: function(name) {return this.query[name];}
	

});

Voyeur.localization.load({
	'app.title' : {en : 'Voyant'}
	,'app.colon' : {en : ': '}
	,'app.loading' : {en : 'Loading'}
	,'app.error' : {en : 'Error'}
	,'app.info' : {en : 'Info'}
	,'app.toggleDetails' : {en: 'Toggle details'}
	,'app.information' : {en : 'Information'}
	,'app.retrieving' : {en : 'Retrieving Results'}
	,'app.split_tip' : {en : 'Drag to resize.'}
	,'app.collapsible_split_tip' : {en : 'Drag to resize. Double click to hide.'}
})


/**
 * @class Voyeur.Corpus Corpus encapulates all the metadata of a Trombone corpus class.
 * You should not need to create a Corpus directly.
 * @constructor
 * @param {Object} data The corpus data object, or null to create an empty corpus.  The following are valid object properties:<ul>
 * <li><b>@id</b> : String<div class="sub-desc">The corpus id.</div></li>
 * <li><b>@lastModified</b> : String<div class="sub-desc">A timestamp indicating when the corpus was last modified.</div></li>
 * <li><b>@totalWordTokens</b> : String<div class="sub-desc">The total word tokens in the corpus.</div></li>
 * <li><b>@totalWordTypes</b> : String<div class="sub-desc">The total word types in the corpus.</div></li>
 * <li><b>documents</b> : Ext.util.MixedCollection<div class="sub-desc">The collection of documents contained in this corpus.</div></li>
 */
Voyeur.Corpus = function(data) {
	var store = new Ext.data.JsonStore( {
		fields : Voyeur.data.Corpus.fields,
		data : data ? [data] : [{'@id': new Date().getTime()+'.'+parseInt(Math.random()*10000)}]
	})
	this.record = store.getAt(0);
	this.documents = new Ext.util.MixedCollection();
	if (data && data.documents) {
		var doc;
		var dates = new Ext.util.MixedCollection();
		var authors = new Ext.util.MixedCollection();
		for ( var i = 0; i < data.documents.length; i++) {
			doc = new Voyeur.Document(data.documents[i]);
			this.documents.add(doc.get('id'), doc);
		}
		if (this.getAuthors().length<2) {
			this.documents.each(function(doc) {doc.isAllSameAuthor=true;})
		}
		if (this.getDates().length<2) {
			this.documents.each(function(doc) {doc.isAllSameDate=true;})
		}
			
	}
}
Voyeur.Corpus.prototype = {

	/**
	 * Get the ID for this corpus.
	 * @return {String} The ID for this corpus.
	 */
	getId : function() {
		return this.record.get('id');
	},
	

	/**
	 * Get the metadata value for the specified key.
	 * @param {String} key The key for the metadata value.
	 * @return {Mixed} The metadata value for the specified key.
	 */
	get : function(key) {
		return this.record.get(key)
	},
	
	/**
	 * Get the document specified by the key (number for index, string for id).
	 * @param {String|Number} key The key to use (number for index, string for id).
	 * @return {Voyeur.Document} The document specified by the key.
	 */
	getDocument: function(key) {
		return this.documents.get(key);
	},

	/**
	 * Get the documents for this corpus, as a {@link Ext.util.MixedCollection}.
	 * @return {Ext.util.MixedCollection} The documents for this corpus.
	 */
	getDocuments: function() {
		return this.documents;
	},

	/**
	 * Get an array of short titles for documents in this corpus.
	 * @return {Array}
	 */
	getShortTitles : function() {
		var titles = [];
		this.documents.each(function(item) {
			titles.push(item.get('title'))
		})
		return titles;
	},

	/**
	 * Get an array of short labels for documents in this corpus.
	 * @return {Array}
	 */
	getShortLabels : function() {
		var labels = [];
		this.documents.each(function(item) {
			labels.push((parseInt(item.get('index')) + 1) + ") "
					+ item.getShortTitle())
		}, this);
		return labels;
	}
	
	/**
	 * Get an array of the unique authors in this corpus.
	 * @return {Array}
	 */
	,getAuthors: function() {
		var auth = {};
		this.documents.each(function(doc) {auth[doc.get('author')]=true;});
		var authors = [];
		for (var k in auth) {authors.push(k);}
		return authors;
	}

	/**
	 * Get an array of the unique dates in this corpus.
	 * @return {Array}
	 */
	,getDates: function() {
		var dats = {};
		this.documents.each(function(doc) {dats[doc.getDate()]=true;});
		var dates = [];
		for (var k in dats) {dates.push(k);}
		return dates;
	}
	
	/**
	 * Gets the number of documents in the current corpus.
	 * @return {Integer}
	 */
	,getSize: function() {
		return this.documents.getCount();
	}

	/**
	 * Determine if the corpus contains any documents.
	 * @return {Boolean}
	 */
	,isEmpty: function() {
		return this.documents.getCount==0;
	}
	
	/**
	 * Get total number of words in the corpus.
	 * @return {Integer}
	 */
	,getTotalWordTokens: function() {
		return this.record.get('totalWordTokens')
	}
}

/**
 * @class Voyeur.Document Document encapsulates all the metadata of a Trombone document class.  You should not need to create a Document directly.
 */
Voyeur.Document = function(data) {
	var store = new Ext.data.JsonStore( {
		fields : Voyeur.data.Document.fields,
		data : data ? [ data ] : []
	})
	this.record = store.getAt(0);
	
	// private – we'll make the short label shorter if all dates are the same
	this.isAllSameDate = false;
	
	// private – we'll make the short label shorter if all authors are the same
	this.isAllSameAuthor = false;
}

Voyeur.Document.prototype = {

	/**
	 * Get the metadata value for the specified key.
	 * @param {String} key The key for the metadata value.
	 * @return {Mixed} The metadata value for the specified key.
	 */
	get : function(key) {
		return this.record.get(key)
	},
	
	/**
	 * Gets a label for this document.
	 * @return {String} The document title, author, and date.
	 */
	getLabel : function() {
		var label = (parseInt(this.record.get('index')) + 1) + ") ";
		label += this.get('title');
		if (!this.isAllSameAuthor) {
			var author = this.getShortAuthor();
			label += author.length > 0 ? ' (' + author + ')' : ''
		}
		if (!this.isAllSameDate) {label += ' '+this.getDate() + " "}
		return label;
	},

	/**
	 * Gets a short label for this document.
	 * @return {String} The short document title, author, and date.
	 */
	getShortLabel : function() {
		var label = (parseInt(this.record.get('index')) + 1) + ") ";
		label += this.getShortTitle();
		if (!this.isAllSameAuthor) {
			var author = this.getShortAuthor();
			label += author.length > 0 ? ' (' + author + ')' : ''
		}
		if (!this.isAllSameDate) {label += ' '+this.getDate() + " "}
		return label;
	},
	
	/**
	 * Gets the short document title.
	 * @return {String}
	 */
	getShortTitle : function() {
		return this.record.get('shortTitle');
	},
	
	/**
	 * Gets the document author (trimmed if necessary).
	 * @return {String}
	 */
	getShortAuthor : function() {
		var author = this.record.get('author');
		if (author.length > 15) {
			return author.substring(0, author.indexOf(" ", 10)) + "…";
		} else {
			return author
		}
	},
	
	/**
	 * Gets the data associated with the document.
	 * @return {Date}
	 */
	getDate : function() {
		return new Date(this.record.get('timeInMillis')).format("Y-m-d")
	}
	
	/**
	 * Gets the index for this document.
	 * @return {Integer}
	 */
	,getIndex: function() {
		return this.record.get('index');
	}
	
	/**
	 * Gets the ID for this document.
	 * @return {String}
	 */
	,getId: function() {
		return this.record.get('id');
	}

	/**
	 * Gets the total number of words in the document.
	 * @return {Integer}
	 */
	,getTotalWordTokens: function() {
		return this.record.get('totalWordTokens')
	}

	/**
	 * Gets the  total number of tokens in the document.
	 * @return {Integer}
	 */
	,getTotalTokens: function() {
		return this.record.get('totalTokens')
	}
}

Ext.namespace('Voyeur.Tool');


/**
 * @class Voyeur.Tool
 * @namespace Voyeur
 * @extends_ext Ext.util.Observable
 * @param {Object} config configuration object
 * @constructor Create a new Voyeur tool.
 */
Voyeur.Tool = Ext.extend(Ext.util.Observable, {
	
    // the parameter values last sent to an update
    lastUpdate : {}
	
	,constructor : function(config, tool) {

		// make sure that we have i18n and api values
		Ext.applyIf(tool, {
			i18n: {}, api: {toolFlow: {'default': null}}
		})
		
		Ext.applyIf(tool.api, {
			'corpus': {
				'default': null
				,'type': String
				,'required': false
				,'value': null
				,'multiple': false
				,'example': 'a_valid_corpus_id'
			}
		})
		

		Voyeur.Tool.superclass.constructor.call(this);
		
		
		this.xtype = config.xtype;
		this.events = [];


		// make sure the application knows about us
		this.addListener('render', function(cmp) {
			Voyeur.application.addTool(cmp);
		});
		
		this.loadLocalization(tool.i18n, this.xtype);
		
		// copy query and config values to this API
		var query = this.getApplication().query;
		for (var k in tool.api) {
			if (config.api && config.api[k]) {
				tool.api[k].value=config.api[k];
			} else if (query[k] != undefined) {
				tool.api[k].value=query[k];
			}
		}
		if (config.api) {
			delete config.api;
		}
		
		this.exporters = config.exporters ? config.exporters : {};
		Ext.applyIf(this.exporters, {
			url: this.localize('exportUrl','tool'),
			button: this.localize('exportButton','tool'),
			iframe: this.localize('exportIframe','tool'),
			citation: this.localize('exportCitation','tool')
			//html: this.localize('exportHtml','tool')
		});
				
		// build the default toolset
		var tools = [
			{
				id : 'save',
				qtip : '<h3>' + this.localize('export', 'tool')
				+ this.localize('colon', 'app')
				+ this.localize('title') + '</h3>'
				+ this.localize('exportTip','tool'),
				handler : this.onExport
				,scope : this
			},/*{
				id : 'plus',
				qtip : '<h3>' + this.localize('more', 'tool')
						+ this.localize('colon', 'app')
						+ this.localize('title') + '</h3>'
						+ this.localize('moreTip', 'tool')
				,handler : this.onMore
				,scope : this
			},*/
			{
				id : 'help',
				qtip : '<h3>' + this.localize('help', 'tool')
						+ this.localize('colon', 'app')
						+ this.localize('title') + '</h3>'
						+ this.localize('help') + '<br /><br /><i>'+this.localize('helpTipClick','tool')+"</i>"
				,handler : this.onHelp
				,scope : this
			}
		];
		
		if (tool.showOptions != undefined) {
			tools.splice(0, 0, {
				id : 'gear',
				qtip : '<h3>' + this.localize('options', 'tool')
				+ this.localize('colon', 'app')
				+ this.localize('title') + '</h3>'
				+ this.localize('optionsTip','tool'),
				handler : this.onOptions,
				scope : this
			});
		}
		
		Ext.applyIf(config, {
			title : this.localize('title') +
				'<span  class="Z3988" title="ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rfr_id=info%3Asid%2Focoins.info%3Agenerator&amp;rft.genre=book&amp;rft.btitle=Voyeur+Tools&amp;rft.title='+this.getApplication().localize('app.title')+' :: '+this.localize('title')+'&amp;rft.aulast=Sinclair&amp;rft.aufirst=St%C3%A9fan&amp;rft.au=St%C3%A9fan+Sinclair&amp;rft.au=Geoffrey+Rockwell&amp;rft.date=2009&amp;rft_id=http://voyeurtools.org/"></span>',
			toolNames : [ 'help' ],
			tools : tools
		});
	}
	
	/**
	 * Sets the given API params for this tool.
	 * @param {Object} params An object containing the keys and values to set.
	 */
	,setApiParams: function(params) {
		for (var k in params) {
			if (this.api[k]) {this.api[k].value = params[k];}
		}
	}
	
	/**
	 * Gets all the API params for this tool.
	 * @return {Object} The API params.
	 */
	,getApiParams : function() {
		var params = {};
		var val;
		for (var k in this.api) {
			val = this.getApiParamValue(k);
			if (val!==null && val!=undefined && val!='' && val != []) {
				params[k] = val;
			}
		}
		return params;
	}

	/**
	 * Gets an API param value by key.
	 * @param {String} key The name of the API param to get the value of.
	 * @return {Mixed} The value of the API param.
	 */
	,getApiParamValue : function(key) {
		if (!this.api[key]) {return null;}
		return this.api[key].value != undefined && this.api[key].value != null ? this.api[key].value : this.api[key]['default'];
	}
	
	/**
	 * Gets the default value of an API param.
	 * @param {String} key The name of the API param to get the default value of.
	 * @return {Mixed} The default value of the API param.
	 */
	,getApiParamDefaultValue : function(key) {
		if (!this.api[key]) {return null;}
		return this.api[key]['default'];
	}
	
	/**
	 * Resets all API params to their default values.
	 */
	,resetApiParamValues : function() {
		var changed = false;
		for (var k in this.api) {
			if (changed==false) {changed = this.api[k].value = this.getApiParamDefaultValue(k)}
			this.api[k].value = this.getApiParamDefaultValue(k);
		}
		return changed;
	}
	
	,onExport : function(ev, tool, panel) {
		if (panel.getColumnModel) {
			Ext.applyIf(this.exporters, {
				htmlTable: this.localize('exportHtmlTable','tool'),
				textTable: this.localize('exportTextTable','tool'),
				csvTable: this.localize('exportCsvTable','tool'),
				tabTable: this.localize('exportTabTable','tool'),
				xmlTable: this.localize('exportXmlTable','tool')
			})
		}
		
		if (this.getCanvasElement(panel)) {
			Ext.applyIf(this.exporters, {
				canvasPng: this.localize('exportCanvasPng','tool')
			})
		}
		var items = [];
		for (var k in panel.exporters) {
			if (panel.exporters[k]) {
				items.push({
					inputValue: k,
					boxLabel: panel.exporters[k],
					name: 'exp',
					checked: k=='url' ? true : false
				})
			}
		}
		
		var win = new Ext.Window({
			title: 'Export',
			modal: true,
			width: 370,
			items: {
				xtype: 'form',
				labelAlign: 'top',
				bodyStyle: 'padding:0 10px 0;',
				items: {
					xtype: 'radiogroup'
					,fieldLabel: ''
					,hideLabel: true
					,items: items
					,columns: 1
				}
				,buttons: [{
					text: this.localize('ok','tool'),
					listeners: {
						click: {
							fn: function(btn) {
								var exp = btn.findParentByType('form').getForm().getValues(false).exp;
								this.fireEvent('export', exp);
								var content = '';
								var isGrid = this.getView && this.getColumnModel;
								msg = ''; // if this is empty at the end nothing will be displayed
								var url = panel.getApplication().getBaseUrl();
								var params = {};
								if (panel.xtype!='voyeurDocumentInputAdd') {url+='tool/'+panel.xtype.replace(/^voyeur/,'')+'/';}
								
								if (exp=='url' || exp=='iframe' || exp=='button') {
									msg += '<p>'+this.localize('apiWarn','tool')+'</p>';
									if (exp=='iframe' || exp=='button') {
										content+="<!--\tExported from "+this.getApplication().getBaseUrl()+".\n\t"+this.localize('apiWarn','tool').replace(/<.+?>/g,'')+" -->\n"
									}
									if (exp=='iframe') {
										var size = this.getSize();
										content+='<iframe width="'+size.width+'" height="'+size.height+'" src="';
									}
									else if (exp=='button') {
										content+="<form action='"+url+"' method='get' target='_blank'>\n";
									}
									if (panel.xtype!='voyeurDocumentInputAdd') {
										url += '?';
										params.corpus = this.getCorpus().getId();
										var apiParams = this.getApiParams();
										for (var k in this.api) {
											if (k && this.api[k].value != null && this.api[k].value != this.api[k]['default']) {
												params[k] = this.api[k].value;
											}
										}
										url+=Ext.urlEncode(params);
									}
									if (exp=='iframe' || exp=='url') {content+=url;}
									if (exp=='iframe') {content+='"></iframe>';}
									else if (exp=='button') {
										for (var k in params) {
	
											if (typeof params[k] == 'string') {
												content+="<input type='hidden' name='"+k+"' value=\""+params[k]+"\" />\n"
											}
											else if (params[k] && params[k].length) {
												for (var i=0;i<params[k].length;i++) {
													content+="<input type='hidden' name='"+k+"' value=\""+params[k][i]+"\" />\n"
												}
											}
										}
										content+='<input type="submit" value="'+new Ext.Template(this.localize('viewResults','tool')).apply([this.localize('title')])+'" />\n</form>';
									}
									else if (exp=='url') {
										msg += '<p>'+ new Ext.Template(this.localize('clickUrl','tool')).apply([url])+'</p>';
									}
								}
								else if (exp=='htmlTable' || exp=='textTable' || exp=='csvTable' || exp=='tabTable' || exp=='xmlTable') {
									if (isGrid) {
										msg += '<p>'+this.localize('exportContentTip','tool')+'</p>';
										var columnModel = this.getColumnModel();
										var columnCount = columnModel.getColumnCount();
										var view = this.getView();
										var rowCount = view.getRows().length;
										var cols = [];
										var rows = [];
										var val;
										for (var i=0;i<columnCount;i++) {
											val = columnModel.getColumnHeader(i);
											if (val.indexOf('-checker')==-1) {
												cols.push(val);
											}
										}
										rows.push(cols);
										for (var i=0;i<rowCount;i++) {
											cols = [];
											for (var j=0;j<columnCount;j++) {
												val = view.getCell(i,j).innerHTML;
												if (val.indexOf('-checker')==-1) {
													cols.push(val);
												}
											}
											rows.push(cols);
										}
										var table = "";
										if (exp=='textTable') {
											longest = new Array(rows[0].length);
											for (var i=0;i<rows.length;i++) {
												for (var j=0;j<rows[i].length;j++) {
													val = rows[i][j].replace(/&nbsp;/g,' ').replace(/<\/?\w+.*?>/g,'');
													rows[i][j] = val;
													if (!longest[j] || longest[j]<val.length) {longest[j]=val.length;}
												}
											}
											for (var i=0;i<rows.length;i++) {
												for (var j=0;j<rows[i].length;j++) {
													rows[i][j] = String.leftPad(rows[i][j],longest[j],' ');
												}
											}
										}
										for (var i=0;i<rows.length;i++) {
											if (exp=='htmlTable') {
												if (i==0) {table+="\t<thead>\n";}
												if (i==1) {table+="\t<tbody>\n";}
												table+="\t\t<tr>\n\t\t\t<td>"+rows[i].join("</td>\n\t\t\t<td>")+"</td>\n\t\t</tr>\n";
												if (i==0) {table+="\t</thead>\n";}
												if (i==rows.length-1) {table+="\t</tbody>\n";}
											}
											else if (exp=='csvTable') {table+=rows[i].join(',')+"\n";}
											else if (exp=='tabTable' || exp=='textTable') {table+=rows[i].join("\t")+"\n";}
											else if (exp=='xmlTable') {
												if (i==0) {continue;}
												table+="\t<row>\n";
												for (var j=0;j<rows[0].length;j++) {
													table+="\t\t<field name='"+rows[0][j]+"'>"+rows[i][j].replace(/<\/?\w+.*?>/g,'')+"</field>\n";
												}
												table+="\t</row>\n";
											}
										}
										table = table.replace(/&nbsp;/g,' ').replace(/<\/?div.*?>/g,'');
										
										if (exp=='htmlTable') {
											if (exp=='html') {
												content+='<html><body><'
											}
											content+="<table cellpadding='0' cellspacing='0' border='1'>\n<caption style='caption-side: bottom; text-align: left;'><p><strong>"+this.localize('title')+"</strong>. "+this.localize('help')+'</p><p>'+this.getFooterText()+"</p></caption>"+table+"</table>\n";
										}
										else if (exp=='xmlTable') {
											content+="<?xml version='1.0'>\n<rows>\n"+table+"</rows>\n";
										}
										else if (exp=='tabTable' || exp=='csvTable') {
											// get rid of tags and commas in numbers (we may get rid of other commas we want, but oh well)
											content+=table.replace(/<\/?\w+.*?>/g,'').replace(/(\d),(\d)/g,'$1$2');
										}
										else {content+=table;}
									}
								}
								else if (exp=='citation') {
									msg+='<br clear="all" />'
									var cite = '<p>Sinclair, Stéfan and Geoffrey Rockwell. &ldquo;' +this.localize('title')+'.&rdquo; '+
										'<u>'+this.localize('title','app')+'</u>. '+
										new Date().format("j M. Y") +
										' &lt;'+url+'&gt;</p>'
									msg+=cite;
									content+=cite;
									cite = '<p>Sinclair, S. and G. Rockwell ('+new Date().format("Y")+'). '+
										this.localize('title')+'. <i>'+this.localize('title','app')+'</i>. '+
										'Retrieved '+new Date().format("F j, Y")+' from '+url+'</p>'
									msg+=cite;
									content+="\n\n"+cite;
								}
								else if (exp=='canvasPng') {
									msg = ' ';
									var canvas = this.getCanvasElement(this);
									var strData = canvas.toDataURL("image/png");
									content = '<img src="'+strData+'" />';
								}
								
								btn.findParentByType('window').close();
								
								if (msg) {
									
									// before we start, change the text area to monospace, nowrapping
									var textarea = Ext.DomQuery.selectNode('textarea',Ext.Msg.getDialog().body.dom);
									textarea.originalFontFamily=textarea.style.fontFamily;
									textarea.originalWrap=textarea.originalWrap;
									textarea.style.fontFamily='monospace';
									textarea.setAttribute('wrap','off');
	
									var msgBox = Ext.Msg.show({
									   title: this.localize('export','tool'),
									   msg:  '<p>'+this.localize('export','tool')+' '+this.exporters[exp]+'.</p>'+msg+"\n",
									   value: content,
									   width: Ext.getBody().getWidth()-50,
									   buttons: Ext.MessageBox.OK,
									   multiline: true,
									   icon: Ext.MessageBox.INFO,
									   fn: function() {
									   		textarea.style.fontFamily=textarea.originalFontFamily;
									   		textarea.setAttribute('wrap',textarea.originalWrap ? textarea.originalWrap : '');
									   		this.fireEvent('exportComplete', exp);
									   },
									   scope: this
									});
								}
							}
							,scope: panel
						}
					}
				},{
					text: this.localize('cancel','tool')
					,handler: function(btn) {
						btn.findParentByType('window').close();
					}
				}]
			},
			listeners: {
				destroy: function() {
					this.fireEvent('exportComplete');
				},
				scope: this
			}
		});
		this.showWindow(win);
	}
	
	,onOptions : function(ev, tool, panel) {
		if (panel.showOptions) {
			panel.showOptions.apply(panel, arguments)
		} else {
			this.alertInfo( {
				msg : this.localize('noOptions', 'tool')
			})
		}
	}
	
	,onHelp : function(ev, tool, panel) {
		console.warn(tool,panel)
		this.showWindow({
			title : this.localize('help', 'tool')
					+ this.localize('colon', 'app')
					+ this.localize('title'),
			msg : this.localize('help') + '<p><a href="http://hermeneuti.ca/voyeur/tools/'+this.xtype.replace(/^voyeur/,'')+'">'+this.localize('helpLink','tool')+'</a></p>',
			buttons : Ext.Msg.OK,
			icon : Ext.Msg.INFO,
			minWidth: 300
			
		}, true);
	}
	
	,onMore : function(ev, tool, panel) {
	}

	/**
	 * @method showOptions
	 * @description Overwrite to provide custom options for each subclass of Voyeur.Tool.
	 * Typically this will involve passing a Ext.Window config to showOptionsWindow.
	 */
	
	/**
	 * Shows the options window.
	 * @param {Object} config An Ext.Window config object.
	 * @param {Boolean} showStopWords True to add the commonly used stop words field.
	 * NB: You need to provide handling of the stop word field in your config object. 
	 */
	,showOptionsWindow : function(config, showStopWords) {
		showStopWords = showStopWords == null ? false : showStopWords;
		var renderTo = config.renderTo ? Ext.get(config.renderTo) : Ext.getBody();
		var width = renderTo.getWidth(true) - 40;
		width = Math.max(Math.min(width, 650), 350);
		Ext.applyIf(config, {
			renderTo : document.body
			,modal : true
			,title : this.localize('options', 'tool')
			,items: []
			,width: width
		});
		if (showStopWords) {
			config.items[0].items.unshift({
				xtype: 'compositefield',
			    fieldLabel : '<span ext:qtip="'
			        + this.localize('stopListTip','tool') + '">'
			        + this.localize('stopList','tool') + '</span>',
			    items:[{
			        xtype : 'combo',
			        id : 'stopList',
			        value : this.getApiParamValue('stopList'),
			        loadingText : this.localize('loading', 'tool'),
			        width : 200,
			        store : this.getApplication().getStopListsStore(),
			        selectOnFocus : true,
			        displayField: 'label',
			        valueField: 'id',
			        mode: 'local',
			        emptyText: this.localize('none','tool'),
			        listeners: {
			        	render: function(combo) {
			        		if (combo.getValue() != null) combo.nextSibling().enable();
			        	},
			            select: function(combo, record, index) {
			                combo.nextSibling().enable();
			            },
			            change: function(combo, newval, oldval) {
			                if (newval == '') combo.nextSibling().disable();
			            }
			        }
			    },{
			        xtype: 'button',
			        text: this.localize('viewStopWords', 'tool'),
			        disabled: true,
			        handler: function(button, event) {
			            var stopListId = button.previousSibling().getValue();
			            var win = new Ext.Window({
			                width: 200,
			                height: 400,
			                modal: false,
			                layout: 'fit',
			                tools: [{
			    				id : 'save',
			    				qtip: '<h3>' + this.localize('export', 'tool')
			    				+ this.localize('colon', 'app')
			    				+ this.localize('stopList', 'tool') + '</h3>'
			    				+ this.localize('exportTip','tool'),
			    				handler: function(ev, tool, panel, tc) {
			    					var stopList = panel.findByType('stopListView')[0];
			    					var grid = stopList.findByType('grid')[0];
			    					stopList.onExport.call(grid, ev, tool, grid);
			    				},
			    				scope: this
			    			}],
			                items: {
			                    xtype: 'stopListView',
			                    application: this.getApplication(),
			                    stopListId: stopListId
			                },
			                buttons: [{
			                    text: this.localize('ok', 'tool'),
			                    handler: function(b, e) {
			                        b.findParentByType('window').close();
			                    }
			                }]
			            });
			            win.show(button.getEl());
			        },
			        scope: this
			    },{
					xtype: 'checkbox',
					boxLabel: '<span ext:qtip="'
				        + this.localize('applyStopWordsGloballyTip','tool') + '">'
				        + this.localize('applyStopWordsGlobally','tool') + '</span>',
					id: 'globalStopWords',
					name: 'globalStopWords'
				}]
			});
		}
		config.items.unshift({
			border: false,
			html : '<div style="text-align: center; margin: .75em;">'+this.localize('hoverLabel','tool')+'</div>'
		});
		this.optionsWindow = new Ext.Window(config);
		this.showWindow(this.optionsWindow);
	},
	
	/**
	 * Hides the options window.
	 */
	hideOptionsWindow : function() {
		if (this.optionsWindow) {this.optionsWindow.close();}
	},
	
	showWindow : function(win, isMsg) {
		var availSpace = Ext.getBody().getBox();
		var dimensions;
		if (isMsg) {
			dimensions = {width: win.width || win.minWidth || 400, height: 100}
		} else {
			if (win.rendered) {
				dimensions = win.getBox();
			} else {
				dimensions = {width: win.initialConfig.width, height: win.initialConfig.height};
			}
		}
		if (dimensions.width > availSpace.width || dimensions.height > availSpace.height) {
			var newWin = confirm(this.localize('newWindow', 'tool'));
			if (newWin == true) {
				win.destroy();
				window.open(document.location.href, '_blank');
				return;
			}
		}
		if (isMsg) {
			Ext.Msg.show(win);
		} else {
			win.show();
		}
	}
	
	// this is meant to be overridden
	,getAdditionalExportParams: function() {
		return {};
	}

	/**
	 * Gets the corpus.
	 * @return {Voyeur.Corpus}
	 */
	,getCorpus : function() {
		return this.getApplication().getCorpus();
	}

	/**
	 * Gets the localization.
	 * @return {Voyeur.Localization}
	 */
	,getLocalization : function() {
		return this.getApplication().getLocalization();
	}

	,loadLocalization : function(loc) {
		this.getApplication().loadLocalization(loc, this.xtype + '.');
	}

	/**
	 * Localizes a string.
	 * @param {String} key The key of the string to localize.
	 * @param {String} [prefix] The prefix to use with the key (optional).
	 * @return {String} The localized string.
	 */
	,localize : function(key, prefix) {
		return this.getLocalization().get(
				(prefix ? prefix : this.xtype) + '.' + key);
	}

	/**
	 * Gets the Voyeur application.
	 * @return {Voyeur.Application}
	 */
	,getApplication : function() {
		return Voyeur.application;
	}

	/**
	 * Displays an error message.
	 * @param {Mixed} config Either a string, or an Ext.MessageBox.show config object.
	 */
	,alertError : function(config) {
		if (typeof config == "string") {
			config = {msg: config}
		}
		Ext.applyIf(config, {
			title : this.localize('error', 'app')+ " ("+this.localize('title')+")",
			buttons : Ext.Msg.OK,
			icon : Ext.Msg.ERROR,
			resizable: true,
			width: 600,
			height: 400
		})
		return Ext.MessageBox.show(config);
	}

	,handleAjaxError: function(response, options) {
		this.alertError({msg: response.responseText})
	}
	
	/**
	 * Displays a message.
	 * @param {Mixed} config Either a string, or an Ext.MessageBox.show config object.
	 */
	,alertInfo : function(config) {
		if (typeof config == "string") {
			config = {msg: config}
		}
		Ext.applyIf(config, {
			title : this.localize('info', 'app')+ " ("+this.localize('title')+")",
			buttons : Ext.Msg.OK,
			icon : Ext.Msg.INFO,
			width: 600,
			height: 400
		})
		return Ext.MessageBox.show(config);
	}

    /**
     * Initiates an Ajax call with progress bar.
     * @param {Object} args configuration options that may contain the following properties:<ul>
     * <li><b>progressWin</b> : Ext.ProgressBar<div class="sub-desc">(Optional) A {@link Ext.ProgressBar} to show progress (if no progressWin is specified, one will be created).</div></li>
     * <li><b>renderTo</b>: Mixed<div class="sub-desc">(Optional) A DOM id, DOM node or {@link Ext.Element} (same arguments as {@link Ext.get}. If this property is not provided then the document body will be used.</div></li>
     * <li><b>title</b>: String<div class="sub-desc">(Optional) The progress bar's title.</div></li>
     * <li><b>params</b>: Mixed<div class="sub-desc">(Optional) Parameters to be passed to the Ajax request, either as an object or as a URL encoded string (name=value&amp;name=value...)</div></li>
     * </ul>
     */
	,update : function(config) {
		this.lastUpdate = config;
		this.getApplication().update(config, this);
	}
	
	/**
	 * Reinitiate the previous update, optionally setting new parameter values (other arguments will remain the same).
	 * @param {Object} config an object representing override parameter values compared to the last update
	 */
	,reupdate : function(params) {
		Ext.apply(this.lastUpdate.params, params);
		this.update(this.lastUpdate);
	}

	/**
	 * Gets the Trombone URL.
	 * @return {String}
	 */
	,getTromboneUrl : function() {
		return this.getApplication().getTromboneUrl();
	}
	
	/**
	 * Gets the directory (URL) that this tool resides in.
	 * Useful for accessing tool-related resources.
	 * @return {String}
	 */
	,getToolDirectoryUrl: function() {
		return this.getApplication().getBaseUrl()+'resources/tools/'+this.xtype.replace(/^voyeur/,'')+'/';
	}
	
	,getFooterText : function(forElement) {
		forElement = forElement ? forElement : Ext.getBody();
		var availableWidth = forElement.getWidth();
		var parts = [
			this.localize('appLink','tool'),
			", <a href='http://stefansinclair.name/'>St&eacute;fan Sinclair</a> &amp; <a href='http://geoffreyrockwell.com'>Geoffrey Rockwell</a>",
			" (&copy;"+ new Date().getFullYear() +")",
			" v. "+this.getApplication().getVersion() + " ("+this.getApplication().getBuild()+")"
		];
		var footer = '';
		var footerWidth = 0;
		var partWidth;
		for (var i=0;i<parts.length;i++) {
			partWidth = forElement.getTextWidth(parts[i]);
			if (footerWidth+partWidth < availableWidth) {
				footer += parts[i];
				footerWidth += partWidth;
			}
		}
		return footer;
	}
	
	/**
	 * Gets a spark line.
	 * @param {Array} values An array of numerical values.
	 * @param {Integer} stretch The width to stretch the spark line towards (currently unused).
	 * @return {String} The image(s) of the spark line.
	 */
	,getSparkLine : function(values, stretch) {
		if (values.length<2) {return ''}
		var min = Number.MAX_VALUE;
		var max = Number.MIN_VALUE;
		var hasDecimal = false;
		for (var i=0;i<values.length;i++) {
			if (values[i]<min) {min=values[i];}
			if (values[i]>max) {max=values[i];}
			if (!hasDecimal && values[i].toString().indexOf('.')>-1) {hasDecimal=true;}
		}
		var dif = (max-min).toString();
		var multiplier = 1;
		var divider = 1;
		
		var newvalues = [];
		if (hasDecimal) {
			var multiplier = 100;
			var ind = dif.indexOf(".")+1;
			for (var i=ind;i<dif.length;i++) {
				if (dif.charAt(i)=='0') {multiplier*=10;}
				else {break;}
			}
			for (var i=0;i<values.length;i++) {
				newvalues[i]=parseInt(values[i]*multiplier);
			}
			max = parseInt(max*multiplier);
			min = parseInt(min*multiplier);
			
		}
		else {
			var divider = 1;
			for (var i=dif.length-1;i>-1;i--) {
				if (dif.charAt(i)=='0') {divider*=10;}
				else {break;}
			}
			if (divider!=1) {
				for (var i=0;i<values.length;i++) {
					newvalues[i]=values[i]/divider;
				}
				max /= divider;
				min /= divider;
			}
			else {
				newvalues=values;
			}
		}
		
		var valLen = (max.toString().length > min.toString().length ? max.toString().length : min.toString().length)+1;
		var valuesPerImage = Math.floor(1800/valLen);
		var baseUrl = 'http://chart.apis.google.com/chart?cht=ls&amp;chco=0077CC&amp;chls=1,0,0&amp;chds='+min+','+max
		var images = Math.ceil(values.length/valuesPerImage);
		var counter=0;
		var response = '';
		var wid;
		if (values.length<5) {wid=5;}
		else if (values.length<10) {wid=4;}
		else if (values.length<20) {wid=3;}
		else if (values.length<50) {wid=2;}
		else {wid=1;}

		/*if (stretch) {
			wid = Math.ceil(stretch/values.length);
			if (wid>5) {wid=5;}
		}*/

		for (var i=0;i<images;i++) {
			var vals = newvalues.slice(counter,counter+=valuesPerImage);
			response += "<img style='margin: 0; padding: 0;' border='0' src='"+baseUrl+'&amp;chs='+(wid*vals.length)+'x15&amp;chd=t:'+vals.join(',')+"' alt='' class='chart-";
			if (images==1) {response+='full';}
			else {
				if (i>0 && i+1<images) {response+'middle';}
				else if (i==0) {response+='left';}
				else {response+='right';}
			}
			response += "' />";
		}
		return response;
	}
	
	/**
	 * Gets the canvas element for this tool.
	 * @return {Element} The canvas element, or undefined if none is found.
	 */
	,getCanvasElement : function(panel) {
		return Ext.DomQuery.selectNode('canvas', panel.body.dom);
	}
});

Voyeur.localization.load( {
	'tool.help' : {
		en : 'Help'
	},
	'tool.helpTipClick' : {
		en : 'Click on the question mark icon for more help.'
	},
	'tool.helpLink' : {
		en: 'More help for this tool...'
	},
	'tool.viewResults' : {
		en : 'View {0} in Voyant.'
	},
	'tool.noResults' : {
		en : 'No results.'
	},
	'tool.noOptions' : {
		en : 'This tool does not define any user-controlled options.'
	},
	'tool.options' : {
		en : 'Options'
	},
	'tool.optionsTip' : {
		en : 'Click here to modify options for this tool.'
	},
	'tool.export' : {
		en : 'Export'
	},
	'tool.exportTip' : {
		en : 'Click here to export data from this tool.'
	},
	'tool.noExport' : {
		en : 'This tool does not provide any export options.'
	},
	'tool.more' : {
		en : 'More Tools'
	},
	'tool.moreTip' : {
		en : 'Click here to see other tools that are available.'
	},
	'tool.more' : {
		en : 'More Tools'
	},
	'tool.hoverLabel' : {
		en: 'Tip: Hover over the labels to see more information about each item.'
	},
	'tool.ok' : {
		en : 'OK'
	},
	'tool.cancel' : {
		en : 'Cancel'
	},
	'tool.restore' : {
		en : 'Defaults'
	},
	'tool.exportUrl' : {
		en: 'a URL for this tool and current data'
	},
	'tool.clickUrl' : {
		en: 'Open this <a href="{0}" target="_blank">URL</a> in a new window.'
	},
	'tool.exportIframe' : {
		en: 'an HTML snippet to embed this tool and current data'
	},
	'tool.exportButton' : {
		en: 'an HTML button for this tool and current data'
	},
//	'tool.exportHtml' : {
//		en: 'a simple HTML page with static tabular data'
//	},
	'tool.exportHtmlTable' : {
		en: 'an HTML snippet with static tabular data'
	},
	'tool.exportTextTable' : {
		en: 'tabular data as plain text'
	},
	'tool.exportCsvTable' : {
		en: 'current data as comma-separated values'
	},
	'tool.exportTabTable' : {
		en: 'current data as tab-separated values'
	},
	'tool.exportXmlTable' : {
		en: 'current data as XML'
	},
	'tool.exportContentTip': {
		en: 'Select all content in the box below and copy it into the clipboard.'
	}
	,'tool.exportCitation' : {
		en: 'a bibliographic citation for this tool'
	},
	'tool.exportCanvasPng' : {
		en: 'a PNG image of the visualization'
	},
	'tool.apiWarn' : {
		en: 'Please note that this is an early version and the API may change.\n\tYou are strongly encouraged to subscribe to a list to receive notifications\n\tof updates to Voyant (updated code, planned outages, etc.) – please send\n\ta message to sgsinclair@voyeurtools.org.'
	},
	'tool.appLink' : {
		en: "<a href='http://hermeneuti.ca/voyeur/' target='_blank'>Voyant Tools</a>"
	},
	'tool.extendedSortZscoreMinimum' : {
			en : 'Minimum Z-Score'
	},
	'tool.extendedSortZscoreMinimumTip' : {
			en : 'This defines the minimum z-score (up to two decimals) for a corpus type. The default value of 1 helps to ensure that only higher frequency terms are kept.'
	},
	'tool.stopList' : {
		en: 'Stop Words List'
	},
	'tool.stopListTip' : {
		en: 'Stop words are usually high frequency words that carry little meaning, such as articles (the, a, etc.) and prepositions (of, to, etc.). You can select a pre-existing list from the menu, specify a URL for a list of stop words in plain text format (one per line), or provide a custom list with words separated by commas.'
	},
	'tool.viewStopWords' : {
		en: 'View Stop Words'
	},
	'tool.applyStopWordsGlobally' : {
		en: 'Apply Stop Words Globally'
	},
	'tool.applyStopWordsGloballyTip' : {
		en: 'Applies the selected stop words to all the tools.  NB: This will take effect on all proceeding actions carried out with the tools.'
	},
	'tool.filtersInEffect' : {
		en: 'Filters in Effect (see options):'
	},
	'tool.none' : {
		en : 'none'
	},
	'tool.searchText' : {
		en : 'Search'
	},
	'tool.newWindow' : {
		en : "There isn't enough room to display the window in the current space.  Would you like to open a new window?"
	}
});
Ext.namespace('Voyeur.data');

Ext.namespace('Voyeur.data.Corpus');

Voyeur.data.Corpus.fields = [
	{name: 'id', mapping: '@id'}
	,{name: 'lastModified', mapping: '@lastModified'}
	,{name: 'totalWordTokens', type: 'int', mapping: '@totalWordTokens'}
	,{name: 'totalWordTypes', type: 'int', mapping: '@totalWordTypes'}
	,{name: 'docsCount', type: 'int', mapping: 'documents', convert: function(val, record) {return val.length}}
]

Ext.namespace('Voyeur.data.Document');

Voyeur.data.Document.fields = [
	{name: 'id', mapping: '@id'}
	,{name: 'index', type: 'int', mapping: '@index'}
	,{name: 'title', mapping: '@title'}
	,{name: 'shortTitle', mapping: '@title', convert: function(val) {
		title = val.replace(/\.(html?|txt|xml|docx?|pdf|rtf|\/)$/,'');
		if (title.length > 25) {
			
				 // maybe a file or URL?
				var slash = title.lastIndexOf("/");
				if (slash>-1) {title = title.substr(slash+1)}
				
				if (title.length>25) {
					var space = title.indexOf(" ", 20);
					if (space < 0 || space > 30) {
						space = 25;
					}
					title = title.substring(0, space) + "…;";					
				}
		}
		return title;
		
	}}
	,{name: 'author', mapping: '@author'}
	,{name: 'timeInMillis', type: 'int', mapping: '@time'}
	,{name: 'totalTokens', type: 'int', mapping: '@totalTokens'}
	,{name: 'totalWordTokens', type: 'int', mapping: '@totalWordTokens'}
	,{name: 'totalWordTypes', type: 'int', mapping: '@totalWordTypes'}
	,{name: 'wordDensity', type: 'float', mapping: '@totalWordTokens', convert: function(val, record) {return val < 1 ? 0 : (record['@totalWordTypes']/val)*1000;}}
]

Ext.namespace('Voyeur.data.CorpusTypes');

Voyeur.data.CorpusTypes.fields = [
	{name : 'type', 	mapping : '@type'}
	,{name : 'rawFreq', 	mapping : '@rawFreq', 	type : 'int'}
	,{name : 'relativeMin', 	mapping : '@relativeDistributionMin', 	type : 'float'}
	,{name : 'relativeMax', 	mapping : '@relativeDistributionMax', 	type : 'float'}
	,{name : 'relativeDistributionMean', 	mapping : '@relativeDistributionMean', 	type : 'float'}
	,{name : 'relativeDistributionStdDev', 	mapping : '@relativeDistributionStdDev', 	type : 'float'}
	,{name : 'relativeDistributionKurtosis', 	mapping : '@relativeDistributionKurtosis', 	type : 'float'}
	,{name : 'relativeDistributionSkewness', 	mapping : '@relativeDistributionSkewness', 	type : 'float'}
	,{name : 'rawZscore', 	mapping : '@rawZscore', 	type : 'float'}
	,{name : 'rawZscoreDifferenceCorpusComparison', 	mapping : '@rawZscoreDifferenceCorpusComparison', 	type : 'float'}
	,{name : 'relativeFreqs', 	mapping : 'relativeFreqs', convert: function(val){return val['float-array']}}
	,{name : 'rawFreqs', 	mapping : 'rawFreqs', convert: function(val){return val['int-array']}}
]

Ext.namespace('Voyeur.data.DocumentTypes');

Voyeur.data.DocumentTypes.fields = [
	{name:'type',mapping:'@type'}
	,{name:'docId',mapping:'@docId'}
	,{name:'docIndex',mapping:'@docIndex',type:'int'}
	,{name:'rawFreq',mapping:'@rawFreq',type:'int'}
	,{name:'rawZscore',mapping:'@rawZscore',type:'float'}
	,{name:'rawZscoreCorpusDelta',mapping:'@rawZscoreCorpusDelta',type:'float'}
	,{name:'relativeFreqCorpusDelta',mapping:'@relativeFreqCorpusDelta',type:'float'}
	,{name:'relativeFreq',mapping:'@relativeFreq',type:'float'}
	,{name:'rawDistributionMin',mapping:'@rawDistributionMin',type:'int'}
	,{name:'rawDistributionMax',mapping:'@rawDistributionMax',type:'int'}
	,{name:'rawDistributionMean',mapping:'@rawDistributionMean',type:'float'}
	,{name:'rawDistributionStdDev',mapping:'@rawDistributionStdDev',type:'float'}
	,{name:'rawDistributionKurtosis',mapping:'@rawDistributionKurtosis',type:'float'}
	,{name:'rawDistributionSkewness',mapping:'@rawDistributionSkewness',type:'float'}
	,{name:'distributionFreqs',mapping:'int-array'}
	,{name : 'relativeFreqs', 	mapping : 'relativeFreqs', convert: function(val){return val['float-array']}}
	,{name : 'rawFreqs', 	mapping : 'rawFreqs', convert: function(val){return val['int-array']}}
]

Ext.namespace('Voyeur.data.DocumentTypeCollocates');

Voyeur.data.DocumentTypeCollocates.fields = [
 	{name: 'type', mapping: '@type'}
	,{name: 'keyword', mapping: '@keyword'}
	,{name: 'docId', mapping: '@docId'}
	,{name: 'docIndex', mapping: '@docIndex',type:'int'}
	,{name: 'keyword_docId', mapping: '@docId', convert: function(val, record) {return record['keyword']+': '+val}}
	,{name: 'rawFreq', mapping: '@rawFreq', type: 'int'}
	,{name: 'relativeFreq', mapping: '@relativeFreq', type: 'float'}
	,{name: 'rawCollocateFreq', mapping: '@rawCollocateFreq', type: 'int'}
	,{name: 'relativeCollocateFreq', mapping: '@relativeCollocateFreq', type: 'float'}
	,{name: 'rawCollocateFreqRatio', mapping: '@rawCollocateFreqRatio', type: 'float'}
]

Ext.namespace('Voyeur.data.TypeKwics');

Voyeur.data.TypeKwics.fields = [
	{name : 'type',mapping:'@type'},
	{name : 'left',mapping:'@left'},
	{name : 'middle',mapping:'@middle'},
	{name : 'right',mapping:'@right'},
	{name : 'docId', mapping:'@docId'},
	{name : 'docIndex',mapping:'@docIndex',type : 'int'},
	{name : 'tokenId', mapping:'@tokenId',type : 'int'},
	{name : 'category' /* for KwicTagger */}
]

Voyeur.data.TypeKwics.reader = new Ext.data.JsonReader({
	root : 'typeKwics.kwics'
	,totalProperty : 'typeKwics["@totalKwics"]'
	,fields: Voyeur.data.TypeKwics.fields
});

Ext.namespace('Voyeur.data.CorpusEntities');

Voyeur.data.CorpusEntities.fields = [
	{name : 'text',mapping:'@text'},
	{name : 'category',mapping:'@category'},
	{name : 'rawFreq',mapping:'@rawFreq', type: 'int'},
	{name : 'relevances',mapping:'relevances/float-array'},
	{name : 'counts',mapping:'counts/float-array'},
	{name : 'disambiguate', mapping:'@disambiguate'}                
]

Voyeur.data.CorpusEntities.reader = new Ext.data.JsonReader({
	root : 'corpusEntities.entities'
	,totalProperty : 'corpusEntities["@totalEntities"]'
	,fields: Voyeur.data.CorpusEntities.fields
});

Ext.namespace('Voyeur.data.DocumentEntities');

Voyeur.data.DocumentEntities.fields = [
   	{name : 'text',mapping:'@text'},
	{name : 'docId',mapping:'@docId'},
	{name : 'category',mapping:'@category'},
	{name : 'rawFreq',mapping:'@rawFreq', type: 'int'},
	{name : 'relevance',mapping:'@relevance', type: 'float'},
	{name : 'disambiguate', mapping:'@disambiguate'}                
]

Voyeur.data.DocumentEntities.reader = new Ext.data.JsonReader({
	root : 'documentEntities.entities'
	,totalProperty : 'documentEntities["@totalEntities"]'
	,fields: Voyeur.data.DocumentEntities.fields
});
Ext.ux.DocumentSelector = Ext.extend(Ext.Button, {
	
	singleSelect: false,
	
	max: null,
	
	itemClicked: false,
	
	initComponent: function() {
		
		this.singleSelect = this.initialConfig.singleSelect == null ? this.singleSelect : this.initialConfig.singleSelect;
		this.max = this.max || this.initialConfig.max;
		
		var config = {
			text: this.initialConfig.text || Voyeur.localization.get('documents'),
			menu: new Ext.menu.Menu({
				items: [],
				listeners: {
					beforehide: function(component) {
						if (this.itemClicked) {
							this.itemClicked = false;
							return false;
						} else {
							this.itemClicked = false;
							return true;
						}
					},
					itemclick: function(item, event) {
						this.itemClicked = true;
						if (item.initialConfig.docId) {
							var menu = item.parentMenu;
							menu.getComponent('selectAll').setChecked(false);
							menu.getComponent('selectNone').setChecked(false);
							if (this.singleSelect) {
								menu.items.each(function(i, index, length) {
									if (i.initialConfig.docId && i.initialConfig.docId != item.initialConfig.docId) {
										i.setChecked(false);
									}
								}, this);
							}
						} else if (item.itemId == 'selectAll') {
							var menu = item.parentMenu;
							var count = 0;
							menu.items.each(function(item, index, length) {
								if (item.initialConfig.docId) {
									count++;
									if (this.max && count > this.max) {
										item.setChecked(false);
									} else {
										item.setChecked(true);
									}
								}
							}, this);
							menu.getComponent('selectNone').setChecked(false);
						} else if (item.itemId == 'selectNone') {
							var menu = item.parentMenu;
							menu.items.each(function(item, index, length) {
								if (item.initialConfig.docId) item.setChecked(false);
							}, this);
							menu.getComponent('selectAll').setChecked(false);
						}
						var docs = [];
						item.parentMenu.items.each(function(i, index, length) {
							if (item.initialConfig.docId && item.initialConfig.docId == i.initialConfig.docId) {
								// check value will be the opposite, since the change hasn't been registered yet
								if (!i.checked) docs.push(i.initialConfig.docId);
							} else if (i.checked) {
								docs.push(i.initialConfig.docId);
							}
						}, this);
						if (this.max) {
							if (docs.length >= this.max) {
								item.parentMenu.items.each(function(i, index, length) {
									if (i.initialConfig.docId) {
										if (docs.indexOf(i.initialConfig.docId) == -1) {
											i.disable();
										}
									}
								}, this);
							} else {
								item.parentMenu.items.each(function(i, index, length) {
									if (i.initialConfig.docId) i.enable();
								}, this);
							}
						}
						this.fireEvent('documentsSelected', docs, this);
					},
					scope: this
				}
			})
		};
		
		Ext.apply(this, config);
        
        Ext.ux.DocumentSelector.superclass.initComponent.call(this);
        
        this.addEvents('documentsSelected');
	},
	
	populate: function(docIds, replace) {
		if (replace) {
			this.menu.removeAll();
		}
		
		this.menu.addMenuItem({
			xtype: 'menucheckitem',
			itemId: 'selectAll',
			text: Voyeur.localization.get('selectAll'),
			disabled: this.singleSelect,
			checked: false
		});
		this.menu.addMenuItem({
			xtype: 'menucheckitem',
			itemId: 'selectNone',
			text: Voyeur.localization.get('selectNone'),
			checked: false
		});
		this.menu.addSeparator();
		
		if (this.singleSelect && docIds && docIds.length > 1) {
			docIds = [docIds[0]];
		} else if (this.max && docIds && docIds.length > this.max) {
			docIds.splice(this.max, docIds.length - this.max);
		}
		Voyeur.application.getCorpus().getDocuments().each(function(doc, index, length) {
			var docId = doc.getId();
			var checked = false;
			var disabled = false;
			if (this.singleSelect && index == 0 && docIds == null) {
				checked = true;
			} else if (docIds == null || docIds.indexOf(docId) != -1) {
				checked = true;
			}
			if (this.max && index >= this.max) {
				disabled = true;
			}
			var menuItem = {
				xtype: 'menucheckitem',
				text: doc.getShortTitle(),
				checked: checked,
				disabled: disabled,
				docId: docId
			};
			if (this.singleSelect) menuItem.group = 'docs';
			this.menu.addMenuItem(menuItem);
		}, this);
		
		this.menu.addSeparator();
		this.menu.addMenuItem({
			xtype: 'button',
			style: 'margin: 0 auto;',
			text: Voyeur.localization.get('Ok'),
			handler: function(item, event) {
//				var docs = [];
//				item.ownerCt.items.each(function(item, index, length) {
//					if (item.initialConfig.docId && item.checked) {
//						docs.push(item.initialConfig.docId);
//					}
//				}, this);
//				this.fireEvent('documentsSelected', docs, this);
				item.ownerCt.hide();
			},
			scope: this
		});
	}
});

Ext.reg('documentSelector', Ext.ux.DocumentSelector);

Voyeur.localization.load({
	documents: {en: 'Documents'},
	selectAll: {en: 'Select All'},
	selectNone: {en: 'Select None'}
});
Ext.ux.TypeSearch = Ext.extend(Ext.form.ComboBox, {	
	
	useLowerCase: true,
	
	initComponent: function() {
		var baseParams = {
			tool: 'CorpusTypeFrequencies',
			limit: 10 
		};
		if (this.initialConfig.stopList) {
			baseParams.stopList = this.initialConfig.stopList;
		}
		if (this.initialConfig.useLowerCase) {
			this.useLowerCase = this.initialConfig.useLowerCase;
		}
		var config = {
			emptyText: this.initialConfig.emptyText || Voyeur.localization.get('tool.searchText'),
		    mode: 'remote',
		    displayField: 'type',
		    enableKeyEvents: true,
			autoSelect: false,
			minChars: 2,
			triggerAction: 'all',
        	store: new Ext.data.JsonStore({
	    		root: 'corpusTypes.types',
	    		totalProperty: 'corpusTypes["@totalTypes"]',
	    		remoteSort: true,
	    		fields: Voyeur.data.CorpusTypes.fields,
	    		proxy: new Ext.data.HttpProxy({
					url: this.initialConfig.parentTool.getTromboneUrl(),
					timeout: 60000,
					listeners: {
						'beforeload': {
							fn: function(proxy, params) {
								if (this.getCorpus().getSize()==0) {return false;}
								params.corpus = this.getApiParamValue('corpus');
								if (this.useLowerCase) {
									params.query = params.query.toLowerCase();
								}
								if (!params.noAnchor) {
									params.query = "\\b"+params.query.toLowerCase();
								}
							},
							scope: this.initialConfig.parentTool
						}
					}
				}),
	    		baseParams: baseParams,
	    		listeners: {
	    			load: {
	    				fn: function(store, records, options) {
	    					if (records.length==0 && options.params.query.indexOf("\\b")==0 && !options.params.noAnchor) {
	    						options.params.noAnchor=1;
	    						options.params.query = options.params.query.substring(2);
	    						store.reload({query: options.params.query.substring(2), noAnchor: 1})
	    					}
	    				},
	    				scope: this
	    			}
	    		}
	    	}),
	    	listeners: {
	    		beforequery: {
	    			fn: function(queryObj) {
	    				if (queryObj.query == '') {
	    					var text = queryObj.combo.getValue();
	    					if (this.useLowerCase) {
	    						text = text.toLowerCase();
	    					}
	    					if (text != '') queryObj.query = text;
	    				}
	    			},
	    			scope: this
	    		},
				select: {
					fn: function(combo, record, index){
						var type = record.get('type');
						this.fireEvent('typeSelected', combo, type, record);
					},
					scope: this
				},
				keyup: {
					fn: function(combo, event) {
	    				if (event.getCharCode() == event.ENTER) {
	    					var type = combo.getRawValue();
	    					if (this.useLowerCase) {
	    						type = type.toLowerCase();
	    					}
	    					var store = combo.getStore();
	    					var index = store.findExact('type', type);
	    					var record = null;
	    					if (index >= 0) {
	    						record = store.getAt(index);
	    						this.fireEvent('typeSelected', combo, type, record);
	    					} else {
	    						store.load({
	    							params: {
	    								type: type,
	    								noAnchor: true,
	    								query: null,
	    								limit: 1
	    							},
	    							callback: function(r, options, success) {
	    								if (success && r.length > 0) {
	    									record = r[0];
	    									this.fireEvent('typeSelected', combo, type, record);
	    								}
	    							},
	    							scope: this
	    						});
	    					}
	    				}
					},
					scope: this
				}
			}
        };
        Ext.apply(this, config);
        Ext.apply(this.initialConfig, config);
        
        Ext.ux.TypeSearch.superclass.initComponent.call(this);
        this.addEvents('typeSelected');
    }
});

Ext.reg('typeSearch', Ext.ux.TypeSearch);
Ext.ux.Translate = Ext.extend(Ext.form.ComboBox, {	
	
	initComponent: function() {
		var config = {
			emptyText: this.initialConfig.emptyText || Voyeur.localization.get('languages'),
		    mode: 'local',
		    displayField: 'lang',
		    valueField: 'code',
		    enableKeyEvents: true,
			autoSelect: false,
			minChars: 2,
			triggerAction: 'all',
        	store: this.initialConfig.parentTool.getLocalization().langCodesStore,
	    	listeners: {
				select: {
					fn: function(combo, record, index) {
						var code = record.get('code');
						var lang = this.store.getById(code).get('lang');
						var docIndex = this.initialConfig.docIndex;
						if (docIndex == null) {
							docIndex = this.initialConfig.parentTool.getApiParamValue('docIndex') || 0;
						}
						this.fireEvent('loading', {code: code, lang: lang}, this);
						Ext.Ajax.request({
							url: Voyeur.application.getTromboneUrl(),
							params: {
								tool: 'Translator',
								referrer: Voyeur.application.getBaseUrl(),
								source: this.initialConfig.source || Voyeur.localization.getLang(),
								target: code,
								corpus: this.initialConfig.parentTool.getCorpus().getId(),
								docIndex: docIndex
							},
							callback: function(options, success, response) {
								var json = Ext.decode(response.responseText);
								if (success) {
									var t = json.translator.texts[0];
									var text = t['@text'];
									this.fireEvent('translationResults', {text: text, code: code, lang: lang}, this);
								}
							},
							scope: this
						});
					},
					scope: this
				}
			}
        };
        Ext.apply(this, config);
        Ext.apply(this.initialConfig, config);
        
        Ext.ux.Translate.superclass.initComponent.call(this);
        
        this.addEvents('translationResults', 'loading');
    }
});

Ext.reg('translate', Ext.ux.Translate);

Voyeur.localization.load({
	languages: {en: 'Languages'}
});
Ext.ux.StopListView = Ext.extend(Ext.Panel, {
	initComponent: function() {
		var config = {
			layout: 'anchor',
			border: false,
			items: [{
				anchor: '100%',
				xtype: 'textfield',
        		enableKeyEvents: true,
        		emptyText: Voyeur.localization.get('tool.searchText'),
        		listeners: {
        			keyup: function(textfield, event) {
        				var q = textfield.getRawValue();
        				textfield.nextSibling().getStore().filter('stopword', q, true);
        			},
        			scope: this
        		}
			},{
				anchor: '100% -22',
				xtype: 'grid',
				colModel: new Ext.grid.ColumnModel([{
					header: Voyeur.localization.get('tool.stopList'),
					dataIndex: 'stopword'
				}]),
				viewConfig: {
					forceFit: true
				},
				store: new Ext.data.ArrayStore({
		    		fields: [{name:'stopword', mapping: 0}]
				}),
				emptyText: Voyeur.localization.get('tool.noResults'),
				listeners: {
					render: function(list) {
						Ext.Ajax.request({
							url: this.initialConfig.application.getTromboneUrl(),
							params: {
								corpus: this.initialConfig.application.getCorpus().getId(),
								outputFormat: 'json',
								stopList: this.initialConfig.stopListId,
								includeStopList: true
							},
							success: function(response) {
								var json = Ext.decode(response.responseText);
								var stopwords = json.stopListImporter.stopwords;
								for (var i = 0; i < stopwords.length; i++) {
									stopwords[i] = [stopwords[i]];
								}
								list.getStore().loadData(stopwords);
							},
							scope: this
						});
					},
					scope: this
				}
			}]
		};
		Ext.apply(this, config);
		Ext.ux.StopListView.superclass.initComponent.call(this);
	},
	
	onExport: function(ev, tool, panel) {
		var exporters = {
			htmlTable: Voyeur.localization.get('tool.exportHtmlTable'),
			textTable: Voyeur.localization.get('tool.exportTextTable'),
			csvTable: Voyeur.localization.get('tool.exportCsvTable'),
			tabTable: Voyeur.localization.get('tool.exportTabTable'),
			xmlTable: Voyeur.localization.get('tool.exportXmlTable')
		};
		var items = [];
		for (var k in exporters) {
			items.push({
				inputValue: k,
				boxLabel: exporters[k],
				name: 'exp',
				checked: false
			});
		}
		
		var win = new Ext.Window({
			title: 'Export',
			modal: true,
			width: 370,
			items: {
				xtype: 'form',
				labelAlign: 'top',
				bodyStyle: 'padding:0 10px 0;',
				items: {
					xtype: 'radiogroup'
					,fieldLabel: ''
					,hideLabel: true
					,items: items
					,columns: 1
				}
				,buttons: [{
					text: Voyeur.localization.get('tool.ok'),
					listeners: {
						click: {
							fn: function(btn) {
								var exp = btn.findParentByType('form').getForm().getValues(false).exp;
								var content = '';
								var msg = '<p>'+Voyeur.localization.get('tool.exportContentTip')+'</p>'; 
								var columnModel = this.getColumnModel();
								var view = this.getView();
								var rowCount = view.getRows().length;
								var rows = [columnModel.getColumnHeader(0)];
								for (var i=0;i<rowCount;i++) {
									rows.push(view.getCell(i,0).innerHTML);
								}
								var val;
								var table = "";
								if (exp=='textTable') {
									var longest = new Array(1);
									for (var i=0;i<rows.length;i++) {
										val = rows[i].replace(/&nbsp;/g,' ').replace(/<\/?\w+.*?>/g,'');
										rows[i] = val;
										if (!longest[i] || longest[i]<val.length) {longest[i]=val.length;}
									}
									for (var i=0;i<rows.length;i++) {
										rows[i] = String.leftPad(rows[i],longest[i],' ');
									}
								}
								if (exp=='htmlTable') {
									for (var i=0;i<rows.length;i++) {
										if (i==0) {table+="\t<thead>\n";}
										if (i==1) {table+="\t<tbody>\n";}
										table+="\t\t<tr>\n\t\t\t<td>"+rows[i]+"</td>\n\t\t</tr>\n";
										if (i==0) {table+="\t</thead>\n";}
										if (i==rows.length-1) {table+="\t</tbody>\n";}
									}
								}
								else if (exp=='csvTable') table+=rows.join(',');
								else if (exp=='tabTable' || exp=='textTable') table+=rows.join("\t");
								else if (exp=='xmlTable') {
									for (var i=0;i<rows.length;i++) {
										if (i==0) {continue;}
										table+="\t<row>\n";
										table+="\t\t<field name='"+rows[0]+"'>"+rows[i].replace(/<\/?\w+.*?>/g,'')+"</field>\n";
										table+="\t</row>\n";
									}
								}
								table = table.replace(/&nbsp;/g,' ').replace(/<\/?div.*?>/g,'');
								
								if (exp=='htmlTable') {
									if (exp=='html') {
										content+='<html><body><'
									}
									content+="<table cellpadding='0' cellspacing='0' border='1'>\n<caption style='caption-side: bottom; text-align: left;'><p><strong>"+Voyeur.localization.get('tool.stopList')+"</strong>. </p></caption>"+table+"</table>\n";
								}
								else if (exp=='xmlTable') {
									content+="<?xml version='1.0'>\n<rows>\n"+table+"</rows>\n";
								}
								else {content+=table;}
								
								btn.findParentByType('window').close();
								
								// before we start, change the text area to monospace, nowrapping
								var textarea = Ext.DomQuery.selectNode('textarea',Ext.Msg.getDialog().body.dom);
								textarea.originalFontFamily=textarea.style.fontFamily;
								textarea.originalWrap=textarea.originalWrap;
								textarea.style.fontFamily='monospace';
								textarea.setAttribute('wrap','off');

								var msgBox = Ext.Msg.show({
								   title: Voyeur.localization.get('tool.export'),
								   msg:  '<p>'+Voyeur.localization.get('tool.export')+' '+exporters[exp]+'.</p>'+msg+"\n",
								   value: content,
								   width: Ext.getBody().getWidth()-50,
								   buttons: Ext.MessageBox.OK,
								   multiline: true,
								   icon: Ext.MessageBox.INFO,
								   fn: function() {
								   		textarea.style.fontFamily=textarea.originalFontFamily;
								   		textarea.setAttribute('wrap',textarea.originalWrap ? textarea.originalWrap : '');
								   },
								   scope: this
								});
							}
							,scope: panel
						}
					}
				},{
					text: Voyeur.localization.get('tool.cancel')
					,handler: function(btn) {
						btn.findParentByType('window').close();
					}
				}]
			}
		});
		
		win.show(tool.id);
	}
});

Ext.reg('stopListView', Ext.ux.StopListView);
/**
 * @class Voyeur.Tool.WordCountFountain 
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.WordCountFountain = Ext.extend(Ext.Panel, {
	constructor : function(config) {
	
		// inherit Voyeur Tool
		Ext.apply(this, new Voyeur.Tool(config, this))

		// call superclass
		Voyeur.Tool.WordCountFountain.superclass.constructor.apply(this, arguments);
		
		// create applet when the corpus is loaded
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function() {
			if (this.rendered) {this.fireEvent('afterrender', this);}
		}, this);
		
		// create applet when the corpus is loaded
		this.addListener('afterrender', function() {
			
			var content = '';
			
			var corpus = this.getCorpus();
			var size = corpus.getSize();
			if (size>0) {
				
				// set core parameters
				var params = this.getApiParams();
				
				// check to make sure we don't have too many words
				if (size>1 && !params.docIndex && !params.docId) {
					var docIndex = [];
					var words = 0;
					for (var i=0;i<size;i++) {
						var doc = corpus.getDocument(i);
						docIndex.push(doc.getIndex())
						words += doc.get('totalWordTokens');
						if (words>100000) {
							params.docIndex = docIndex;
							break;
						}
					}
				}
				
				// create applet
				content = '<div align="center"><applet code="ca.hermeneuti.processing.wordcountfountain.WordCountFountain" archive="'+this.getToolDirectoryUrl()+'WordCountFountain.jar" width="1200" height="600">'+
					'<param name="source" value="'+this.getTromboneUrl()+'?tool=DocumentExporter&outputFormat=text&template=docExport2plainText&' + Ext.urlEncode(params)+ '" />'+
					'<param name="title" value="'+this.localize('title')+' in '+this.localize('title','app')+'" />'+
					'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet><div>'+this.localize('adaptedFrom')+'</div></div>'
			}
			
			// update body contents	with applet
			this.body.update(content);
			
		}, this)
	}

	,api: {
		toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded']
	}
	
	,thumb: {
		large: 'WordCountFountain.png'
	}

	// localization variables
	,i18n : {
		title : {en: "Word Count Fountain"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool visualizes word frequencies as a fountain."}
		,adaptedFrom: {en: "Word Count Fountain is adapted from <a href=\"http://iragreenberg.com/\" target=\"_blank\">Ira Greenberg</a>'s <a href=\"http://iragreenberg.com/poetess/viz01/\" target=\"_blank\">work</a> with <a href=\"http://www.users.muohio.edu/mandellc/\" target=\"_blank\">Laura Mandell</a>."}
	}
});

Ext.reg('voyeurWordCountFountain', Voyeur.Tool.WordCountFountain);

/**
 * @class Voyeur.Tool.NetVizApplet A wrapper for the NetVizApplet
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.NetVizApplet = Ext.extend(Ext.Panel, {
	constructor : function(config) {
	
		Ext.useShims = true;
		
		// inherit Voyeur Tool
		Ext.apply(this, new Voyeur.Tool(config, this))

		// call superclass
		Voyeur.Tool.NetVizApplet.superclass.constructor.apply(this, arguments);
		
		// create applet when the corpus is loaded
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function() {
			if (this.rendered) {this.fireEvent('afterrender', this);}
		}, this);
		
		// create applet when the corpus is loaded
		this.addListener('afterrender', function() {
			
			var content = '';
			
			var corpus = this.getCorpus();
			var size = corpus.getSize();
			if (size>0) {
				
				// set core parameters
				var params = this.getApiParams();
				
				// check to make sure we don't have too many words
				if (size>1 && !params.docIndex && !params.docId) {
					var docIndex = [];
					var words = 0;
					for (var i=0;i<size;i++) {
						var doc = corpus.getDocument(i);
						docIndex.push(doc.getIndex())
						words += doc.get('totalWordTokens');
						if (words>100000) {
							params.docIndex = docIndex;
							break;
						}
					}
				}

				// create applet
				content = '<div align="center">'+
					'<applet code="components.GUI" archive="'+this.getToolDirectoryUrl()+'NetVizApplet.jar" codebase="'+this.getToolDirectoryUrl()+'" width="900" height="660">'+
					'<param name="source" value="'+this.getToolDirectoryUrl()+'southey_VIZ.xml" />'+
					'<param name="sourceType" value="url" />'+
					'<param name="title" value="'+this.localize('title')+' in '+this.localize('title','app')+'" />'+
					'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet><div>'+this.localize('adaptedFrom')+'</div></div>'
			}
			
			// update body contents	with applet
			this.body.update(content);
			
		}, this)
	}

	,api: {
		toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded']
	}
	
	,thumb: {
		large: 'NetVizApplet.png'
	}

	// localization variables
	,i18n : {
		title : {en: "NetVizApplet"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool visualizes connections in a network of related entities."}
		,adaptedFrom: {en: ""}
	}
});

function showImage(image) {
	window.open('data:image/png;base64,'+image)
}

Ext.useShims = true;

Ext.reg('voyeurNetVizApplet', Voyeur.Tool.NetVizApplet);

/**
 * @class Voyeur.Tool.Links A simple panel which loads the Links app.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.Links = Ext.extend(Ext.Panel, {
    initialized: false,
    getLinksObjectId : function() {return this.id.replace(/-/g,'_')+'_links';},
    // tracks the document ids that have been added
    docIds: [],
    // configuration for the flash app
    flashvars : {
        bridgeName: 'links',
        autoFit: true,
        valueSizing: true,
        removeOrphans: true,
        groupsOpen: true
    },
    initLinks: function() {
        if (!this.initialized) {
            var id = this.getLinksObjectId();
var scripts = '<script type="text/javascript">'+
'function linksClickHandler(word, docIds, event) {'+
'if (window.console && console.info) console.info(word, event.type);'+
'var linksTool = Ext.ComponentMgr.all.find(function(object){if (object.xtype==\'voyeurLinks\') {return true;} else {return false;}});'+
'linksTool.linksClickHandler(word, docIds, event);'+
'}'+
'</script>';
            this.body.update(scripts+'<div id="'+id+'"></div>', true);
            var params = {
                menu: "false",
                scale: "noScale",
                allowFullscreen: "true",
                allowScriptAccess: "always",
                bgcolor: "#FFFFFF",
                wmode: 'opaque'
            };
            var attributes = {
                id: id,
                name: id
            };
            swfobject.embedSWF(this.getApplication().getBaseUrl()+"resources/lib/links/Links.swf", id,
                '100%', '100%', "10.0.0", "expressInstall.swf", this.flashvars, params, attributes);
            this.initialized = true;
        }
    },
    linksClickHandler: function(word, docIds, event) {
        if (event.type == 'doubleClick') {
            this.addWord(word);  // use add word so we can get frequency info first
            
            if (docIds == null) Voyeur.application.dispatchEvent('corpusTypeSelected', this, {type: word});
            else {
                var docIdTypes = [];
                for (var i = 0; i < docIds.length; i++) {
                    docIdTypes.push(this.unscrubId(docIds[i])+':'+word);
                }
                Voyeur.application.dispatchEvent('documentTypesSelected', this, {docIdType: docIdTypes});
            }
        }
    },
    constructor : function(config) {
    	if (config.flashvars) {
    		Ext.apply(this.flashvars, config.flashvars);
    		delete config.flashvars;
    	}
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        // add exporter for image
        this.exporters.img = this.localize('exportImg');
        
        Ext.applyIf(config, {
            tbar: [{
                xtype: 'typeSearch',
                width: 100,
                parentTool: this,
                emptyText: this.localize('findTerm'),
                listeners: {
            	    typeSelected: function(combo, type, record) {
                		this.addWord(type);
            	    },
            	    scope: this
                }
            },'->',{
            	xtype: 'button',
            	text: this.localize('clearTerms'),
            	handler: function() {
            		var linksApp = FABridge.links.root();
            		linksApp.removeAllItems(false);
            	},
            	scope: this
            }]
        });
        
        Voyeur.Tool.Links.superclass.constructor.apply(this, arguments);

        this.addListener('afterrender', function(src, params) {
            this.initLinks();
        }, this);
        
        /**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusSummaryResultLoaded', function(src, params) {
            this.initLinks();
            var me = this;
            FABridge.addInitializationCallback('links', function() {
                me.getCorpus().getDocuments().each(function(doc, index) {
                    me.addDocument(doc.getId());
                }, me);
            	me.addInitialContent(params);
            });
        }, this);
        
        /**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
        	if (src == this) {
        		this.handleCorpusTypeData(this.corpusTypeReader.readRecords(data).records);
        	}
        }, this);
        
        /**
		 * @event DocumentTypeFrequenciesResultLoaded
		 * @type listener
		 */
        this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
        	if (src == this) {
        		this.handleDocumentTypeData(this.documentTypeReader.readRecords(data).records);
        	}
        }, this);
        
        /**
		 * @event DocumentTypeCollocateFrequenciesResultLoaded
		 * @type listener
		 */
        this.addListener('DocumentTypeCollocateFrequenciesResultLoaded', function(src, data) {
        	if (src == this) {
        		this.handleTypeCollocateData(this.documentTypeCollocatesReader.readRecords(data).records);
        	}
        }, this);
        
        /**
		 * @event corpusDocumentSelected
		 * @type listener
		 */
        this.addListener('corpusDocumentSelected', function(src, params) {
            var urlParams = Ext.urlDecode(location.search.substring(1));
            var corpus = urlParams.corpus;
//            this.addDocument(corpus, params.docId);
        }, this);
        
        /**
		 * @event corpusTypeSelected
		 * @type listener
		 */
        this.addListener('corpusTypeSelected', function(src, params) {
            if (src != this) {
            	this.addWord(params.type);
            }
        }, this);
        
        /**
		 * @event documentTypeSelected
		 * @type listener
		 */
        this.addListener('documentTypeSelected', function(src, params) {
            var wordParams = params.docIdType.split(':');
            this.addWord(wordParams[1]);
        }, this);
        this.addListener('export', function(exp) {
            if (exp=='img') {
                var img_win = window.open('', 'Charts: Export as Image');
                var linksApp = FABridge.links.root();
                var img_data = linksApp.getImageBinary();
                with(img_win.document) {
                    write('<html><head><title>'+this.localize('title')+'<\/title><\/head><body>' + 
                            "<img src='data:image/png;base64," + img_data + "' /><br />"+this.getFooterText()+"<p>"+this.localize("exportImgSrc")+"</p><tt>&lt;img src=\"data:image/png;base64,"+img_data+"\" alt=\"\" /&gt;</tt><\/body><\/html>")
                }
                img_win.document.close(); // stop the 'loading...' message
                img_win.focus();
            }
        }, this);
    }
    
    ,addInitialContent : function(params) {
    	var params = this.getApiParams();

    	var linksApp = FABridge.links.root();
    	linksApp.setGroupsName('Documents');

        if (params.docId || params.docIndex) {
        	if (params.docIndex) { // convert to docId
        		params.docId = [];
        		var corpus = this.getCorpus();
        		if (typeof params.docIndex == "string") {
        			params.docId.push(corpus.getDocument(parseInt(params.docIndex)).getId());
        		}
        		else {
        			for (var i=0;i<params.docIndex.length;i++) {
        				var id = corpus.getDocument(parseInt(params.docIndex[i])).getId();
        				params.docId.push(id);
        			}
        		}
        		delete params.docIndex;
        	}
        	
        	// convert to array
        	if (typeof params.docId == 'string') {
        		params.docId = [params.docId];
        	}
        	
//        	for (var i=0;i<params.docId.length;i++) {
//            	this.addDocument(params.docId[i]);
//        	}
        	
        	//params.sortBy = 'rawZscoreCorpusDelta';
        	params.sortDirection = 'DESC';
            params.extendedSortZscoreMinimum = 1;
            this.fetch('DocumentTypeFrequencies', params);
        } else {
        	params.corpus = this.getApiParamValue('corpus');
            params.limit = 5;
            params.sortBy = 'rawFreq';
            params.start = 0;
            params.sortDirection = 'DESC';
            params.extendedSortZscoreMinimum = 1;
            this.fetch('CorpusTypeFrequencies', params);
        }
    }
    
    ,addDocument : function(docId) {
        if (this.docIds.indexOf(docId) == -1) {
            this.docIds.push(docId);
            var doc = this.getCorpus().getDocuments().get(docId);
            var linksApp = FABridge.links.root();
            linksApp.addGroup({
                groupId: this.scrubId(docId),
                label: doc.getShortTitle()
            });
        }
    }
    
    ,addWord : function(word) {
        var params = {
            type: word,
//            sortBy: 'rawZscoreCorpusDelta',
            extendedSortZscoreMinimum: 1
        };
        this.fetch('CorpusTypeFrequencies', params);
    }
    
    ,removeWord : function(word) {
        var linksApp = FABridge.links.root();
        linksApp.removeItem(word);
        linksApp.anchorGraph();
    }
    
    ,getCollocates : function(word) {
    	var docIdTypes = [];
        for (var i = 0; i < this.docIds.length; i++) {
        	docIdTypes.push(this.docIds[i]+':'+word);
        }
        var params = {
            corpus: this.getCorpus().getId(),
            docIdType: docIdTypes,
            limit: 3 * this.docIds.length
        };
        this.fetch('DocumentTypeCollocateFrequencies', params);
    }
    
    ,handleCorpusTypeData : function(records) {
        // find document with highest relative frequency for top corpus type
    	var corpus = this.getCorpus();
    	var corpusId = corpus.getId();
    	var limit = this.getApiParamValue('limit');
    	var words = [];
    	var types = [];
    	var docIds = corpus.getDocuments().keys;
    	for (var h=0;h<records.length;h++) {
    		var record = records[h];
    		var type = record.get('type');
    		var rawFreq = record.get('rawFreq');
    		var max = record.get('relativeMax');
	        var relativeFreqs = record.get('relativeFreqs');
	        types.push(type);
//    		for (var i = 0; i < docIds.length; i++) {
//    			words.push({
//    				word: type,
//    				groupId: docIds[i],
//    				value: rawFreq
//    			});
//    		}
	        for (var i = 0; i < relativeFreqs.length; i++) {
	            if (relativeFreqs[i] == max) {
	            	var docId = corpus.getDocument(i).getId();
	            	words.push({
	    				word: type,
	    				groupId: docId,
	    				value: rawFreq
	    			});
	                this.fetch('DocumentTypeCollocateFrequencies', {
	                	corpus: corpusId,
	                	limit: limit,
	                	docIdType: docId+':'+type
	                });
	                break;
	            }
	        }
    	}
    	this.sendWords(words);
//    	for (var h=0; h<types.length; h++) {
//    		this.getCollocates(types[h]);
//    	}
    }
    
    ,handleDocumentTypeData : function (records) {
        var words = [];
        var limit = Math.max(1, Math.ceil(10 / this.getCorpus().getSize()));
        for (var i = 0; i < records.length; i++) {
            var type = records[i].get('type');
            var docId = records[i].get('docId');
            var rawFreq = records[i].get('rawFreq');
            if (rawFreq > 0) {
                words.push({
                    word: type,
                    groupId: docId,
                    value: rawFreq
                });
                
                var params = {
                    corpus: this.getCorpus().getId(),
                    docIdType: docId+':'+type,
                    limit: limit
                    
                };
                this.fetch('DocumentTypeCollocateFrequencies', params);
            }
        }
        this.sendWords(words);
    }
    
    ,handleTypeCollocateData : function (records) {
        var words = [];
        for (var i = 0; i < records.length; i++) {
        	var r = records[i];
            words.push({
                word: r.get('type'),
                groupId: r.get('docId'),
                value: r.get('rawFreq'),
                parent: r.get('keyword')
            });
        }
        this.sendWords(words);
    }
    
    ,scrubId: function(id) {
    	return id.replace(/-/g,"__dash__").replace(/\./g,"__dot__").replace(/'/g,"__apos__");
    }
    
    ,unscrubId: function(id) {
    	return id.replace(/__dash__/g,"-").replace(/__dot__/g,".").replace(/__apos__/g,"'");
    }

    ,sendWords: function(words) {
    	for (var i=0;i<words.length;i++) {
    		// handle character that are problematic
    		var word = words[i];
    		if (typeof word.groupId == 'string') {
    			word.groupId = [word.groupId];
    		}
    		for (var j=0;j<word.groupId.length;j++) {
    			word.groupId[j] = this.scrubId(word.groupId[j]);
    		}
    	}
        var linksApp = FABridge.links.root();
        linksApp.addItems(words);
        linksApp.anchorGraph(1000); // anchor graph 1 second after adding items
    }
    
    /**
     * Fetch Voyeur results using the specified tool and the provided params (and other parameters as needed).
     * @params {String} tool the tool to fetch data (CorpusTypeFrequencies or DocumentTypeFrequencies)
     * @params {Object} params parameters to be sent to the tool (others may be provided by default if not specified).
     * @private
     */
    ,fetch : function(tool,params) {
    	var apiParams = this.getApiParams();
        if (apiParams.stopList) {params.stopList = apiParams.stopList}
        this.update({tool: tool, params: params, title: tool, renderTo: this.body});
    }
    
    ,corpusTypeReader : new Ext.data.JsonReader({
        root : 'corpusTypes.types'
        ,totalProperty : 'corpusTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields))
    ,documentTypeReader : new Ext.data.JsonReader({
        root : 'documentTypes.types'
        ,totalProperty : 'documentTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields)) 
    ,documentTypeCollocatesReader : new Ext.data.JsonReader({
        root : 'documentTypeCollocateFrequencies.types'
        ,totalProperty : 'documentTypeCollocateFrequencies["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.DocumentTypeCollocates.fields))
    
    ,showOptions : function() {
        this.showOptionsWindow({
        	width: 480,
            items : [{
                xtype : 'form',
                labelWidth : 150,
                labelAlign : 'right',
                border : false,
                items : [{
                    xtype: 'radiogroup',
                    id: 'sizing',
                    fieldLabel: '<span ext:qtip="'
                            + this.localize('sizingTip') + '">'
                            + this.localize('sizing') + '</span>',
                    width : 250,
                    items: [
                        {boxLabel: this.localize('valueSizing'), name: 'sizing', inputValue: 'value', checked: this.flashvars.valueSizing == true},
                        {boxLabel: this.localize('linkSizing'), name: 'sizing', inputValue: 'link', checked: this.flashvars.valueSizing != true}
                    ]
                },{
                    xtype: 'checkbox',
                    id: 'autoFit',
                    checked: this.flashvars.autoFit,
                    fieldLabel: '<span ext:qtip="'
                            + this.localize('autoFitTip') + '">'
                            + this.localize('autoFit') + '</span>'
                },{
                    xtype: 'checkbox',
                    id: 'removeOrphans',
                    checked: this.flashvars.removeOrphans,
                    fieldLabel: '<span ext:qtip="'
                            + this.localize('removeOrphansTip') + '">'
                            + this.localize('removeOrphans') + '</span>'
                }],
                buttons : [{
                    text : this.localize('ok', 'tool'),
                    iconCls : 'icon-accept',
                    listeners : {
                        click : {
                            fn : function(btn) {
                                var formPanel = btn.findParentByType('form');
                                var form = formPanel.getForm();
                                var stopList = form.findField('stopList');
                                var global = form.findField('globalStopWords').getValue();
                                var sizing = form.findField('sizing');
                                var autoFit = form.findField('autoFit');
                                var removeOrphans = form.findField('removeOrphans');
                                
                                // make sure we don't have any queries
                                if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
                                else if (stopList.lastQuery != '' && stopList.lastQuery != stopList.getValue()) {
                                    stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
                                    stopList.setValue(stopList.lastQuery)
                                }

                                if (form.isDirty()) {
                                	this.setApiParams({
                                		stopList: stopList.getValue()
                                	});
                                    var linksApp = FABridge.links.root();
                                    this.flashvars.valueSizing = sizing.getValue().inputValue == 'value';
                                    this.flashvars.autoFit = autoFit.checked;
                                    this.flashvars.removeOrphans = removeOrphans.checked;
                                    linksApp.setValueSizing(this.flashvars.valueSizing);
                                    linksApp.setAutofit(autoFit.checked);
                                    linksApp.setRemoveOrphans(removeOrphans.checked);
                                    
                                }
                                formPanel.findParentByType('window').destroy();
                            },
                            scope : this
                        }
                    }
                }, {
                    text : this.localize('cancel', 'tool'),
                    handler : function(btn) {
                        btn.findParentByType('window').destroy();
                    }
                }]
            }]
        }, true);
    }
    
    
    ,api: {
    	/**
    	 * @property start The index in the results to start at.
    	 * @type Integer
    	 * @default 0
    	 */
    	start: {'default': 0}
	    /**
	     * @property limit The number of words to return in each call.
	     * @type Integer
	     * @default 5
	     */
    	,limit: {'default': 5}
    	/**
    	 * @property extendedSortZscoreMinimum The minimum extended sort Z score that each result should have.
    	 * @type Number
    	 * @default 1
    	 */
        ,extendedSortZscoreMinimum: {'default': 1}
        /**
         * @property docId The document ID to restrict results to.
         * @type String
         * @default null
         */
        ,docId: {'default': null}
        /**
         * @property docIndex The document index to restrict results to.
         * @type Integer
         * @default null
         */
        ,docIndex: {'default': null}
        /**
         * @property stopList The stop list to use to filter results.
         * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
         * @type String
         * @default null
         * @choices stop.en.taporware.txt, stop.fr.veronis.txt
         */
        ,stopList: {
        	'default': null,
        	'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
        }
        /**
         * @property sortBy The property to sort results by.
         * @type String
         * @default relativeCollocateRatio
         */
        ,sortBy: {'default': 'relativeCollocateRatio'}
        /**
         * @property sortDirection The direction to sort results in.
         * @type String
         * @default ASC
         * @choices ASC, DESC
         */
        ,sortDirection: {'default': 'DESC'}
        ,toolType: ['Visualization']
        ,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded', 'DocumentTypeFrequenciesResultLoaded', 'DocumentTypeCollocateFrequenciesResultLoaded',  'corpusDocumentSelected', 'corpusTypeSelected', 'documentTypeSelected']
    }
    
    ,thumb: {
    	large: 'Links.png'
    }
    
    ,i18n : {
        title : {en: "Collocate Clusters"}
        ,type : {en: "Visualization"}
        ,help: {
            en: "This tool finds collocates for words and displays links between them using a force directed graph.<br/><br/><b>Controls</b><br/>"+
            "Double-click a word to find its collocates.<br/>Right-click a word to remove it, or make it \"sticky\".<br/>Right-click an empty area to save the graph to an image file."
        }
        ,findTerm : {en: 'Find Term'}
        ,clearTerms : {en: 'Clear Terms'}
        ,adaptedFrom: {
            en: "This tool is based on the <a href=\"http://taporware.mcmaster.ca/~taporware/otherTools/viscollocator.shtml\" target=\"_blank\">Visual Collocator</a>, and uses the <a href=\"http://mark-shepherd.com/blog/springgraph-flex-component/\" target=\"_blank\">SpringGraph</a> Flex component."
        }
        ,sizing: {
            en: "Node size determined by" 
        }
        ,sizingTip: {
            en: "Select either type frequencies or node links (number of collocates) to determine the size of each node."
        }
        ,valueSizing: {
            en: "Type frequency"
        }
        ,linkSizing: {
            en: "Node links" 
        }
        ,autoFit: {
            en: "Autofit graph on screen" 
        }
        ,autoFitTip: {
            en: "If checked, the graph will scroll and zoom automatically, in order to contain all nodes within the viewing area."
        }
        ,removeOrphans: {
            en: "Remove orphans" 
        }
        ,removeOrphansTip: {
            en: "If checked, nodes with no links will be automatically removed."
        }
        ,exportImg: {en : 'a static image in a new window'}
        ,exportImgSrc: {en: 'HTML source code for this image:'}
    }
});

Ext.reg('voyeurLinks', Voyeur.Tool.Links);

/**
 * @class Voyeur.Tool.DocumentTypeKwicsGrid A panel that provides per-document KWIC data in a tabular format.
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.DocumentTypeKwicsGrid = Ext.extend(Ext.grid.GridPanel, {
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		var store = new Ext.data.GroupingStore({
			reader: Voyeur.data.TypeKwics.reader
			,groupField:'docIndex'
			,proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()})
			,listeners : {
				'beforeload' : {
					fn : function(store, options) {
						Ext.applyIf(options.params, this.getApiParams());
						if (!options.params.corpus) {return false;}
						Ext.apply(options.params, {tool: 'TypeKwics'});
					},
					scope : this
				}
				,'loadexception' : {
					fn: function(conn, proxy, response, error){
						this.alertError(response.responseText);
					}
					,scope : this
				}
			}
			,remoteSort: true
			// TODO: restore sorting of KWICS
			,sortInfo : {field : 'RAWFREQ', direction : this.getApiParamValue('sortDirection')}
		});
		
		var size = this.getApiParamValue('limit');
		
		var xtypePrefix = this.getXType()+'.';

		this.expander = new Ext.ux.grid.RowExpander({context: this.getApiParamValue('preview')});
		this.expander.tromboneUrl = this.getTromboneUrl();
		
		var forTool = this; /* create a reference that can be used for direct access (instead of relying on scope) */
		
		this.expander.getBodyContent = function(record,index) {
			
	        var body = '<div id="tmp' + record.id + '">Loading…</div>';
	        var params = forTool.getApiParams();
	        params.start = this.grid.getBottomToolbar().cursor+index;
	        params.limit = 1;
	        params.context = params.preview;
	        params.tool = 'TypeKwics';
	        Ext.Ajax.request({
	           url: forTool.getTromboneUrl()
			   ,params : params
	           ,disableCaching: true
	           ,success: function(response, options) {
	        		var data = Ext.util.JSON.decode(response.responseText);
	        		var recordsObject = Voyeur.data.TypeKwics.reader.readRecords(data);
	        		var record = recordsObject.records[0];
	        		var text = record.get('left')+" <span class='keyword'>"+record.get('middle')+' </span>'+record.get('right');
	        		text = text.replace(/\n\s*/g,'<br />');
	 	            Ext.getDom('tmp' + options.objId).innerHTML = '<div class="x-selectable">'+text+'</div>';
	           }
	           ,failure: function(response, options) {
	           }
			   ,objId: record.id
			   ,type : record.get('type')
	        });

	        return body;
		}
		
		// override class to allow cell selection
		this.expander.getRowClass = function(record, rowIndex, p, ds){
	        p.cols = p.cols-1;
	        var content = this.bodyContent[record.id];
	        if(!content && !this.lazyRender){
	            content = this.getBodyContent(record, rowIndex);
	        }
	        if(content){
	            p.body = content;
	        }
	        return 'x-selectable ' + (this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed');
	    }
		
		this.expander.beforeExpand = function(record, body, rowIndex,a,b,c,d,e){
	        if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){
	            body.innerHTML = this.getBodyContent(record, rowIndex);
	            return true;
	        } else{
	            return false;
	        }
	    }
					
		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
			forceFit:true,
			emptyText : this.localize('noResults','tool'),
			deferEmptyText: false
			,hideGroupedColumn: true
		})
		
		if (!config.plugins) {config.plugins=[]}
		config.plugins.push(this.expander, new Ext.ux.grid.Favs())

		var currentContext = this.getApiParamValue('context');
		var currentPreview = this.getApiParamValue('preview');

		var pagingToolBar = new Ext.PagingToolbar({
			store : store
            ,enableOverflow: true
	        ,pageSize: size
			,displayInfo: true
			,displayMsg : "{0}-{1} of {2}"
			,items : [
				'-',
				{
					text : Voyeur.localization.get(this.xtype+'.context')
					,tooltip : Voyeur.localization.get(this.xtype+'.contextTip')
					,menu : new Ext.menu.Menu({
						items : [
							new Ext.menu.CheckItem({text : '5',checked:currentContext==5,group:'context'})
							,new Ext.menu.CheckItem({text : '10',checked:currentContext==10,group:'context'})
							,new Ext.menu.CheckItem({text : '15',checked:currentContext==15,group:'context'})
							,new Ext.menu.CheckItem({text : '20',checked:currentContext==20,group:'context'})
							,new Ext.menu.CheckItem({text : '25',checked:currentContext==25,group:'context'})
						]
						,listeners : {
							'itemclick' : {
								fn : function(item) {
									this.setApiParams({context: item.text})
									this.getStore().load();
								}
								,scope : this
							}
						}
					})
				}
				,{
					text : Voyeur.localization.get(this.xtype+'.preview')
					,tooltip : Voyeur.localization.get(this.xtype+'.previewTip')
					,menu : new Ext.menu.Menu({
						items : [
							new Ext.menu.CheckItem({text : '10',checked:currentPreview==10,group:'preview'})
							,new Ext.menu.CheckItem({text : '20',checked:currentPreview==20,group:'preview'})
							,new Ext.menu.CheckItem({text : '30',checked:currentPreview==30,group:'preview'})
							,new Ext.menu.CheckItem({text : '40',checked:currentPreview==40,group:'preview'})
							,new Ext.menu.CheckItem({text : '50',checked:currentPreview==50,group:'preview'})
							,new Ext.menu.CheckItem({text : '75',checked:currentPreview==75,group:'preview'})
							,new Ext.menu.CheckItem({text : '100',checked:currentPreview==100,group:'preview'})
							,new Ext.menu.CheckItem({text : '200',checked:currentPreview==200,group:'preview'})
							,new Ext.menu.CheckItem({text : '300',checked:currentPreview==300,group:'preview'})
							,new Ext.menu.CheckItem({text : '400',checked:currentPreview==400,group:'preview'})
							,new Ext.menu.CheckItem({text : '500',checked:currentPreview==500,group:'preview'})
						]
						,listeners : {
							'itemclick' : {
								fn : function(item) {
									this.setApiParams({preview: item.text});
									// look through rows and re-fetch needed previews
									this.getStore().each(function(record, ind) {
										if (this.expander.state[record.id]) {
											this.expander.getBodyContent(record, ind);
										}
									}, this);
								}
								,scope : this
							}
						}
					})
				},'-',{
	                xtype: 'typeSearch',
	                width: 100,
	                parentTool: this,
	                listeners: {
	            	    typeSelected: function(combo, type, record) {
	                		this.setApiParams({type: type});
	                		var docIdTypes = [];
	                		this.getCorpus().getDocuments().eachKey(function(key, doc) {
	                			var docIdType = key+':'+type;
	                			docIdTypes.push(docIdType);
	                		}, this);
	                		this.setApiParams({docIdType: docIdTypes});
							this.getStore().load();
	            	    },
	            	    scope: this
	                }
	            }
			]
		});
		
		Ext.applyIf(config, {
			view :  new Ext.grid.GroupingView(config.viewConfig)
			,iconCls : 'table'
			,loadMask : true
			,sm : new Ext.grid.CheckboxSelectionModel({
				listeners: {
					rowselect: {
						fn: function(sm, index, record) {
							/**
							 * @event tokenSelected
							 * @param {Voyeur.Tool.DocumentTypeKwicsGrid} tool
							 * @param {Object} params <ul>
							 * <li><b>docIdType</b> : String</li>
							 * <li><b>tokenId</b> : Integer</li>
							 * </ul>
							 * @type dispatcher
							 */
							this.getApplication().dispatchEvent('tokenSelected', this, {
								docIdType: record.get('docId')+':'+record.get('type'),
								tokenId: record.get('tokenId')
							});
						}
						,scope: this
					}
				}
			}),
			stripeRows : true,
			colModel : new Ext.grid.ColumnModel([
				this.expander		
				,{header : 'Left', css : 'direction: rtl;', dataIndex : 'left', toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val) {val=Ext.util.Format.htmlEncode(val); return val; return "…"+val.substring(val.length-30)}, align : 'right'},
				{header : 'Keyword', dataIndex : 'middle', toolTip : Voyeur.localization.get('tool.frequencies_document.document_index'), renderer : function(val) {return "<span class='keyword'>" +Ext.util.Format.htmlEncode(val) + '</span>';}, align : 'center', width : 50},
				{header : 'Right', dataIndex : 'right', toolTip : Voyeur.localization.get('tool.frequencies_document.document_name'), renderer : function(val) {val = Ext.util.Format.htmlEncode(val); return val; return val.substring(0,30)+"…"}},
				{header : 'Document', dataIndex : 'docIndex', toolTip : Voyeur.localization.get('tool.frequencies_document.document_name'), renderer : function(value, b,c,d ,e, store) {return forTool.getCorpus().getDocument(value).getLabel()}, scope: this, width: 75},
				{header : 'Position', dataIndex : 'tokenId', toolTip : Voyeur.localization.get('tool.frequencies_document.document_name'), renderer : function(val) {return val}, width : 75, hidden : true}
			]),
			store : store,
			bbar : pagingToolBar
		});
		Voyeur.Tool.DocumentTypeKwicsGrid.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			this.getStore().load();
		}, this);

		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType,  type: null})
			if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
			this.getStore().load();
		}, this);

		/**
		 * @event documentTypesSelected
		 * @type listener
		 */
		this.addListener('documentTypesSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType,  type: null})
			this.getStore().load();
		}, this);
		
		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data) {
			if (this.getCorpus().getSize() == 1) {
				var docIdType = this.getCorpus().getDocument(0).getId()+':'+data.type;
				this.setApiParams({docIdType: docIdType,  type: null})
				if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
				this.getStore().load();
			}
		}, this);
		
		/**
		 * @event corpusTypesSelected
		 * @type listener
		 */
		this.addListener('corpusTypesSelected', function(src, data) {
			if (this.getCorpus().getSize() == 1) {
				var docId = this.getCorpus().getDocument(0).getId();
				var docIdType = [];
				for (var i = 0; i < data.type.length; i++) {
					docIdType.push(docId+':'+data.type[i]);
				}
				this.setApiParams({docIdType: docIdType,  type: null})
				if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
				this.getStore().load();
			}
		}, this);
		
//		this.addListener('tokenSelected', function(src, data) {
//			this.setApiParams({docIdType: data.docIdType,  type: null})
//			this.getStore().load();
//		}, this);

	}

	,api: {
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property context The number of surrounding words to display in the collapsed view.
		 * @type Integer
		 * @default 5
		 */
		,context: {'default': 5}
		/**
		 * @property preview The number of surround words to display in the expanded view.
		 * @type Integer
		 * @default 50
		 */
		,preview: {'default': 50}
		//,sortBy: {'default': 'tokenId'}
		//,sortDirection: {'default': 'desc'}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property tokenIdStart The token id to start at.
		 * @type Integer
		 * @default null
		 */
		,tokenIdStart: {'default': null}
		,toolType: ['Table']
//		,visibleColumn: {'default': ['type','rawFreq','relativeFreqs']}
		,listeners: ['CorpusSummaryResultLoaded', 'documentTypeSelected', 'documentTypesSelected', 'corpusTypeSelected', 'corpusTypesSelected']
		,dispatchers: ['tokenSelected']
	}
	
	,thumb: {
		large: 'DocumentTypeKwicsGrid.png'
	}

	// private localization variables
	,i18n : {
		title : {en: "Keywords in Context"}
		,help : {en: "This tool shows each occurrence of a selected term with some context (words to the left and right of the keyword)."}
		,context : {en: "Context"}
		,contextTip : {en: "This value determines how many words to display on the left and the right of each keyword."}
		,preview : {en: "Preview"}
		,previewTip : {en: "This value determines how many words to display on the left and the right of each keyword in the preview section (the preview is available by expanding the keyword row)."}
		,selectRowsForResults : {en: "Generate results by selecting rows from the <i>Keyword in Context</i> tool."}
	}
});

Ext.reg('voyeurDocumentTypeKwicsGrid', Voyeur.Tool.DocumentTypeKwicsGrid);

Voyeur.Tool.Equalizer = Ext.extend(Ext.Panel, {
	constructor : function(config) {

		// register localization variables
		Ext.apply(this, new Voyeur.Tool(config, this))

		// set configuration options for superclass initialization, but allow overriding from skin
		Ext.applyIf(config, {
			layout: 'fit'
			,html: '<div class="token_images" style="height: 100%; width: 30px;"> </div><div id="prospect_reader" style="position: absolute; left: 40px; right: 5px; top: 0px; height: 100%; overflow: auto; "> </div>'
			,autoScroll: true
//			bodyStyle: {'background-color': 'yellow', padding: '1em'} // unlikely
		})

		var store = new Ext.data.JsonStore({
			root : 'corpusTypes.types',
			totalProperty : 'corpusTypes["@totalTypes"]',
			remoteSort : true,
			fields : [{name: 'type', mapping: '@type'}],
			proxy : new Ext.data.HttpProxy({
						url : this.getTromboneUrl(),
						timeout : 60000
					}),
			baseParams: {
				tool: 'CorpusTypeFrequencies'
				,limit: 5
			}
			,listeners : {
				'beforeload' : {
					fn : function(store, options) {
						if (this.getCorpus().getSize()==0) {return false;}
						options.params.corpus = this.getApiParamValue('corpus');
					},
					scope : this
				}
			}
		})
		
		Ext.applyIf(config, {})
		
		Voyeur.Tool.Equalizer.superclass.constructor.apply(this, arguments);

		this.addListener('afterrender', function(panel) {
			if (this.getCorpus().getSize()>0) {this.fireEvent('CorpusSummaryResultLoaded', this);}
		}, this);

		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			if (this.rendered) {
//				this.buildProspect();
//				this.fetchText();
			}
		}, this);
	}

	,api: {
		toolType: ['Corpus']
		,listeners: ['CorpusSummaryResultLoaded']
	}
	
	,i18n : {
		title : {
			en : 'Equalizer'
		}
	}
});
Ext.reg('voyeurEqualizer', Voyeur.Tool.Equalizer);

/**
 * @class Voyeur.Tool.Bubblelines A visualization tool for displaying word frequencies.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Andrew MacDonald
 */
Voyeur.Tool.Bubblelines = Ext.extend(Ext.Panel, {
	
	INTERVAL: Ext.isGecko ? 50 : 10, // milliseconds between each redraw
	DISMISS_DELAY: 2500, // milliseconds before tooltip auto hides
	
	// these 2 get set in the code
	MAX_LABEL_WIDTH: 0,
	MAX_LINE_WIDTH: 0,
	
	MIN_GRAPH_SEPARATION: 50, // vertical separation between graphs
	graphSeparation: 50,
	
	colors: [[0, 0, 255], [51, 197, 51], [255, 0, 255], [121, 51, 255], [28, 255, 255], [255, 174, 0], [30, 177, 255], [182, 242, 58], [255, 0, 164], [51, 102, 153], [34, 111, 52], [155, 20, 104], [109, 43, 157], [128, 130, 33], [111, 76, 10], [119, 115, 165], [61, 177, 169], [202, 135, 115], [194, 169, 204], [181, 212, 228], [182, 197, 174], [255, 197, 197], [228, 200, 124], [197, 179, 159]],
//	colors: [[116,116,181], [139,163,83], [189,157,60], [171,75,75], [174,61,155]],
	
	intervalId: null,
	clearToolTipId: null,
	overBubbles: [],
	dragInfo: null,
	store: null,
	canvas: null,
	ctx: null,
	maxDocLength: 0,
	maxFreq: {type: null, value: 0},
	maxFreqChanged: false,
	maxRadius: 0,
	selectedDocs: null,
	cache: new Ext.util.MixedCollection(),
	typeTpl: new Ext.XTemplate(
		'<tpl for=".">',
			'<div class="type" style="color: rgb({color});">{type}</div>',
		'</tpl>'
	),
	typeStore: new Ext.data.ArrayStore({
        id: 0,
        fields: ['type', 'color'],
        listeners: {
        	load: function(store, records, options) {
        		var typesView = Ext.getCmp('typesView');
        		for (var i = 0; i < records.length; i++) {
        			var r = records[i];
        			typesView.select(r, true);
        		}
        	},
        	scope: this
        } 
    }),
	
	getObjectId : function() {
		return this.id.replace(/-/g,'_')+'_bubblelines';
	},
	
	corpusTypeReader: new Ext.data.JsonReader({
        root : 'corpusTypes.types'
        ,totalProperty : 'corpusTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields)),
    documentTypeReader: new Ext.data.JsonReader({
        root : 'documentTypes.types'
        ,totalProperty : 'documentTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields)), 
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		Ext.applyIf(config, {
            tbar: [{
                xtype: 'typeSearch',
                width: 100,
                parentTool: this,
                emptyText: this.localize('findTerm'),
                listeners: {
            	    typeSelected: function(combo, type, record) {
            	    	var types = this.getApiParamValue('type') || [];
            	    	if (typeof types == 'string') types = [types];
            	    	var typesA = type.split(/,\s*/);
            	    	for (var i=0; i < typesA.length; i++) {
            	    		types.push(typesA[i]);
            	    	}
                		this.setApiParams({type: types});
						this.store.load({params: {type: type}, add: true});
            	    },
            	    scope: this
                }
            }
            ,' ',{
            	xtype: 'button',
            	text: this.localize('clearTerms'),
            	handler: function() {
            		this.removeAllTypes();
            		Ext.getCmp('typesView').clearSelections(true);
            		this.typeStore.removeAll();
            		this.store.removeAll();
            		this.setApiParams({typeFilter: []});
            		this.drawGraph();
            	},
            	scope: this
            },'-',{
            	xtype: 'documentSelector',
            	listeners: {
            		documentsSelected: function(docIds) {
            			this.setApiParams({docId: docIds});
            			
            			this.filterDocuments();
            			
            			var container = Ext.getCmp('canvasParent');
            			var height = Math.max(this.selectedDocs.getCount() * this.graphSeparation + 15, container.ownerCt.getHeight());
        				var width = container.ownerCt.getWidth();
        				this.canvas.height = height;
            			
            			this.selectedDocs.each(this.findLongestDocument, this);
            			this.selectedDocs.each(this.findLongestDocumentTitle, this);
        				this.setMaxLineWidth(width - this.MAX_LABEL_WIDTH - 75);    
            			
            			this.reloadTypeData();
            		},
            		scope: this
            	}
            },'-',this.localize('segments'),' ',{
            	xtype: 'slider',
            	width: 100,
            	increment: 10,
            	minValue: 10,
            	maxValue: 300,
            	value: parseInt(this.getApiParamValue('bins')),
            	plugins: new Ext.slider.Tip({
            		getText: function(thumb) {
            			return thumb.value;
            		}
            	}),
            	listeners: {
            		changecomplete: function(slider, newvalue) {
            			this.setApiParams({bins: newvalue});
            			this.reloadTypeData();
            		},
            		scope: this
            	}
            }],
            border: false,
            layout: 'fit',
            items: {
            	layout: 'vbox',
            	align: 'stretch',
            	defaults: {border: false},
	            items: [{
	            	height: 30,
	            	id: 'typesView',
	            	xtype: 'dataview',
	            	store: this.typeStore,
	            	tpl: this.typeTpl,
	            	itemSelector: 'div.type',
	            	overClass: 'over',
	            	selectedClass: 'selected',
	            	multiSelect: true,
	            	simpleSelect: true,
	            	listeners: {
	            		beforeclick: function(dv, index, node, event) {
	            			event.preventDefault();
	            			event.stopPropagation();
	            			dv.fireEvent('contextmenu', dv, index, node, event);
	            			return false;
	            		},
	            		selectionchange: function(dv, selections) {
	            			var types = [];
	            			var allTypes = Ext.DomQuery.jsSelect('div[class*=type]', dv.el.dom);
	            			var i, type;
	            			for (i = 0; i < selections.length; i++) {
	            				type = selections[i];
	            				var rec = dv.getRecord(type);
	            				types.push(rec.get('type'));
	            				allTypes.remove(type);
	            				Ext.get(type).removeClass('unselected');
	            			}
	            			for (i = 0; i < allTypes.length; i++) {
	            				Ext.get(allTypes[i]).addClass('unselected');
	            			}
	            			this.setApiParams({typeFilter: types});
	            			this.drawGraph();
	            		},
	            		contextmenu: function(dv, index, node, event) {
	            			event.preventDefault();
	            			event.stopPropagation();
	            			var isSelected = dv.isSelected(node);
	            			var menu = new Ext.menu.Menu({
	            				floating: true,
	            				items: [{
	            					text: isSelected ? this.localize('hideTerm') : this.localize('showTerm'),
	            					handler: function() {
	            						if (isSelected) {
	            							dv.deselect(node);
	            						} else {
	            							dv.select(node, true);
	            						}
	            					},
	            					scope: this
	            				},{
	            					text: this.localize('removeTerm'),
	            					handler: function() {
	            						dv.deselect(index);
	            						var type = this.typeStore.getAt(index).get('type');
	            						this.typeStore.removeAt(index);
	            						this.removeType(type);
	            						this.drawGraph();
	            					},
	            					scope: this
	            				}]
	            			});
	            			menu.showAt(event.getXY());
	            		},
	            		scope: this
	            	} 
	            },{
	            	flex: 1,
	            	xtype: 'container',
	            	autoEl: 'div',
	            	id: 'canvasParent',
	            	autoScroll: true,
	            	cls: 'noHorizontalScroll'
	            }]
            	,listeners: {
            		afterlayout: function(container) {
            			if (this.canvas != null) {
            				var height = Math.max(this.selectedDocs.getCount() * this.graphSeparation + 15, container.getHeight() - 32);
            				var width = container.getWidth();
            				var children = Ext.DomQuery.jsSelect('div:not(div[class*=type])', container.el.dom);
            				for (var i = 0; i < children.length; i++) {
            					var child = Ext.fly(children[i]);
            					child.setWidth(width);
            				}
            				var children = Ext.DomQuery.jsSelect('div', Ext.get('canvasParent').dom);
            				for (var i = 0; i < children.length; i++) {
            					var child = Ext.fly(children[i]);
            					child.setHeight(height);
            				}
            				this.canvas.width = width;
            				this.canvas.height = height;
            				this.setMaxLineWidth(width - this.MAX_LABEL_WIDTH - 75);            				
            				this.recache();
            				this.drawGraph();
            			}
            		},
            		scope: this
            	}
            }
		});
		
		this.store = new Ext.data.JsonStore({
			root : 'documentTypes.types',
			totalProperty : 'documentTypes["@totalTypes"]',
			fields: Voyeur.data.DocumentTypes.fields,
			proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()}),
			listeners: {
				'beforeload': {
					fn: function(store, options) {
						if (this.hidden || this.getCorpus().isEmpty() || (!this.getApiParamValue('docIdType') && !this.getApiParamValue('docIndex') && !this.getApiParamValue('type') && !this.getApiParamValue('docId'))) {
							return false;
						}
						Ext.applyIf(options.params, this.getApiParams());
						var clean = ['docId', 'docIdType', 'type', 'query', 'typeFilter'];
						for (var i=0; i<clean.length;i++) {
							if (!options.params[clean[i]]) {
								delete options.params[clean[i]];
							}
						}
						Ext.apply(options.params, {tool: 'DocumentTypeFrequencies'});
					},
					scope: this
				},
				'load': {
					fn: function(store, records, options) {
						this.cacheDocuments();
						if (this.maxFreqChanged) {
							this.calculateBubbleRadii();
						} else {
							this.calculateBubbleRadii(options.params.type);
						}
						this.drawGraph();
					},
					scope: this
				}
			}
		});
				
		Voyeur.Tool.Bubblelines.superclass.constructor.apply(this, [config]);
		
		// hack to add legend to graph, right before export
		for (var i = 0; i < this.tools.length; i++) {
			var tool = this.tools[i];
			if (tool.id == 'save') {
				var that = this;
				var me = tool;
				var oldHandler = me.handler;
				tool.handler = function(event, tool, panel) {
					that.drawGraph(true);
					oldHandler.apply(me.scope, [event, tool, panel]);
				}
			}
		}
		
		// add css
		var link = document.createElement('link');
		link.href = this.getToolDirectoryUrl()+'Bubblelines.css';
		link.rel = 'stylesheet';
		link.type = 'text/css';
		document.getElementsByTagName('head')[0].appendChild(link);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
//			if (Ext.isIE) {
//				this.body.update('<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/excanvas/excanvas.js"></script>', true);
//			}
			
			this.filterDocuments();
			
			var docIds = this.getApiParamValue('docId');
			if (typeof docIds == 'string') docIds = [docIds];
			this.getTopToolbar().findByType('documentSelector')[0].populate(docIds);
			
			this.selectedDocs.each(this.findLongestDocument, this);
			
			if (this.maxDocLength <= 1) {
				Ext.Msg.alert('Bubblelines', this.localize('corpusTooSmall'));
			} else {
				var container = Ext.getCmp('canvasParent');
				var height = Math.max(this.selectedDocs.getCount() * this.graphSeparation + 15, container.ownerCt.getHeight());
				var width = container.ownerCt.getWidth();
				var id = this.getObjectId();
				container.add({
					width: width,
					height: height,
					html: '<canvas id="'+id+'" width="'+width+'" height="'+height+'"></canvas>',
					border: false,
	            	listeners: {
	            		afterrender: {
	            			fn: function(cnt) {
	        					this.canvas = document.getElementById(id);
	            				this.ctx = this.canvas.getContext('2d');
	            				this.canvas.addEventListener('click', this.clickHandler.createDelegate(this), false);
	            				this.canvas.addEventListener('mousedown', this.mouseDownHandler.createDelegate(this), false);
	            				this.canvas.addEventListener('mouseup', this.mouseUpHandler.createDelegate(this), false);
	            				this.canvas.addEventListener('mousemove', this.moveHandler.createDelegate(this), false);
	            				            				
	            				this.selectedDocs.each(this.findLongestDocumentTitle, this);
	            				
	            				this.setMaxLineWidth(width - this.MAX_LABEL_WIDTH - 75);
	            				
	            				this.cacheDocuments();
	            				this.drawGraph();
	            				this.addInitialContent();
	            			},
	            			single: true,
	            			scope: this
	            		}
	            	}
				});
				container.doLayout();
			}
		}, this);
//		this.addListener('documentTypeSelected', function(src, data) {
//			this.setApiParams({docIdType: data.docIdType,  type: null})
//		}, this);
//
//		this.addListener('documentTypesSelected', function(src, data) {
//			this.setApiParams({docIdType: data.docIdType,  type: null})
//		}, this);
		/**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
            this.handleTypeData(this.corpusTypeReader.readRecords(data).records);
        }, this);
		
		this.addListener('exportComplete', function() {
			this.drawGraph();
		}, this);
	},
	
	filterDocuments: function() {
		var docIds = this.getApiParamValue('docId');
		if (typeof docIds == 'string') docIds = [docIds];
		
		if (docIds == null) {
			this.selectedDocs = this.getCorpus().getDocuments().clone();
			var count = this.selectedDocs.getCount();
			if (count > 10) {
				for (var i = 10; i < count; i++) {
					this.selectedDocs.removeAt(10);
				}
			}
			docIds = [];
			this.selectedDocs.eachKey(function(docId, doc) {
				docIds.push(docId);
			}, this);
			this.setApiParams({docId: docIds});
		} else {
			this.selectedDocs = this.getCorpus().getDocuments().filterBy(function(doc, docId) {
				return docIds.indexOf(docId) != -1;
			}, this);
		}
	},
	
	setMaxLineWidth: function(width) {
		this.maxRadius = width / 30;
		this.MAX_LINE_WIDTH = width - this.maxRadius;
		this.graphSeparation = Math.max(this.maxRadius, this.MIN_GRAPH_SEPARATION);
	},
	
	findLongestDocument: function(doc) {
		var twt = doc.getTotalWordTokens();
		if (twt > this.maxDocLength) {
			this.maxDocLength = twt;
		}
	},
	
	findLongestDocumentTitle: function(doc) {
		this.ctx.font = 'bold 12px Verdana';
		var title = doc.getShortTitle();
		var width = this.ctx.measureText(title).width;
		if (width > this.MAX_LABEL_WIDTH) {
			this.MAX_LABEL_WIDTH = width;
		}
	},
	
	drawGraph: function(includeLegend) {
		if (this.intervalId != null) clearInterval(this.intervalId);
		
		this.intervalId = setInterval(this.doDraw.createDelegate(this, [includeLegend]), this.INTERVAL);
	},
	
	doDraw: function(includeLegend) {
		this.clearCanvas();
		this.cache.each(this.drawDocument.createDelegate(this, [includeLegend], true), this);
		if (includeLegend === true) this.drawLegend();
		else this.drawToolTip();
		this.doDrag();
	},
	
	drawDocument: function(doc, index, totalDocs, includeLegend) {
		var xIndex = 5;
		var yIndex = this.maxRadius + (index * this.graphSeparation);
		if (includeLegend === true) yIndex += 30;
		
		this.ctx.textBaseline = 'top';
		this.ctx.font = 'bold 12px Verdana';
		
		if (this.dragInfo != null && this.dragInfo.oldIndex == index) {
			this.ctx.fillStyle = 'rgba(128, 128, 128, 0.5)';
			xIndex += doc.titleIndent;
			this.ctx.fillText(doc.title, xIndex, yIndex);
		} else {
			// draw label
			this.ctx.fillStyle = 'rgba(128, 128, 128, 1.0)';
			xIndex += doc.titleIndent;
			this.ctx.fillText(doc.title, xIndex, yIndex);
			
			// draw line
			xIndex = this.MAX_LABEL_WIDTH + this.maxRadius;
			yIndex += 4;
			this.ctx.strokeSyle = 'rgba(128, 128, 128, 1.0)';
			this.ctx.fillStyle = 'rgba(128, 128, 128, 1.0)';
			this.ctx.lineWidth = 0.25;
			
			var lineLength = doc.lineLength;
			this.ctx.beginPath();
			this.ctx.moveTo(xIndex, yIndex);
			this.ctx.lineTo(xIndex + lineLength, yIndex);
			this.ctx.stroke();
			
			// draw bubbles
			this.ctx.lineWidth = 0.1;
			var pi2 = Math.PI * 2;
			
			var freqTotal = 0;
			doc.freqCounts = {};
			var types = doc.types;
			var filter = this.getApiParamValue('typeFilter');
			for (var t in types) {
				if (filter.indexOf(t) != -1) {
	//				console.log('drawing', t);
					var freqForType = 0;
					var info = types[t];
					
					var c = info.color.join(',');
					this.ctx.strokeSyle = 'rgba('+c+', 0.1)';
					this.ctx.fillStyle = 'rgba('+c+', 0.35)';
					
					freqTotal += info.rawFreq;
					freqForType += info.rawFreq;
					
					for (var i = 0; i < info.pos.length; i++) {
						var b = info.pos[i];
						if (b.radius > 0) {
							this.ctx.beginPath();
							this.ctx.arc(b.x+xIndex, yIndex, b.radius, 0, pi2, true);
							this.ctx.closePath();
							this.ctx.fill();
							this.ctx.stroke();
						}
					}
					doc.freqCounts[t] = freqForType;
				}
			}
			xIndex += lineLength;
			
			// draw count
			yIndex -= 4;
			this.ctx.fillStyle = 'rgba(0, 0, 0, 1.0)';
			this.ctx.font = '10px Verdana';
			
			this.ctx.fillText(freqTotal, xIndex + 5, yIndex);
		}
	},
	
	drawLegend: function() {
		var x = this.MAX_LABEL_WIDTH + this.maxRadius;
		var y = 5;
		this.ctx.textBaseline = 'top';
		this.ctx.font = '16px serif';
		this.typeStore.each(function(record) {
			var color = record.get('color').join(',');
			this.ctx.fillStyle = 'rgb('+color+')';
			var type = record.get('type');
			this.ctx.fillText(type, x, y);
			var width = this.ctx.measureText(type).width;
			x += width + 8;
		}, this);
	},
	
	doDrag: function() {
		if (this.dragInfo != null) {
			var ordering = {};
			for (var i = 0; i < this.cache.getCount(); i++) {
				if (i < this.dragInfo.oldIndex && i < this.dragInfo.newIndex) {
					ordering[i] = i;
				} else if (i < this.dragInfo.oldIndex && i >= this.dragInfo.newIndex) {
					ordering[i] = i + 1;
				} else if (i == this.dragInfo.oldIndex) {
					ordering[i] = this.dragInfo.newIndex;
				} else if (i > this.dragInfo.oldIndex && i > this.dragInfo.newIndex) {
					ordering[i] = i;
				} else if (i > this.dragInfo.oldIndex && i <= this.dragInfo.newIndex) {
					ordering[i] = i - 1;
				}
			}
			this.dragInfo.oldIndex = this.dragInfo.newIndex;
			
			this.cache.reorder(ordering);
			
			var doc = this.cache.get(this.dragInfo.oldIndex);
			this.ctx.fillStyle = 'rgba(128, 128, 128, 1)';
			this.ctx.textBaseline = 'top';
			this.ctx.font = 'bold 12px Verdana';

			this.ctx.fillText(doc.title, this.dragInfo.x - this.dragInfo.xOffset, this.dragInfo.y - this.dragInfo.yOffset);
		}
	},
	
	drawToolTip: function() {
		if (this.overBubbles.length > 0) {
			this.ctx.fillStyle = 'rgba(224, 224, 224, 0.8)';
			this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';
			
			var x = this.overBubbles[0].x;
			var y = this.overBubbles[0].y;
			var width = 110;
			if (x + width > this.canvas.width) {
				x -= width;
			}
			var height;
			var docIndex = this.overBubbles[0].docIndex;
			if (docIndex != null) {
				var doc = this.cache.get(docIndex);
				var count = 1;
				for (var t in doc.freqCounts) {
					count++;
				}
				height = count * 16 + 10;
				if (y + height > this.canvas.height) {
					y -= height;
				}
				this.ctx.fillRect(x, y, width, height);
				this.ctx.strokeRect(x, y, width, height);
				x += 10;
				y += 10;
				this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
				this.ctx.font = '10px Verdana';
				var total = 0;
				for (var t in doc.freqCounts) {
					var freq = doc.freqCounts[t];
					total += freq;
					this.ctx.fillText(t+': '+freq, x, y, 90);
					y += 16;
				}
				this.ctx.fillText(this.localize('total')+': '+total, x, y, 90);
				
			} else {
				height = this.overBubbles.length * 16 + 10;
				if (y + height > this.canvas.height) {
					y -= height;
				}
				this.ctx.fillRect(x, y, width, height);
				this.ctx.strokeRect(x, y, width, height);
				x += 10;
				y += 10;
				this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
				this.ctx.font = '10px Verdana';
				for (var i = 0; i < this.overBubbles.length; i++) {
					var b = this.overBubbles[i];
					this.ctx.fillText(b.type+': '+b.freq, x, y, 90);
					y += 16;
				}
			}
			
			if (this.clearToolTipId == null) {
				this.clearToolTipId = setTimeout(this.clearToolTip.createDelegate(this), this.DISMISS_DELAY);
			}
		}
	},
	
	clearToolTip: function() {
		this.overBubbles = [];
		clearTimeout(this.clearToolTipId);
		this.clearToolTipId = null;
	},

	reloadTypeData: function() {
		this.cache.clear();
		this.store.removeAll();
		var types = this.getApiParamValue('type');
		if (types.length == 0) {
			this.cacheDocuments();
			this.drawGraph();
		}
		for (var i = 0; i < types.length; i++) {
    		var type = types[i];
    		this.store.load({params: {type: type}, add: true});
    	}
	},
	
	recache: function() {
		this.cache.clear();
		this.cacheDocuments();
		this.calculateBubbleRadii();
	},
	
	cacheDocuments: function() {
		this.selectedDocs.each(this.cacheDocument, this);
	},
	
	cacheDocument: function(doc) {
		var percentage = Math.log(doc.getTotalWordTokens()) / Math.log(this.maxDocLength);
		var lineLength = percentage * this.MAX_LINE_WIDTH;
		var docId = doc.getId();
		if (!this.cache.containsKey(docId)) {
			var title = doc.getShortTitle();
			this.ctx.textBaseline = 'top';
			this.ctx.font = 'bold 12px Verdana';
			var labelWidth = this.ctx.measureText(title).width;
			var indent = this.MAX_LABEL_WIDTH - labelWidth;
			var totalTokens = doc.get('totalTokens') - 1;
		
			this.cache.add(docId, {
				id: docId,
				title: title,
				titleIndent: indent,
				totalTokens: totalTokens,
				types: {},
				lineLength: lineLength,
				freqCounts: {}
			});
		}
		
		var typeResults = this.store.query('docId', docId);
		typeResults.each(function(info) {
			var type = info.get('type');
			if (this.cache.get(docId)['types'][type] == null) {
				this.cacheBubbles(info, lineLength);
			}
		}, this);
	},
	
	cacheBubbles: function(info, lineLength) {
		var rawFreq = info.get('rawFreq');
		if (rawFreq > 0) {
			var docId = info.get('docId');
			var doc = this.cache.get(docId);
			
			var type = info.get('type');
			var color;
			if (this.typeStore.getById(type) == undefined) {
				color = this.getNewColor();
				this.typeStore.loadData([[type, color]], true);
			} else {
				color = this.typeStore.getById(type).get('color');
			}
//			console.log('caching', type);
			var cachedPositions = [];
			
			var maxDistribution = info.get('rawDistributionMax');
			if (maxDistribution > this.maxFreq.value) {
				this.setMaxFreq({type: type, value: maxDistribution});
			}
	
			var spacing = lineLength / this.getApiParamValue('bins');
			var xIndex = 0;
			var bubbles = info.get('rawFreqs');
			for (var i = 0; i < bubbles.length; i++) {
				var b = bubbles[i];
				cachedPositions.push({x: xIndex, freq: b, radius: 0, bin: i});
				xIndex += spacing;
			}
			
			doc['types'][type] = {pos: cachedPositions, maxDistribution: maxDistribution, rawFreq: rawFreq, color: color};
		}
	},
	
	calculateBubbleRadii: function(newType) {
//		console.log('calc all', newType);
		var maxFreqLog = Math.log(this.maxFreq.value);
		var minFreq = Math.log(2) / 2;
		this.cache.each(function(doc) {
			for (var t in doc.types) {
				var type = doc.types[t];
				if (newType == null || t == newType) {
					for (var i = 0; i < type.pos.length; i++) {
						var bubble = type.pos[i];
						if (bubble.freq > 0) {
							var freqLog = Math.max(Math.log(bubble.freq), minFreq);
							bubble.radius = freqLog / maxFreqLog * this.maxRadius;
						} else {
							bubble.radius = 0;
						}
					}
				}
			}
		}, this);
		this.maxFreqChanged = false;
	},
	
	moveHandler: function(event) {
		this.clearToolTip();
		
		this.overBubbles = [];
		var x = event.layerX - this.MAX_LABEL_WIDTH;
		var y = event.layerY;
		var docIndex = Math.round((y - this.maxRadius) / this.graphSeparation);
		if (this.dragInfo != null) {
			this.dragInfo.x = event.layerX;
			this.dragInfo.y = y;
			
			if (docIndex >= this.cache.getCount()) docIndex = this.cache.getCount()-1;
			else if (docIndex < 0) docIndex = 0;
			
			this.dragInfo.newIndex = docIndex;
			document.body.style.cursor = 'move';
		} else {
			if (docIndex >= 0 && docIndex < this.cache.getCount()) {
				if (x >= 0) {
					x -= this.maxRadius; // re-adjust x to the start of the doc line 
					var hits = [];
					var doc = this.cache.get(docIndex);
					if (x >= doc.lineLength) {
						this.overBubbles = [{docIndex: docIndex, x: event.layerX, y: event.layerY}];
					} else {
						var filter = this.getApiParamValue('typeFilter');
						var spacing = doc.lineLength / this.getApiParamValue('bins');
						var xIndex = Math.round(x / spacing);
						for (var t in doc.types) {
							if (filter.indexOf(t) != -1) {
								var type = doc.types[t];
								if (type.pos[xIndex] && type.pos[xIndex].radius > 0) {
									this.overBubbles.push({type: t, freq: type.pos[xIndex].freq, x: event.layerX, y: event.layerY});
								}
							}
						}
					}
					document.body.style.cursor = 'auto'
				} else {
					document.body.style.cursor = 'move';
				}
			} else {
				document.body.style.cursor = 'auto';
			}
		}
	},
	
	mouseDownHandler: function(event) {
		var x = event.layerX;
		var y = event.layerY;
		if (x < this.MAX_LABEL_WIDTH) {
			var docIndex = Math.floor(y / this.graphSeparation);
			if (docIndex < this.cache.getCount()) {
				var xOffset = x - 5;
				var yOffset = 5;
				this.dragInfo = {
					oldIndex: docIndex,
					newIndex: docIndex,
					xOffset: xOffset,
					yOffset: yOffset,
					x: x,
					y: y
				};
			}
		}
	},
	
	mouseUpHandler: function(event) {
		this.dragInfo = null;
	},
	
	clickHandler: function(event) {
		this.overBubbles = [];
		var x = event.layerX - (this.MAX_LABEL_WIDTH + this.maxRadius);
		var y = event.layerY;
		if (x >= 0) {
			var docIndex = Math.floor(y / this.graphSeparation);
			if (docIndex < this.cache.getCount()) {
				var hits = [];
				var tokenId = 0;
				var doc = this.cache.get(docIndex);
				var filter = this.getApiParamValue('typeFilter');
				var spacing = doc.lineLength / this.getApiParamValue('bins');
				var xIndex = Math.round(x / spacing);
				for (var t in doc.types) {
					if (filter.indexOf(t) != -1) {
						var type = doc.types[t];
						if (type.pos[xIndex] && type.pos[xIndex].radius > 0) {
							hits.push(doc.id+':'+t);
							tokenId = type.pos[xIndex].bin;
						}
					}
				}
				if (tokenId != 0) {
					var tokensPerBin = doc.totalTokens / this.getApiParamValue('bins');
					tokenId = Math.floor(tokenId * tokensPerBin); // approximate token id
				}
				if (hits.length == 1) {
					/**
					 * @event documentTypeSelected
					 * @param {Voyeur.Tool.Bubblelines} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : String</li>
					 * <li><b>tokenId</b> : Integer</li>
					 * </ul>
					 * @type dispatcher
					 */
					Voyeur.application.dispatchEvent('documentTypeSelected', this, {docIdType: hits[0], tokenId: tokenId});
				} else if (hits.length > 1) {
					/**
					 * @event documentTypesSelected
					 * @param {Voyeur.Tool.Bubblelines} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : Array</li>
					 * <li><b>tokenId</b> : Integer</li>
					 * </ul>
					 * @type dispatcher
					 */
					Voyeur.application.dispatchEvent('documentTypesSelected', this, {docIdType: hits, tokenId: tokenId});
				}
			}
		}
	},
	
	setMaxFreq: function(maxObj) {
		if (maxObj == null) {
			maxObj = this.findMaxFreq();
		}
		this.maxFreq = maxObj;
		this.maxFreqChanged = true;
//		console.log('changed', maxObj.type, maxObj.value);
	},
	
	findMaxFreq: function() {
		var max = {type: '', value: 0};
		this.cache.each(function(doc) {
			for (var t in doc.types) {
				var maxDistribution = doc.types[t].maxDistribution;
				if (maxDistribution > max.value) {
					max = {type: t, value: maxDistribution};
				}
			}
		}, this);
		return max;
	},
	
	getNewColor: function() {
		var color = null;
		for (var i = 0; i < this.colors.length; i++) {
			color = this.colors[i];
			var match = this.typeStore.findExact('color', color);
			if (match == -1) break;
			else color = null;
		}
		if (color == null) color = [128, 128, 128];
		return color;
	},
	
	removeAllTypes: function() {
		this.typeStore.each(function(record) {
			var type = record.get('type');
			this.removeType(type);
		}, this);
	},
	
	removeType: function(type) {
		var types = this.store.query('type', type);
		types.each(function(type) {
			this.store.remove(type);
		}, this);
		
		var types = this.getApiParamValue('type');
		types = types.remove(type);
		this.setApiParams({type: types});
		
		var getNewMax = false;
		this.cache.each(function(doc) {
			for (var t in doc.types) {
				if (t == type) {
					if (this.maxFreq.type == type) {
						this.maxFreq = {type: null, value: 0};
						getNewMax = true;
					}
					delete doc.types[t];
				}
			}
		}, this);
		if (getNewMax) {
			this.setMaxFreq();
			this.calculateBubbleRadii();
			this.drawGraph();
		}
	},
	
	clearCanvas: function() {
		this.canvas.width = this.canvas.width;
	},

	addInitialContent: function(params) {
        var params = this.getApiParams();
        params.limit = 5;
        params.sortBy = 'rawFreq';
        params.stopList = 'stop.en.smart.txt';
        params.start = 0;
        params.sortDirection = 'DESC';
        params.extendedSortZscoreMinimum = 1;
        this.update({tool: 'CorpusTypeFrequencies', params: params});
    },
    
    handleTypeData: function(records) {
    	for (var i = 0; i < records.length; i++) {
    		var type = records[i].get('type');
    		var types = this.getApiParamValue('type') || [];
    		if (typeof types == 'string') types = [types];
    		types.push(type);
    		this.setApiParams({type: types});
    		this.store.load({params: {type: type}, add: true});
    	}
    },
	
	api: {
		/**
		 * @property bins How many "bins" to separate a document into.
		 * @type Integer
		 * @default 50
		 */
		bins: {'default': 50}
    	/**
    	 * @property query A string to search for in a document.
    	 * @type String
    	 * @default null
    	 */
		,query: {'default': null}
		/**
		 * @property noAnchor True to search for a query anywhere in a word, false to anchor the search to the beginning of a word.
		 * @type Boolean
		 * @default null
		 */
		,noAnchor: {'default': null}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default undefined
		 */
		,docId: {'default': undefined}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default undefined
		 */
		,docIndex: {'default': undefined}
		,typeFilter: {'default': null}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
		,dispatchers: ['documentTypeSelected', 'documentTypesSelected']
	}
	

	,thumb: {
		large: 'Bubblelines.png'
	}
	
    ,i18n : {
        title : {en: 'Bubblelines'}
        ,type : {en: 'Visualization'}
        ,findTerm : {en: 'Find Term'}
        ,clearTerms : {en: 'Clear Terms'}
        ,removeTerm : {en: 'Remove Term'}
        ,showTerm : {en: 'Show Term'}
        ,hideTerm : {en: 'Hide Term'}
        ,segments : {en: 'Segments'}
        ,total : {en: 'Total'}
        ,corpusTooSmall : {en: 'The provided corpus is too small for this tool.'}
        ,help: {en: ''}
        ,adaptedFrom: {en: ''}
    }
});

Ext.reg('voyeurBubblelines', Voyeur.Tool.Bubblelines);
/**
 * @class Voyeur.Tool.Cirrus A simple panel which loads the Cirrus app.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.Cirrus = Ext.extend(Ext.Panel, {
    initialized: false,
    getCirrusObjectId : function() {return this.id.replace(/-/g,'_')+'_cirrus';},
    cirrusApp : null, // the actual canvas/flash app
    
    initCirrus: function(params) {
        var id = this.getCirrusObjectId();
        var appVars = {
        	id: id
        };
        var keys = ['background','fade','smoothness','diagonals'];
        for (var k = 0; k < keys.length; k++) {
            appVars[keys[k]] = this.getApiParamValue(keys[k]);
        }
        
        // from http://www.kirupa.com/developer/mx/detection.htm
        var has_flash = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
        if (Ext.isIE) {
        	try {
                var obj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
                has_flash = true;
            } catch(err){
            }
        }

        // canvas and canvas text detection code from: http://diveintohtml5.org/detect.html#canvas
        var has_canvas = !!(!!document.createElement('canvas').getContext && typeof document.createElement('canvas').getContext('2d').fillText == 'function');
        
        var forceFlash = this.getApiParamValue('forceFlash');
        var forceHtml5 = this.getApiParamValue('forceHtml5');
        
        // for now we'll keep flash the default since the html5 version seems less efficient
        if ((has_flash && !forceHtml5) || forceFlash) {
			var scripts = '<script type="text/javascript">'+
				'function cirrusClickHandler'+id+'(word, value) {'+
				'if (window.console && console.info) console.info(word, value);'+
				'var cirrusTool = Ext.getCmp("'+this.id+'");'+
				'cirrusTool.cirrusClickHandler(word, value);'+
				'}'+
				'function cirrusLoaded'+id+'() {'+
				'if (window.console && console.info) console.info("cirrus flash loaded");'+
				'Ext.getCmp("'+this.id+'").loadInitialData();'+
				'}'+
				'</script>';
            this.body.update(scripts+'<div id="'+id+'" style="height: 100%; width: 100%;"></div>', true);
            appVars.background = '0x'+appVars.background;
            var params = {
                menu: 'false',
                scale: 'showall',
                allowScriptAccess: 'always',
                bgcolor: '#222222',
                wmode: 'opaque'
            };
            var attributes = {
                id: id,
                name: id
            };
            swfobject.embedSWF(this.getApplication().getBaseUrl()+"resources/lib/cirrus/Cirrus.swf", id,
                '100%', '100%', "10.0.0", "expressInstall.swf", appVars, params, attributes);
            this.cirrusApp = Ext.getDom(id);
            this.initialized = true;
        	
        } else if (has_canvas || forceHtml5) {
            appVars.background = '#'+appVars.background;
            var initCanvasApp = function() {
            	if (Ext.DomQuery.selectNode('canvas', Ext.get(id).dom) == null) {
	                try {
	                    appVars.containerId = id;
	                    this.cirrusApp = new Cirrus(appVars);
	                    $('#'+id).bind('wordclicked', function(e, word, value) {
	                        this.cirrusClickHandler(word, value);
	                    }.createDelegate(this));
	                    this.initialized = true;
	                    this.loadInitialData();
	                } catch(e) {
	                    setTimeout(initCanvasApp.createDelegate(this), 250);
	                }
            	}
            }
            var scripts = ''+ 
            '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/jquery/jquery-1.4.2.min.js"></script>'+
            '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/cirrus_canvas/Cirrus.js"></script>'+
            '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/cirrus_canvas/Word.js"></script>'+
            '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/cirrus_canvas/WordController.js"></script>';
            this.body.update(scripts+'<div id="'+id+'" style="height: 100%; width: 100%;"></div>', true, initCanvasApp.createDelegate(this));
        } else {
        	this.body.update('Your browser does not support this tool.');
        }
    },
    
    cirrusClickHandler: function(word, value) {
        if (value == this.getCorpus().getId()) {
        	/**
			 * @event corpusTypeSelected
			 * @param {Voyeur.Tool.Cirrus} tool
			 * @param {Object} params <ul>
			 * <li><b>type</b> : String</li>
			 * </ul>
			 * @type dispatcher
			 */
            Voyeur.application.dispatchEvent('corpusTypeSelected', this, {type: word});
        } else {
            var docIdType = value + ':' + word;
            /**
			 * @event documentTypeSelected
			 * @param {Voyeur.Tool.Cirrus} tool
			 * @param {Object} params <ul>
			 * <li><b>docIdType</b> : String</li>
			 * </ul>
			 * @type dispatcher
			 */
            Voyeur.application.dispatchEvent('documentTypeSelected', this, {docIdType: docIdType});
        }
    },
    
    constructor : function(config) {
        Ext.apply(this, new Voyeur.Tool(config, this));

        Voyeur.Tool.Cirrus.superclass.constructor.apply(this, arguments);
        
        /**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusSummaryResultLoaded', function() {
			if (this.rendered) {this.fireEvent('afterrender', this);}
        }, this);
        
        this.addListener('afterrender', function(src, params) {
			if (this.getCorpus().getSize()==0) {this.body.update('');}
			else {
				this.initCirrus(params);
			}
        }, this);
        
//        this.addListener('corpusDocumentSelected', function(src, params) {
//        	this.setApiParams(params);
//        	this.fetch('CorpusTypeFrequencies');
//        }, this);
        
        /**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
    	this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
    		if (src==this) { // only if it originates from here
	    		this.handleTypeData(this.corpusTypeReader.readRecords(data).records,'corpus');
    		}
    	}, this);
    	this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
    		if (src==this) { // only if it originates from here
	    		this.handleTypeData(this.documentTypeReader.readRecords(data).records,'document');
    		}
    	}, this);
    }
    
    ,loadInitialData: function() {
    	var type = this.getApiParamValue('type');
    	// default to document mode if there's only 1 doc in the corpus
    	if (this.getCorpus().getSize() == 1) {
    		this.setApiParams({docId: this.getCorpus().getDocument(0).getId(), docIndex: 0});
    	}
		if (type != null) {
			// make sure type is included, then remove it so we can get other words
			this.fetch(this.getApiParamValue('docId') || this.getApiParamValue('docIndex') ? 'DocumentTypeFrequencies' : 'CorpusTypeFrequencies');
			this.setApiParams({type: null});
		}
    	this.fetch(this.getApiParamValue('docId') || this.getApiParamValue('docIndex') ? 'DocumentTypeFrequencies' : 'CorpusTypeFrequencies');
    }
    
    ,handleTypeData : function (records, mode) {
        var words = [];
        for (var i = 0; i < records.length; i++) {
        	var freq = records[i].get('rawFreq');
            var word = {word: records[i].get('type'), size: freq, label: freq};
            if (mode =='corpus') word.value = this.getCorpus().getId();
            else word.value = records[i].get('docId');
            words.push(word);
        }
        this.sendWords(words);
    }

    ,clearAll: function() {
        this.cirrusApp.clearAll();
    }
    
    ,sendWords: function(words) {
		var me = this;
		try {
            this.cirrusApp.addWords(words);
            this.cirrusApp.arrangeWords();
		}
		catch (e) {
			setTimeout(function(){me.sendWords.call(me,words)},750);
		}
    }
    
    /**
     * Fetch Voyeur results using the specified tool and the provided params (and other parameters as needed).
     * @params {String} tool the tool to fetch data (CorpusTypeFrequencies or DocumentTypeFrequencies)
     * @params {Object} params parameters to be sent to the tool (others may be provided by default if not specified).
     * @private
     */
    ,fetch : function(tool) {
    	var params = this.getApiParams();
    	var remove = ['background', 'diagonals', 'fade', 'smoothness'];
    	for (var i = 0; i < remove.length; i++) {
    		delete params[remove[i]];
    	}
    	this.update({tool: tool, params: params});
    }
    
	,corpusTypeReader : new Ext.data.JsonReader({
		root : 'corpusTypes.types'
		,totalProperty : 'corpusTypes["@totalTypes"]'
	}, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields)) 
	,documentTypeReader : new Ext.data.JsonReader({
		root : 'documentTypes.types'
		,totalProperty : 'documentTypes["@totalTypes"]'
	}, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields))
	
	
	,showOptions : function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
					xtype: 'textfield',
					id: 'query',
					value: this.getApiParams()['query'],
					fieldLabel : '<span ext:qtip="'
						+ this.localize('queryTip') + '">'
						+ this.localize('query') + '</span>',
					width: 300
				}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								var query = form.findField('query');
								
								var params = {stopList: stopList.getValue(), query: query.getValue()};
								if (params.stopList == '') params.stopList = null;
								
								formPanel.findParentByType('window').destroy();
								
								this.clearAll();
								this.setApiParams(params);

								if (global) {
									this.getApplication().applyParamsGlobally({
										stopList: this.getApiParamValue('stopList')
									}, true);
								}
								else {
									this.initialized = false;
									this.initCirrus({});
								}
						    	
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								var form = btn.findParentByType('form').getForm();
								form.findField('stopList').setValue(this.getApiParamDefaultValue('stopList'));
							},
							scope : this
						}
					}
	
				}]
			}]
		}, true);

	}
	
	,api: {
		/**
		 * @property query A string to search for in a document.
		 * @type String
		 * @default null
		 */
		query: {'default': null}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		,'stopList': {
			'default': null
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
			,'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
			,'example': 'stop.en.taporware.txt'
		}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default null
		 */
		,'docIndex': {
			'default': null
			,'type': Number
			,'required': false
			,'value': null
			,'multiple': true
			,'example': 0
		}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default null
		 */
		,'docId': {
			'default': null
			,'type': String
			,'required': false
			,'value': null
			,'multiple': true
			,'example': 'a_valid_document_id'
		}
		/**
		 * @property background The background colour (in hexadecimal) of the app.
		 * @type String
		 * @default ffffff
		 */
		,'background': {
			'default': 'ffffff'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
		}
		/**
		 * @property fade Whether words should fade in or not.
		 * @type Boolean
		 * @default true
		 */
		,'fade': {
			'default': 'true'
			,'type': Boolean
			,'required': false
			,'value': null
			,'multiple': false
		}
		/**
		 * @property smoothness How many words to display at a time (lower is smoother).
		 * @type Integer
		 * @default 2
		 */
		,'smoothness': {
			'default': 2
			,'type': Number
			,'required': false
			,'value': null
			,'multiple': false
		}
		/**
		 * @property diagonals What words to arrange diagonally (Flash version only). Possible values: all, bigrams, none.
		 * @type String
		 * @default none
		 */
		,'diagonals': {
			'default': 'none'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
			,'choices': ['all', 'bigrams', 'none']
		}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 75
		 */
		,limit: {
			'default': 75
			,type: Number
		}
		/**
		 * @property forceFlash True to force the use of the Flash version.
		 * @type Boolean
		 * @default null
		 */
		,forceFlash: {
			'default': null
		}
		/**
		 * @property forceHtml5 True to force the use of the Canvas version.
		 * @type Boolean
		 * @default null
		 */
		,forceHtml5: {
			'default': null
		}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded', 'DocumentTypeFrequenciesResultLoaded']
		,dispatchers: ['corpusTypeSelected', 'documentTypeSelected']
	}
	
	,thumb: {
		large: 'Cirrus.png'
	}
	
    ,i18n : {
        title : {en: 'Cirrus'}
        ,type : {en: "Visualization"}
        ,query : {en: 'Search'}
        ,queryTip : {en: 'Enter a comma-separated list of words to search for.'}
        ,help: {en: 'This tool organizes words into a "cloud".'}
        ,adaptedFrom: {en: 'This tool is based on this <a href="http://emumarketing.uoregon.edu/paul/2008/09/28/the-new-tag-cloud/" target="_blank">Wordle Clone</a>.'}
    }
});

Ext.reg('voyeurCirrus', Voyeur.Tool.Cirrus);

/**
 * @class Voyeur.Tool.MicroSearch A visualization tool for displaying word frequencies.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 */
Voyeur.Tool.MicroSearch = Ext.extend(Ext.Panel, {
	
	/**
	 * This is the maximum number of tokens in any of the documents, as set when corpus is loaded.
	 */
	maxTokens: 0
	
	/**
	 * The number of tokens represented by each segment, as calculated when canvas is loaded.
	 */
	,tokensPerSegment: 0
	
	,type: ''
	
	,constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		Ext.applyIf(config, {
			layout: 'fit',
			html: '<div class="microSearchSection" style="height: 100%; width: 100%;"> </div>',
			tbar: [{
                xtype: 'typeSearch',
                width: 100,
                parentTool: this,
                emptyText: this.localize('findTerm'),
                listeners: {
            	    typeSelected: function(combo, type, record) {
                		this.setApiParams({type: type.split(/,\s*/)});
                		this.updateData();
            	    },
            	    scope: this
                }
            }]			
		});

				
		Voyeur.Tool.MicroSearch.superclass.constructor.apply(this, [config]);

		this.addListener('afterrender', function(panel) {
			panel.body.addListener('click', function(e) {
				var target = e.getTarget(null,null,true);
				if (target && target.dom.tagName=='SPAN' && target.dom.id.indexOf("prospect")==0) {
					var parts = target.dom.id.split(".");
					var docIndex = parseInt(parts[1]);
					var docSegment = parseInt(parts[2]);
					
					var docId = this.getCorpus().getDocument(docIndex).getId();
					var docIdType = [];
					var params = this.getApiParams();
					if (params.type) {
						if (typeof params.type == 'string') {
							docIdType = docId+':'+params.type
						}
						else {
							for (var i=0, len=params.type.length; i<len; i++) {
								docIdType.push(docId+':'+params.type[i]);
							}
						}
					}
					else {return}

					/**
					 * @event documentTypeSelected
					 * @param {Voyeur.Tool.Reader} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					this.getApplication().dispatchEvent('documentTypeSelected', this, {
						docIdType : docIdType,
						tokenIdStart: docSegment*this.tokensPerSegment
					});
				}
			}, this);
		}, this);

		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			
			// set the max number of tokens in order to do layout
			var corpus = this.getCorpus();
			this.maxTokens = 0;
			corpus.getDocuments().each(function(document) {
				var totalTokens = document.getTotalTokens();
				if (totalTokens>this.maxTokens) {this.maxTokens=totalTokens;}
			}, this);
			
			this.createCanvas();
		}, this);
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
			var store = new Ext.data.JsonStore({
				fields: Voyeur.data.CorpusTypes.fields
				,data: data.corpusTypes.types
			});
			var types = [];
			store.each(function(type) {
				types.push(type.get('type'))
			});
			this.setApiParams({type: types});
			this.updateData();
		}, this);

	},
	
	createCanvas: function() {
		var corpus = this.getCorpus();

		
		var el = this.body.first();
		
		// max lines
		var lineSize = 5; // pixels, including margins below and above
		var maxVerticalLines = Math.floor((el.getHeight() - 10 /* margin of 5px */) / lineSize);
		
		// max segments
		var gutterSize = 10;
		var corpusSize = corpus.getSize();
		var gutter = corpusSize * gutterSize;
		var columnSize = Math.floor((el.getWidth() - gutter - 10 /* margin of 5px */) / corpusSize);
		if (columnSize>200) {columnSize=200;}
		var segmentWidth = 3; // each segment is 3 pixels
		var maxSegmentsPerLine = Math.floor(columnSize / segmentWidth);
		if (maxSegmentsPerLine<1) {maxSegmentsPerLine=1;}
		
		// and the answer is...
		var maxSegments = maxSegmentsPerLine * maxVerticalLines;
		
		this.tokensPerSegment = this.maxTokens < maxSegments ? 1 : Math.ceil(this.maxTokens/maxSegments);
		
		var canvas = "<table cellpadding='0' cellspacing='0'><tr>";
		
		this.segments = [];
		corpus.getDocuments().each(function(document) {
			docIndex = document.getIndex();
			canvas+='<td style="width: '+columnSize+'px;" id="'+this.body.id+'.'+docIndex+'">';
			canvas+='<div class="docLabel"><span ext:qtip="'+document.getLabel()+'">'+(docIndex+1)+")</span> </div>";
			var totalTokens = document.getTotalTokens();
			var segments = Math.ceil(totalTokens / this.tokensPerSegment);
			this.segments[docIndex] = segments;
			for (var i=0; i<segments; i++) {
				canvas += "<span id='prospect."+docIndex+'.'+i+"'><img src='"+Ext.BLANK_IMAGE_URL+"' class='microSearch' /></span>";	
			}
			
			// debug the last visible segment
			// canvas += "<img src='"+Ext.BLANK_IMAGE_URL+"' class='microSearch' style='background-color: red;' id='prospect."+docIndex+'.'+i+"' />";
			
			canvas += '</td>';
			if (docIndex+1<corpusSize) {canvas+='<td style="width: '+gutterSize+'px;">&nbsp;</td>';}
		}, this);
		canvas+='</tr></table>';
		el.update(canvas);
		
		// see if we need to get new data
		this.updateData();
		
	}

	,updateData : function() {
		var params = this.getApiParams();
		if (!params.type && !params.query && !params.docIdType) {
			this.update({tool: 'CorpusTypeFrequencies', params: {limit: 5}})
			return
		}
		
		var corpus = this.getCorpus();
		var corpusId = corpus.getId();

		corpus.getDocuments().each(function(document) {
			
			var index = document.getIndex();
			var cell = Ext.get(this.body.id+'.'+index);
			cell.mask();

			var bins = this.segments[document.getIndex()]
			Ext.Ajax.request({
				url: this.getTromboneUrl()
				,params: {corpus: corpusId, tool: 'DocumentTypeFrequencies', type: params.type, bins: bins, docIndex: index}
				,callback: function(options, success, response) {
					if (success) {
						var data = Ext.decode(response.responseText);
						var store = new Ext.data.JsonStore({
							fields: Voyeur.data.DocumentTypes.fields
							,data: data.documentTypes.types
						});
						
						// go through types and accumulate values in each segment
						var counts = [];
						var words = [];
						store.each(function (type) {
							var freqs = type.get('relativeFreqs');
							var word = type.get("type");
							var raws = type.get('rawFreqs');
							for (var i=0, len=freqs.length; i<len; i++) {
								if (freqs[i]>0) {									
									if (!counts[i]) {
										counts[i]=0;
										words[i]=[];
									}
									counts[i] += freqs[i];
									words[i].push(word+': '+raws[i])
								}
							}
						});
						
						// find max for comparasion (opacity)
						var max = Math.max.apply(null, counts);
						
						// go through images
						var spans = cell.query("span");
						for (var i=1, len=spans.length; i<len; i++) {
							var freq = counts[i] ? counts[i] : 0;
							var span = spans[i];
							var img = span.firstChild;
							var hadFreq = img.style.backgroundColor!='';
							if (freq || (!freq && hadFreq)) {
								img.style.backgroundColor = freq ? 'red' : '';
								img.style.opacity = freq ? (((freq*.5)/max)+.5) : 1;
								span.setAttribute('ext:qtip',words[i] ? words[i].join("; ") : '');
							}
						}
						cell.unmask();
					}
				}
				,scope: this
			})

		}, this);
		
	}
	
	,api: {
    	/**
    	 * @property query A string to search for in a document.
    	 * @type String
    	 * @default null
    	 */
		query: {'default': null}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		,toolType: ['Visualization']
//		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
//		,dispatchers: ['documentTypeSelected', 'documentTypesSelected']
	}
	

    ,i18n : {
        title : {en: 'MicroSearch'}
        ,type : {en: 'Visualization'}
        ,findTerm : {en: 'Find Term'}
        ,clearTerms : {en: 'Clear Terms'}
    }
});

Ext.reg('voyeurMicroSearch', Voyeur.Tool.MicroSearch);
/**
 * @class Voyeur.Tool.VisualCollocator A wrapper for the VisualCollocator applet
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.VisualCollocator = Ext.extend(Ext.Panel, {
	constructor: function(config) {
	
		Ext.useShims = true;
		
		// inherit Voyeur Tool
		Ext.apply(this, new Voyeur.Tool(config, this))

		// call superclass
		Voyeur.Tool.VisualCollocator.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function() {
			this.addInitialContent();
		}, this);
		
		/**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
            var r = this.corpusTypeReader.readRecords(data).records;
            var type = r[0].get('type');
            this.initApplet(type);
        }, this);
		
		var stopList = this.getApplication().getQueryParam('stopList');
		if (stopList != null) {
			if (stopList == 'true') {
				stopList = this.getApplication().getStopListsStore().getAt(0).get('id');
			}
			this.setApiParams({stopList: stopList});
		}
	},
	
	initApplet: function(word) {
		var content = '';
		
		var width = this.body.getWidth();
		var height = this.body.getHeight();
		
		var corpus = this.getCorpus();
		var size = corpus.getSize();
		if (size > 0) {
			
			var url = this.getTromboneUrl()+'?corpus='+corpus.getId()+'&tool=DocumentTypeCollocateFrequencies&outputFormat=xml&limit=5';
			var stopList = this.getApiParamValue('stopList');
			if (stopList != null && stopList != '') url += '&stopList='+stopList;
			var context = this.getApiParamValue('context');
			if (context != null && context != '') url += '&context='+context;
			
			// create applet
			content = '<div style="border: 1px solid #808080; width: '+width+'px; height: '+height+'px; margin: 0 auto;">'+
				'<applet code="ca.mcmaster.taporware.bubblegraph.applet.BubbleGraph" archive="'+this.getToolDirectoryUrl()+'VisualCollocator.jar" codebase="'+this.getToolDirectoryUrl()+'" width="'+width+'" height="'+height+'">'+
				'<param name="url" value="'+url+'" />'+
				'<param name="word" value="'+word+'" />'+
				'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet><div>'+this.localize('adaptedFrom')+'</div></div>'
		}
		
		// update body contents	with applet
		this.body.update(content);
	},

	corpusTypeReader: new Ext.data.JsonReader({
	    root : 'corpusTypes.types'
	    ,totalProperty : 'corpusTypes["@totalTypes"]'
	}, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields)),

	addInitialContent: function(params) {
	    var params = this.getApiParams();
	    params.limit = 1;
	    params.sortBy = 'rawFreq';
	    params.stopList = 'stop.en.smart.txt';
	    params.start = 0;
	    params.sortDirection = 'DESC';
	    params.extendedSortZscoreMinimum = 1;
	    this.update({tool: 'CorpusTypeFrequencies', params: params});
	},
	
	showOptions: function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
					xtype: 'textfield',
					id: 'context',
					value: this.getApiParamValue('context'),
					fieldLabel : '<span ext:qtip="'
						+ this.localize('contextTip') + '">'
						+ this.localize('context') + '</span>',
					width: 50,
					regex: /\d+/,
					regexText: this.localize('numbersOnly'),
					emptyText: this.localize('none','tool')
				}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								var context = form.findField('context');
								
								if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery)
								}
								if (context.getValue() && !context.getRawValue()) {context.setValue('');}
								
								if (form.isDirty()) {
									this.setApiParams({
										stopList: stopList.getValue(),
										context: context.getValue()
									});
									if (global) {
										this.getApplication().applyParamsGlobally({
											stopList: this.getApiParamValue('stopList')
										}, true);
									}
									else {
										this.addInitialContent();
									}
								}
								formPanel.findParentByType('window').destroy();
							},
							scope: this
						}
					}
				}]
			}]
		}, true);
    },

	api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		},
		/**
		 * @property context The number of surrounding words to examine for collocates.
		 * @type Integer
		 * @default null
		 */
		context: {'default': null},
		toolType: ['Visualization'],
		listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
	},
	
	thumb: {
		large: 'VisualCollocator.png'
	}

	// localization variables
	,i18n : {
		title : {en: "VisualCollocator"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool visualizes connections in a network of related entities."}
		,adaptedFrom: {en: ""}
		,context: {en: "Context"}
		,contextTip: {en: "The number of words before and after a term to examine when looking for collocates."}
		,numbersOnly: {en: "Please enter a number."}
	}
});

Ext.useShims = true;

Ext.reg('voyeurVisualCollocator', Voyeur.Tool.VisualCollocator);

/**
 * @class Voyeur.Tool.ToolBrowser A simple panel which lists available tools.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.ToolBrowser = Ext.extend(Ext.Panel, {
    
    tool_names: [
	    'Bubblelines',
        'Bubbles',
        //'Centroid',
        'Cirrus',
        'CorpusGrid',
        'CorpusSummary',
        'CorpusTypeFrequenciesGrid',
        'DocumentInputAdd',
        'DocumentTypeCollocateFrequenciesGrid',
        'DocumentTypeFrequenciesGrid',
        'DocumentTypeKwicsGrid',
        //'Equalizer',
        'Knots',
        'Lava',
        'Links',
        'Mandala',
        'NetVizApplet',
        'Reader',
        'ScatterPlot',
        //'Ticker',
        //'TokensViz',
        'TypeFrequenciesChart',
        'VisualCollocator',
        //'WordCloud',
        'WordCountFountain'
    ],
    toolsList: [],
    itemSelector: 'div.tool',
    
    toolTpl: new Ext.XTemplate(
        '<tpl for=".">'+
        '<hr class="divider" />'+
        '<div class="tool x-unselectable" unselectable="on">'+
        '<div class="icon" style="background-image: url(\'{thumbsmall}\');"><img src="'+Ext.BLANK_IMAGE_URL+'" style="height: 32px; width: 32px;" class="{icontype}" /></div>'+
        '<div class="limited description"><h4>{title}</h4>{desc}</div>'+
        '<div class="more">More...</div>'+
        '</div>'+
        '</tpl>'
    ),
    
    initComponent : function() {
        Voyeur.Tool.ToolBrowser.superclass.initComponent.call(this);
        
        var link = document.createElement('link');
		link.href = this.getToolDirectoryUrl()+'ToolBrowser.css';
		link.rel = 'stylesheet';
		link.type = 'text/css';
		document.getElementsByTagName('head')[0].appendChild(link);
        
        this.addEvents({
        	toolsloaded: true,
        	tooldblclick: true
        });
    },
    
    getToolDirectoryUrlForXtype : function(xtype) {
    	return this.getApplication().getBaseUrl()+'resources/tools/'+xtype.replace(/^voyeur/,'')+'/';
    },
    
    constructor : function(config) {
    
        // inherit Voyeur Tool
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        Ext.applyIf(config, {
            draggablePanels: false,
            layout: 'fit',
            border: false,
            items: {
                xtype: 'tabpanel',
                id: 'tool-tabs',
                enableTabScroll: true,
                layoutOnTabChange: true,
                defaults: {autoScroll: true},
                items: [{
                	title: this.localize('relatedTools'),
                	itemId: 'related',
                	closable: true,
                	items: []
                }],
                listeners: {
                	afterlayout: {
                        fn: function(container, layout) {
                        	container.remove(container.getComponent('related'));
                        },
                        scope: this,
                        single: true
                    },
            		beforeremove: function(container, tab) {
            			if (tab.itemId == 'related') {
            				container.hideTabStripItem(tab);
            				container.activate('All');
            				return false;
            			}
            			return true;
            		}
            	}
            }
        });
        
        // call superclass
        Voyeur.Tool.ToolBrowser.superclass.constructor.apply(this, [config]);
        
        this.addListener('afterrender', function(src, params){
        	var scripts = '';
    		for (var i = 0; i < this.tool_names.length; i++) {
    			var tool = this.tool_names[i];
    			scripts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/tools/'+tool+'/'+tool+'.js"></script>';
    		}
            this.body.update(scripts, true, this.initTools.createDelegate(this));
        }, this);
    },

    initTools: function() {
    	try {
    		var i = 0;
            var typeEntry = null;
            var lang = Voyeur.application.getLocalization().getLang();
            var defaultThumbSmall = this.getToolDirectoryUrl()+'thumb_small.png';
            var defaultThumbLarge = this.getToolDirectoryUrl()+'thumb_large.png';
            for (var i = 0; i < this.tool_names.length; i++) {
                var name = this.tool_names[i];
                var toolClass = Voyeur.Tool[name];
                var xtype = toolClass.xtype;
                var proto = toolClass.prototype;
                var dir = this.getToolDirectoryUrlForXtype(xtype);
                if (proto.i18n && proto.api) {
                    var types = proto.api.toolType ? proto.api.toolType : ['Default'];
                    types.unshift('All');
                    var icontype = '';
                    if (types.indexOf('Visualization') != -1) {
                    	icontype = 'viz';
                    } else if (types.indexOf('Table') != -1) {
                    	icontype = 'table';
                    } else if (types.indexOf('Corpus') != -1) {
                    	icontype = 'corpus';
                    } else if (types.indexOf('Document') != -1) {
                    	icontype = 'document';
                    }
                    var title = proto.i18n.title ? proto.i18n.title[lang] || this.localize('noTitle') : this.localize('noTitle');
                    var desc = proto.i18n.help ? proto.i18n.help[lang] || this.localize('noDescription') : this.localize('noDescription');
                    var listeners = proto.api ? proto.api.listeners || [] : [];
                    var dispatchers = proto.api ? proto.api.dispatchers || [] : [];
                    var thumblarge = defaultThumbLarge;
                    var thumbsmall = defaultThumbSmall;
                    if (proto.thumb) {
                    	if (proto.thumb.large) thumblarge = dir + proto.thumb.large;
                    	if (proto.thumb.small) thumbsmall = dir + proto.thumb.small;
                    }
                    for (var j = 0; j < types.length; j++) {
                    	var type = types[j];
                    	var info = {
                            type: type,
                            xtype: xtype,
                            title: title,
                            desc: desc,
                            listeners: listeners,
                            dispatchers: dispatchers,
                            icontype: icontype,
                            thumblarge: thumblarge,
                            thumbsmall: thumbsmall
                        };
                    	typeEntry = null;
                        for (var t in this.toolsList) {
                            if (this.toolsList[t].type == type) {
                                typeEntry = this.toolsList[t];
                                break;
                            }
                        }
                        if (typeEntry == null) {
                            this.toolsList.push({
                                type: type,
                                tools: [info]
                            });
                        } else {
                            typeEntry.tools.push(info);
                        }
                    }
                }
            }
            
            var toolTabs = Ext.getCmp('tool-tabs');
            for (var i = 0; i < this.toolsList.length; i++) {
                var typeParent = this.toolsList[i];
                var panels = this.createToolPanels(typeParent.tools);
                var tab = {
                    title: typeParent.type,
                    itemId: typeParent.type,
                    items: panels
                };
                if (typeParent.type == 'All') {
                	tab.tbar = [{
                		xtype: 'textfield',
                		emptyText: this.localize('searchText', 'tool'),
                		width: 200,
                		enableKeyEvents: true,
                		listeners: {
                			keyup: function(textfield, event) {
                				var q = textfield.getRawValue();
                				var r = new RegExp(q, 'i');
                				var tab = textfield.findParentByType('panel');
                				tab.items.each(function(item, index, length) {
                					if (q == '' || r.test(item.initialConfig.data.title) || r.test(item.initialConfig.data.desc)) {
                						item.show();
                					} else {
                						item.hide();
                					}
                					return true;
                				}, this);
                			},
                			scope: this
                		}
                	}];
                }
                toolTabs.add(tab);
            }
            toolTabs.doLayout();
            toolTabs.setActiveTab('All');
            this.fireEvent('toolsloaded', this);
    	} catch (e) {
    		setTimeout(this.initTools.createDelegate(this), 250);
    	}
    },
    
    createToolPanels : function(tools) {
    	var panels = [];
        for (var i = 0; i < tools.length; i++) {
            var data = tools[i];
            var panel = this.getPanelFromData(data);
            panels.push(panel);
        }
        return panels;
    },
    
    getPanelFromData : function(data) {
    	return {
            border: false,
            draggable: this.draggablePanels,
            layout: 'fit',
            unstyled: true,
            style: 'float: left;',
            tpl: this.toolTpl,
            data: data,
            style: 'width: 100%;',
            listeners: {
            	afterlayout: {
                    fn: function(container, layout) {
                    	container.el.on('dblclick', function() {
                    		this.fireEvent('tooldblclick', this, container);
                    	}, this);
                    	var more = Ext.DomQuery.selectNode('div[class=more]', container.el.dom);
                    	Ext.Element.fly(more).on('click', this.showToolDetails.createDelegate(this, [more, container.initialConfig.data.type, container.initialConfig.data.xtype]), this);
                    },
                    scope: this,
                    single: true
                }
            }
        };
    },
    
    addToolPanel : function(type, xtype) {
        var tab = Ext.getCmp('tool-tabs').getComponent(type);
        var data = this.getToolData(type, xtype);
        if (data) {
        	var panel = this.getPanelFromData(data);
            tab.add(panel);
            tab.doLayout();
        }
    },
    
    removeToolPanel : function(type, panelId) {
        var tab = Ext.getCmp('tool-tabs').getComponent(type);
        tab.items.removeKey(panelId);
    },
    
    showToolDetails : function(el, type, xtype) {
    	var data = this.getToolData(type, xtype);
    	var win = new Ext.Window({
    		title: data.title,
    		width: 472,
    		height: 322,
    		resizable: false,
    		layout: 'hbox',
    		items: [{
    			width: 256,
    			html: '<img src="'+data.thumblarge+'" alt="'+data.title+' Screenshot" title="'+data.title+' Screenshot" />'
    		},{
    			flex: 1,
    			html: '<div class="detailed description">'+data.desc+'</div>'
    		}],
    		buttons: [{
    			text: this.localize('relatedTools'),
    			handler: function(b, e) {
    				this.showRelatedTools(data.type, data.xtype);
    			},
    			scope: this
    		},{
    			text: 'Close',
    			handler: function(b, e) {
    				b.findParentByType('window').close();
    			}
    		}]
    	});
    	win.show(el);
    },
    
    showRelatedTools : function(type, xtype) {
    	var data = this.getToolData(type, xtype);
    	var senders = []
    	var receivers = [];
    	if (data.listeners.length > 0) {
    		senders = this.getToolsMatchingEvents(data.listeners, 'dispatchers');
    	}
    	if (data.dispatchers.length > 0) {
    		receivers = this.getToolsMatchingEvents(data.dispatchers, 'listeners');
    	}
    	var tabs = Ext.getCmp('tool-tabs');
    	var tab = tabs.getComponent('related');
    	tab.items.each(function(item, index, length) {
    		if ((item.initialConfig.data && item.initialConfig.data.xtype != xtype) || item.initialConfig.data == null) {
    			item.ownerCt.remove(item);
    		} else {
    			item.hide(); // need to hide this so panelproxy still has a dom to work with
    		}
    	}, this);
    	if (receivers.length > 0) {
    		tab.add({
	    		layout: 'fit',
	            unstyled: true,
	            style: 'float: left;',
	            border: false,
	            html: '<div class="related">'+this.localize('receiveEvents')+' '+data.title+'</div>'
	    		
	    	});
    		tab.add(this.createToolPanels(receivers));
    	}
    	if (senders.length > 0) {
    		tab.add({
	    		layout: 'fit',
	            unstyled: true,
	            style: 'float: left;',
	            border: false,
	            html: '<div class="related>'+this.localize('sendEvents')+' '+data.title+'</div>'
	    		
	    	});
    		tab.add(this.createToolPanels(senders));
    	}
    	tabs.unhideTabStripItem(tab);
    	tab.doLayout();
    	tabs.activate(tab);
    	
    },
    
    hideRelatedTools : function() {
    	var tabs = Ext.getCmp('tool-tabs');
    	tabs.remove(tabs.getComponent('related'));
    },
    
    getToolsMatchingEvents : function(events, category) {
    	var matchingTools = [];
		var typeEntry = this.toolsList[0]; // All tools
		for (var j = 0; j < typeEntry.tools.length; j++) {
			var tool = typeEntry.tools[j];
			if (matchingTools.indexOf(tool) == -1) {
    			for (var k = 0; k < events.length; k++) {
    				var e = events[k];
    				if (tool[category].indexOf(e) != -1) {
    					matchingTools.push(tool);
    					break;
    				}
    			}
			}
		}
    	return matchingTools;
    },
    
    getToolData : function(type, xtype) {
    	var data = null;
        for (var i = 0; i < this.toolsList.length; i++) {
            if (data) break;
            var typeEntry = this.toolsList[i];
            if (typeEntry.type == type || type == null) {
                for (var j = 0; j < typeEntry.tools.length; j++) {
                    var tool = typeEntry.tools[j];
                    if (tool.xtype == xtype) {
                        data = tool;
                        break;
                    }
                }
            }
        }
        return data;
    },
    
    api: {
    	toolType: ['Meta']
    }

    // localization variables
    ,i18n : {
        title : {en: "Tool Browser"},
        help: {en: "This tool displays all other tools."},
        noTitle: {en: 'No title'},
        noDescription: {en: 'No description'},
        relatedTools: {en: 'Related Tools'},
        receiveEvents: {en: 'Receive Events From'},
        sendEvents: {en: 'Send Events To'}
    }
});

Ext.reg('voyeurToolBrowser', Voyeur.Tool.ToolBrowser);

/**
 * @class Voyeur.Tool.CorpusTypeFrequenciesGrid A panel for displaying type frequencies across the corpus in a tabular format.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.CorpusTypeFrequenciesGrid = Ext.extend(Ext.grid.GridPanel, {
	
	constructor : function(config) {

		Ext.apply(this, new Voyeur.Tool(config,this));

		var store = new Ext.data.JsonStore({
			root : 'corpusTypes.types',
			totalProperty : 'corpusTypes["@totalTypes"]',
			remoteSort : true,
			fields : Voyeur.data.CorpusTypes.fields,
			sortInfo : {field : this.getApiParamValue('sortBy'), direction : this.getApiParamValue('sortDirection')},
			proxy : new Ext.data.HttpProxy({
						url : this.getTromboneUrl(),
						timeout : 60000
					}),
			listeners : {
				'beforeload' : {
					fn : function(store, options) {
						Ext.applyIf(options.params, this.getApiParams());
						if (!options.params.corpus) {return false;}
						Ext.apply(options.params, {tool: 'CorpusTypeFrequencies'});
					},
					scope : this
				},
				'load' : {
					fn : function(store, records, options) {
						var filters = [];
						if (options.params) {
							var sortBy = options.params.sortBy;
							if (sortBy != 'type' && sortBy != 'rawFreq'
									&& sortBy != 'rawZscore') {filters.push(this.localize('extendedSortZscoreMinimum','tool'));}
							if (options.params.stopList) {filters.push(this.localize('stopList','tool'));}
							if (filters.length>0) {
								this.getApplication().growl(this.localize('title'),
										this.localize('filtersInEffect','tool')+'<ul><li>'+filters.join('</li><li>')+'</ul>',
										this.body)
							}							
						}
					},
					scope : this
				},
				'loadexception' : {
					fn : function(conn, proxy, response, error) {
						this.alertError(response.responseText);
					},
					scope : this
				}
			}
		})

		var xtypePrefix = config.xtype + '.';

		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
					//forceFit : true,
					emptyText : this.localize('noResults', 'tool'),
					deferEmptyText : false
				})

		if (!config.plugins) {
			config.plugins = []
		}

		this.search = new Ext.ux.grid.Search({
			iconCls : 'icon-zoom'
		});

		config.plugins.push(new Ext.ux.grid.Favs({exporter: 'type', align: 'left'}))

		var sm = new Ext.grid.CheckboxSelectionModel()

		var panel = this;

		this.pagingToolBar = new Ext.PagingToolbar({
			store : store,
            enableOverflow: true,
			pageSize : this.getApiParamValue('limit'),
			displayInfo: true,
			displayMsg : "{0}-{1} of {2}",
			items: [{
                xtype: 'typeSearch',
                width: 70,
                parentTool: this,
                listeners: {
	        	    typeSelected: function(combo, type, record) {
	        	    	this.setApiParams({type: null, query: type})
	        	    	this.getStore().load({})
//						this.update({params: this.getApiParams(), tool: 'CorpusTypeFrequencies'});
	        	    },
	        	    scope: this
            	}
        	}],
			listeners : {
				'afterrender' : function(tb) {
					tb.first.hide();
					tb.last.hide();
					tb.refresh.hide();
					seps = tb.findByType('tbseparator');
					for (var i=0;i<seps.length;i++) {seps[i].hide();}
					tb.doLayout();
				}
			}
		});
				
		var panel = this;

		
		var visibleColumn =this.getApiParamValue('visibleColumn');
		
		Ext.applyIf(config, {
			viewConfig : config.viewConfig,
			iconCls : 'table',
			loadMask : true,
			stripeRows : true,
			autoExpandColumn : this.getId()+'-column-type',
			sm: new Ext.grid.CheckboxSelectionModel({
				listeners: {
					selectionchange: {
						fn: function() {this.fireSelectionChange();}
						,scope: this
					}
				}

			}),
			colModel : new Ext.grid.ColumnModel(
				{
					columns: [sm, {
						header : this.localize('type'),
						dataIndex : 'type',
						sortable : true,
						tooltip : this.localize('typeTip'),
						hidden: visibleColumn.indexOf('type')==-1,
						renderer : function(val) {
							return "<span class='keyword'>" + val + "</span>"
						}
						,id: this.getId()+'-column-type'
					}, {
						header : this.localize('rawFreq'),
						dataIndex : 'rawFreq',
						sortable : true,
						tooltip : this.localize('rawFreqTip'),
						hidden: visibleColumn.indexOf('rawFreq')==-1,
						renderer : Ext.util.Format.numberRenderer('0,000')
						,width: 100
					}, {
						header : this.localize('rawZscore'),
						dataIndex : 'rawZscore',
						sortable : true,
						tooltip : this.localize('rawZscoreTip'),
						hidden: visibleColumn.indexOf('rawZscore')==-1,
						renderer : Ext.util.Format.numberRenderer('0,000.00')
						,width: 100
					}, {
						header : this
								.localize('rawZscoreDifferenceCorpusComparison'),
						dataIndex : 'rawZscoreDifferenceCorpusComparison',
						sortable : true,
						tooltip : this
								.localize('rawZscoreDifferenceCorpusComparisonTip'),
						hidden: visibleColumn.indexOf('rawZscoreDifferenceCorpusComparison')==-1,
						renderer : function(val) {
							return isNaN(val) ? '–' : "<span class='"
									+ (val < 0 ? 'negative' : 'positive') + "'>"
									+ Ext.util.Format.number(val,'0,000.0') + '</span>'
						}
						,width: 100
					}, {
						header : this.localize('relativeDistributionMean'),
						dataIndex : 'relativeDistributionMean',
						sortable : true,
						tooltip : this.localize('relativeDistributioneanTip'),
						hidden: visibleColumn.indexOf('relativeDistributionMean')==-1,
						renderer : function(val) {return Ext.util.Format.number(val*10000,'0,000.0')}
						,width: 100
					}, {
						header : this.localize('relativeDistributionStdDev'),
						dataIndex : 'relativeDistributionStdDev',
						sortable : true,
						tooltip : this.localize('relativeDistributionStdDevTip'),
						hidden: visibleColumn.indexOf('relativeDistributionStdDev')==-1,
						renderer : function(val) {
							return isNaN(val) ? '–' : Ext.util.Format.number(val,'0,000.000')
						}
						,width: 100
					}, {
						header : this.localize('relativeDistributionKurtosis'),
						dataIndex : 'relativeDistributionKurtosis',
						sortable : true,
						tooltip : this.localize('relativeDistributionKurtosisTip'),
						hidden: visibleColumn.indexOf('relativeDistributionKurtosis')==-1,
						renderer : function(val) {
							return isNaN(val) ? '–' : "<span class='"
									+ (val < 0 ? 'negative' : 'positive') + "'>"
									+ Ext.util.Format.number(val,'0,000.00') + '</span>'
						}
						,width: 100
					}, {
						header : this.localize('relativeDistributionSkewness'),
						dataIndex : 'relativeDistributionSkewness',
						sortable : true,
						tooltip : this.localize('relativeDistributionSkewnessTip'),
						hidden: visibleColumn.indexOf('relativeDistributionSkewness')==-1,
						renderer : function(val) {
							return isNaN(val) ? '–' : "<span class='"
									+ (val < 0 ? 'negative' : 'positive') + "'>"
									+ Ext.util.Format.number(val,'0,000.00') + '</span>'
						}
					}, {
						header : this.localize('relativeFreqs'),
						dataIndex : 'relativeFreqs',
						tooltip : this.localize('relativeFreqsTip'),
						hidden: visibleColumn.indexOf('relativeFreqs')==-1,
						width : 100,
						renderer : function(val, cell, record, rowIndex, colIndex) {
							return panel.getSparkLine(val, panel.getColumnModel().getColumnWidth(colIndex))
						}
					}]
					,listeners: {
						hiddenchange: {
							// FIXME this code moved from Voyeur.Tool as being too specific
							// also in DocumentTypeCollocateFrequenciesGrid
							fn: function(cm) {
								var columns = [];
								var size = cm.getColumnCount();
								var dataIndex;
								for (var i=0;i<size;i++) {
									if (!cm.isHidden(i)) {
										dataIndex = cm.getDataIndex(i);
										if (dataIndex) {columns.push(cm.getDataIndex(i));}
									}
								}
								this.setApiParams({visibleColumn: columns});
								if (this.store && this.store.groupField) {this.setApiParams({groupBy: this.store.groupField})}
							}
							,scope: this
						}
					}
				}),
			store : store,
			bbar : this.pagingToolBar
//			,enableDragDrop: true
		});
		store.paramNames.sort = 'sortBy';
		store.paramNames.dir = 'sortDirection'

		Voyeur.Tool.CorpusTypeFrequenciesGrid.superclass.constructor.apply(
				this, arguments);

		this.addListener('rowclick', function(src, grid, rowIndex, e) {
			this.fireSelectionChange();
			return true;
		}, this);
	

		/**
		 * @event CorpusTypeFrequenciesRequest
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesRequest', function(src, params) {
			this.setApiParams(params);
			if (params.sortBy) {
				this.getStore().setDefaultSort(params.sortBy, 'DESC');
			}
			else {this.getStore().setDefaultSort(this.getApiParamDefaultValue('sortBy','DESC'))}
			this.getStore().load({params: params});
		}, this);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
//			this.getStore().baseParams.corpus = data.corpus['@id'];
			var size = this.getCorpus().getSize();
			if (size>100) {
				var cm = this.getColumnModel();
				var ind = cm.findColumnIndex('relativeFreqs');
				cm.setColumnWidth(ind, size);
			}
			if (this.rendered) {this.fireEvent('afterrender', this);}
			
		}, this);
		
		this.addListener('afterrender', function(panel) {
			if (this.getCorpus().getSize()==0) {return;}

			// make sure sort field is visible
			var cm = this.getColumnModel();
			var ind = cm.findColumnIndex(this.getApiParamValue('sortBy'));
			cm.setHidden(ind, false);
			
			// make sure query term is set
			if (this.getApiParamValue('query')) {
				this.search.field.setValue(this.getApiParamValue('query'));
			}
			
			this.getStore().load();
			/*
			if (params.query) {
				this.search.field.setValue(params.query);
			}
			*/
		}, this);

	}

	,fireSelectionChange : function() {
		var time = new Date().getMilliseconds();
		this.lastSelectionTime = time;
		var me = this;
		setTimeout(function() {
			if (me.lastSelectionTime == time) {
				records = me.getSelectionModel().getSelections();
				types = []
				for (var i = 0; i < records.length; i++) {
					types.push(records[i].get('type'));
				}
				if (types.length == 1) {
					/**
					 * @event corpusTypeSelected
					 * @param {Voyeur.Tool.CorpusTypeFrequenciesGrid} tool
					 * @param {Object} params <ul>
					 * <li><b>type</b> : String</li>
					 * <li><b>record</b> : Ext.data.Record</li>
					 * </ul>
					 * @type dispatcher
					 */
					Voyeur.application.dispatchEvent('corpusTypeSelected', this,  {type : types[0], record: records[0]});
				} else if (types.length > 1) {
					/**
					 * @event corpusTypesSelected
					 * @param {Voyeur.Tool.CorpusTypeFrequenciesGrid} tool
					 * @param {Object} params <ul>
					 * <li><b>type</b> : Array</li>
					 * <li><b>record</b> : Array</li>
					 * </ul>
					 * @type dispatcher
					 */
					Voyeur.application.dispatchEvent('corpusTypesSelected', this, {type : types, record: records});
				}
			}
		}, 1000);
	},

	lastSelectionTime : 0

	,showOptions : function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
					xtype : 'combo',
					id : 'comparisonCorpus',
					value : this.getApiParamValue('comparisonCorpus'),
					fieldLabel : '<span ext:qtip="'
							+ this.localize('comparisonCorpusTip') + '">'
							+ this.localize('comparisonCorpus') + '</span>',
					loadingText : this.localize('loading', 'tool'),
					width : 300,
					store : this.getApplication().getCorporaStore()
				    ,mode:'local'
				    ,selectOnFocus : true
				    ,displayField: 'label'
				    ,triggerAction: 'all'
				    ,valueField: 'id'
				    ,emptyText: this.localize('none','tool')
				}, {
					xtype : 'numberfield',
					id : 'extendedSortZscoreMinimum',
					value : this.getApiParamValue('extendedSortZscoreMinimum'),
					fieldLabel : '<span ext:qtip="'
							+ this.localize('extendedSortZscoreMinimumTip', 'tool')
							+ '">'
							+ this.localize('extendedSortZscoreMinimum','tool')
							+ '</span>'
				}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var comparisonCorpus = form.findField('comparisonCorpus');
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								
								// make sure we don't have any queries
								if (comparisonCorpus.getValue() && !comparisonCorpus.getRawValue()) {comparisonCorpus.setValue('');}
								else if (comparisonCorpus.lastQuery && comparisonCorpus.lastQuery!=comparisonCorpus.getValue()) {
									comparisonCorpus.getStore().loadData({corpora: {corpora: [{id: comparisonCorpus.lastQuery, label: comparisonCorpus.lastQuery, description: ''}]}}, true);
									comparisonCorpus.setValue(comparisonCorpus.lastQuery)
								}
								
								if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery)
								}

								if (form.isDirty()) {
									var compCorpus = form.findField('comparisonCorpus').getValue();
									this.setApiParams({
										comparisonCorpus: compCorpus ? form.findField('comparisonCorpus').getValue() : null
										,extendedSortZscoreMinimum: form.findField('extendedSortZscoreMinimum').getValue()
										,stopList: stopList.getValue()
									});
									if (global) {
										this.getApplication().applyParamsGlobally({
											stopList: this.getApiParamValue('stopList')
										}, true);
									}
									else {
										this.getStore().load();
									}
								}
								formPanel.findParentByType('window').destroy();
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								var form = btn.findParentByType('form').getForm();
								form.findField('comparisonCorpus').setRawValue(this.getApiParamDefaultValue('comparisonCorpus'));
								form.findField('extendedSortZscoreMinimum').setValue(this.getApiParamDefaultValue('extendedSortZscoreMinimum'));
								form.findField('stopList').setValue(this.getApiParamDefaultValue('stopList'));
							},
							scope : this
						}
					}
	
				}]
			}]
		}, true);

	}
	
	,api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		/**
		 * @property comparisonCorpus The ID of a corpus to compare results against.
		 * @type String
		 * @default null
		 */
		,comparisonCorpus: {'default': null}
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		,start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property sortBy The property to sort results by.
		 * @type String
		 * @default rawFreq
		 */
		,sortBy: {'default': 'rawFreq'}
		/**
		 * @property sortDirection The direction to sort results in.
		 * @type String
		 * @default DESC
		 * @choices ASC, DESC
		 */
		,sortDirection: {'default': 'DESC'}
		/**
		 * @property extendedSortZscoreMinimum The minimum extended sort Z score that each result should have.
		 * @type Number
		 * @default null
		 */
		,extendedSortZscoreMinimum: {'default': null}
		/**
		 * @property query A string to search for in the corpus.
		 * @type String
		 * @default null
		 */
		,query: {'default': null}
		/**
		 * @property visibleColumn The list of columns which are visible.
		 * @type Array
		 * @default ['type','rawFreq','relativeFreqs']
		 * @choices type, rawFreq, rawZscore, rawZscoreDifferenceCorpusComparison, relativeDistributionMean, relativeDistributionStdDev, relativeDistributionKurtosis, relativeDistributionSkewness, relativeFreqs
		 */
		,visibleColumn: {'default': ['type','rawFreq','relativeFreqs']}
		,toolType: ['Table', 'Corpus']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesRequest']
		,dispatchers: ['corpusTypeSelected', 'corpusTypesSelected']
	}
	
	,thumb : {
		large: 'CorpusTypeFrequenciesGrid.png'
	}

	// private localization variables
	,
	i18n : {
		title : {
			en : "Words in the Entire Corpus"
		},
		help : {
			en : "This tool shows overall word frequencies for the entire corpus as well as information about how word frequencies are spread out over documents within the corpus. Hover over column headers and buttons for more information."
		},
		type : {
			en : "Frequencies"
		},
		typeTip : {
			en : "The normalized word found in the corpus."
		},
		rawFreq : {
			en : "Count"
		},
		rawFreqTip : {
			en : "The raw count of this word in the entire corpus."
		},
		rawZscore : {
			en : "Z-Score"
		},
		rawZscoreTip : {
			en : "This is the z-score – or <a href='http://en.wikipedia.org/wiki/Standard_score' target='_blank'>standard score</a> – of the total raw frequencies for the type in all documents, compared to other types; it is a normalized version of the raw frequency, showing the number of standard deviations a value is above or below the mean of type frequencies."
		},
		rawZscoreDifferenceCorpusComparison : {
			en : "Difference"
		},
		rawZscoreDifferenceCorpusComparisonTip : {
			en : "This is the difference between the z-score of the word in this corpus and the z-score of the word in the comparison corpus. Positive values mean the word is more frequent in the document than in the corpus as a whole). See the options for defining the comparison corpus. The z-score – <a href='http://en.wikipedia.org/wiki/Standard_score' target='_blank'>standard score</a> is a normalized version of the raw frequency, showing the number of standard deviations a value is above or below the mean of type frequencies."
		},
		relativeDistributionMean : {
			en : "Mean"
		},
		relativeDistributionMeanTip : {
			en : "The average of the relative frequencies (per 10,000 words) for each document in the corpus."
		},
		relativeDistributionStdDev : {
			en : "Std. Dev."
		},
		relativeDistributionStdDevTip : {
			en : "The standard deviation of the relative frequencies (per 10,000 words) for each document in the corpus."
		},
		relativeDistributionKurtosis : {
			en : "Peakedness"
		},
		relativeDistributionKurtosisTip : {
			en : "A measure of the <a href='http://en.wikipedia.org/wiki/Kurtosis' target='_blank'>peakedness</a> (presence of infrequency extreme deviations) of relative frequency values."
		},
		relativeDistributionSkewness : {
			en : "Skew"
		},
		relativeDistributionSkewnessTip : {
			en : "A measure of the <a href='http://en.wikipedia.org/wiki/Skewness' target='_blank'>asymmetry</a> of relative frequency values for each document in the corpus."
		},
		relativeFreqs : {
			en : "Trend"
		},
		relativeFreqsTip : {
			en : "This graph shows variation in the relative frequencies of a word for each document in a corpus.."
		},
		searchText : {
			en : 'Search'
		},
		selectRowsForResults : {
			en : "Generate results by selecting rows from the <i>Corpus Types</i> tool."
		},
		selectRowsForMoreResults : {
			en : "Select one or more rows to see more results."
		},
		extendedSortInEffect : {
			en : 'A minimum z-score threshold is in effect – see options for more details'
		},
		comparisonCorpus : {
			en : 'Comparison Corpus'
		},
		comparisonCorpusTip : {
			en : 'Define an existing corpus to use for comparison purposes.'
		}
	}
});

Ext.reg('voyeurCorpusTypeFrequenciesGrid', Voyeur.Tool.CorpusTypeFrequenciesGrid);


/**
 * @class Voyeur.Tool.TypeFrequenciesChart A panel for displaying graphs of the frequencies of types across the entire corpus.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
 
Voyeur.Tool.TypeFrequenciesChart = Ext.extend(Ext.Panel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))
		
		var bins = parseInt(this.getApiParamValue('bins'));
		this.bins = new Ext.Button({
			text: this.localize('bins')
			,tooltip : this.localize('binsTip')
			,menu: new Ext.menu.Menu({
				items : [
						new Ext.menu.CheckItem({text : '5',checked:bins==5,group:'bins'})
						,new Ext.menu.CheckItem({text : '10',checked:bins==10,group:'bins'})
						,new Ext.menu.CheckItem({text : '15',checked:bins==15,group:'bins'})
						,new Ext.menu.CheckItem({text : '20',checked:bins==20,group:'bins'})
						,new Ext.menu.CheckItem({text : '25',checked:bins==25,group:'bins'})
						,new Ext.menu.CheckItem({text : '30',checked:bins==30,group:'bins'})
						,new Ext.menu.CheckItem({text : '40',checked:bins==40,group:'bins'})
						,new Ext.menu.CheckItem({text : '50',checked:bins==50,group:'bins'})
						,new Ext.menu.CheckItem({text : '75',checked:bins==75,group:'bins'})
						,new Ext.menu.CheckItem({text : '100',checked:bins==100,group:'bins'})
					]
					,listeners : {
						'itemclick' : {
							fn : function(item) {
								this.setApiParams({bins: item.text});
								// this should only be visible for documents, so no need to check mode
								this.update({params: this.getApiParams(), tool: 'DocumentTypeFrequencies'});
							}
							,scope : this
						}
					}
			})
		});
		
		this.typeSearch = new Ext.ux.TypeSearch({
			width: 100,
            parentTool: this,
            listeners: {
        	    typeSelected: function(combo, type, record) {
    				var types = type.split(/,\s*/);
    				this.setApiParams({type: types});
    				var params = this.getApiParams();
    				this.update({params : params, tool : this.getCorpus().getSize()>1 ? 'CorpusTypeFrequencies' : 'DocumentTypeFrequencies'});
        	    },
        	    scope: this
            }
        });
		
		var freqsMode = this.getApiParamValue('freqsMode');
		Ext.applyIf(config, {
			iconCls : 'chart-line'
			,html : '<span class="x-grid-empty">'+this.localize('noResults')+'</span>'
			,bbar: [new Ext.Button({
				text: this.localize(freqsMode=='raw' ? 'raw' : 'relative'),
				menu: new Ext.menu.Menu({
					items: [
					        {
					        	text: this.localize('rawFrequencies'),
					        	checked: freqsMode=='raw',
					        	group: 'freqsMode',
					        	itemId: 'raw'
					        }
					        ,{
					        	text: this.localize('relativeFrequencies'),
					        	checked: freqsMode!='raw',
					        	group: 'freqsMode',
					        	itemId: 'relative'
					        }
					],
					listeners: {
						'itemclick': {
							fn: function(item) {
								this.setApiParams({freqsMode: item.itemId});
								var params = this.getApiParams();							
								this.update({params: params, tool: params.mode=='document' || this.getCorpus().getSize()<2 ? 'DocumentTypeFrequencies' : 'CorpusTypeFrequencies'});
							}
							,scope: this
						}
					}
				})
			}),'-',this.bins, '-', this.typeSearch]
		});
		Voyeur.Tool.TypeFrequenciesChart.superclass.constructor.apply(this, arguments);
		
		// add exporter for image
		this.exporters.png = this.localize('exportPNG');
		this.exporters.svg = this.localize('exportSVG');
		
		this.addListener('resize', function() {
			if (this.chart) {
				var size = this.body.getSize();
				this.chart.setSize(size.width, size.height);
			}
		}, this)

		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data) {
			if (src && src.xtype==this.xtype) {return;}
			if (data.record) {
				this.handleTypeSelection([data.record],'corpus');
			} else {
				this.typeSearch.setValue(data.type);
				this.setApiParams({type: data.type, docIdType: null});
				var params = this.getApiParams();
				this.update({params : params, tool : this.getCorpus().getSize()>1 ? 'CorpusTypeFrequencies' : 'DocumentTypeFrequencies'});
			}
		});
		
		/**
		 * @event corpusTypesSelected
		 * @type listener
		 */
		this.addListener('corpusTypesSelected', function(src, data){
			if (data.record) {
				this.handleTypeSelection(data.record,'corpus');
			} else {
				this.setApiParams({type: data.type, docIdType: null});
				var params = this.getApiParams();
				this.update({params : params, tool : this.getCorpus().getSize()>1 ? 'CorpusTypeFrequencies' : 'DocumentTypeFrequencies'});
			}
		});
		
		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data){
			if (src && src.xtype==this.xtype) {return false;}
			if (data.record) {
				this.handleTypeSelection([data.record],'document');
			} else {
				this.setApiParams({docIdType: data.docIdType, type: null});
				this.update({params: this.getApiParams(), tool: 'DocumentTypeFrequencies'});
			}
		});
		
		/**
		 * @event documentTypesSelected
		 * @type listener
		 */
		this.addListener('documentTypesSelected', function(src, data){
			if (data.record) {
				this.handleTypeSelection(data.record,'document');
			} else {
				this.setApiParams({docIdType: data.docIdType, type: null});
				this.update({params: this.getApiParams(), tool: 'DocumentTypeFrequencies'});
			}
		});
		
		/**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
			if (src==this) {
				this.handleTypeSelection(this.corpusTypeReader.readRecords(data).records,'corpus');
			}
		}, this);
		
		/**
		 * @event DocumentTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
			if (src==this) {
				this.handleTypeSelection(this.documentTypeReader.readRecords(data).records,'document');
			}
		}, this);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			var params = this.getApiParams();
			if (params.type) {
				delete params.limit;
			}
			this.update({params : params, tool : params.mode=='document' ? 'DocumentTypeFrequencies' : 'CorpusTypeFrequencies'});
		}, this);
		this.addListener('export', function(exp) {
			if (exp=='png') {
				this.chart.exportChart();
			}
			else if (exp=='svg') {
				this.chart.exportChart({
					type: 'image/svg+xml'
				});
			}
		}, this);
		
		this.addListener('resize', function() {
			if (this.chart) {
				var size = this.body.getSize();
		        this.chart.setSize(size.width, size.height);
		    }
		}, this);
	}
	
	,handleTypeSelection: function(records, mode) {
		if (this.rendered && !this.hidden && !this.collapsed && (this.ownerCt && !this.ownerCt.collapsed)) {
			
			// disable bins button if we're in corpus mode
			this.bins.setDisabled(mode=='corpus');
			
			// bail if we have no records
			if (records.length==0) {
				return;
			}
			
			var freqsMode = this.getApiParamValue('freqsMode')

			// check if we have the right number of bins for document frequencies
			if (mode=='document') {
				var bins = this.getApiParamValue('bins');
				var freqs = records[0].get(freqsMode+'Freqs');
				if (bins!=freqs.length) { // refetch
					this.update({params: this.getApiParams(), tool: 'DocumentTypeFrequencies'});
					return;
				}
			}
	
			var lastParams = {mode: mode, docIdType: null, type: null}
			
			var categories = [];
			if (mode=='document') {
				lastParams.docIdType = []
			}
			else {
				lastParams.type = [];
				categories = Voyeur.application.getCorpus().getShortLabels();
			}
			
			var freqsModeField = freqsMode+'Freqs';
			var series = [];
			for (var i=0;i<records.length;i++) {
				var record = records[i];
				var counts = record.get(freqsModeField);
				var type = record.get('type');
				var extras = {};
				if (mode=='document') {
					lastParams.docIdType.push(record.get('docId')+':'+type);
					extras.docId = record.get('docId');
				}
				else {
					lastParams.type.push(type);
				}
				series.push({
					name: type
					,data: record.get(freqsModeField)
					,extras: extras
				});
			}
			
			this.setApiParams(lastParams);
	
			var tool = this;
			this.chart = new Highcharts.Chart({
				chart: {
					renderTo: this.body.dom.id,
					defaultSeriesType: 'spline',
					margin: [50,20,70,60],
					zoomType: 'x'
				},
				title: {
					text: null
				},
				xAxis: {
					categories: categories
					,labels: {
						rotation: 315
						,align: 'right'
						,formatter: function() {
							if (mode=='document') {return this.value+1;}
							else if (this.value.length>10) {
								return this.value.substring(0,10)+'…'
							}
							else {return this.value}
						}
					}
				},
				yAxis: {
					title: {
						text: this.localize(freqsMode+'Frequencies')
					}
					,labels: {
						formatter: function() {
							return freqsMode=='relative' ? Ext.util.Format.number(this.value*10000,'0,000.0') : this.value;
						}
					}
					,min: 0
				},
				tooltip: {
					enabled: true,
					formatter: function() {
						if (mode=='corpus') {
							return '<b>'+ this.series.name +'</b>: <b>'+ (freqsMode == 'relative' ? Ext.util.Format.number(this.y*10000,'0,000.00')+'</b> '+tool.localize('per10kwords') : this.y+'</b>')+"<br/>"+
							tool.localize('in') + '<i>'+this.x+"</i>"
						}
						else {
							return '<b>'+ this.series.name +'</b>: <b>'+this.y+ '</b><br/>'+
								tool.localize('inSegment')+' #'+(this.x+1) +' '+tool.localize('of') + '<i>'+tool.getCorpus().getDocument(this.series.options.extras.docId).getShortLabel()+"</i>"
							
						}
					}
				},
				legend: {
					verticalAlign: 'top',
					symbolPadding: 3,
					borderRadius: 5,
					y: 10,
					symbolWidth: 10
				},
				plotOptions: {
					spline: {
						lineWidth: 1,
						marker: {
							radius: 4
						},
						cursor: 'pointer',
						point: {
							events: {
								click: function(event) {
									var type = this.series.name;
									var params = {};
									var docId;
									if (mode == 'document') { // set position
										docId = this.series.options.extras.docId;
										var totalTokens = tool.getCorpus().getDocument(docId).get('totalTokens') - 1;
										params.tokenIdStart = parseInt(this.category * totalTokens / this.series.data.length);
									} else {
										docId = tool.getCorpus().getDocument(event.point.x).getId();
									}
									params.docIdType = docId+':'+type;
									// NB only sending documentTypeSelected now, for use with ScatterPlot skin
									/**
									 * @event documentTypeSelected
									 * @param {Voyeur.Tool.TypeFrequenciesChart} tool
									 * @param {Object} params <ul>
									 * <li><b>docIdType</b> : String</li>
									 * <li><b>tokenIdStart</b> : Integer</li>
									 * </ul>
									 * @type dispatcher
									 */
									tool.getApplication().dispatchEvent('documentTypeSelected', tool, params);
								}
							}
						}
					}
				},
				series: series,
				credits: {enabled: false},
				exporting: {
					enabled: false // hide buttons
				}
				
			});
			
			if (mode=='corpus' && this.getCorpus().getSize()==1) {
				this.getApplication().growl(this.localize(mode=='corpus'  ? 'relativeFrequencies' : 'rawFrequencies'), this.localize('onlyOneDocumentInCorpus'), this.body);
			}
		}
	}

	,corpusTypeReader : new Ext.data.JsonReader({
			root : 'corpusTypes.types'
			,totalProperty : 'corpusTypes["@totalTypes"]'
		}, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields)) 

	,documentTypeReader : new Ext.data.JsonReader({
			root : 'documentTypes.types'
			,totalProperty : 'documentTypes["@totalTypes"]'
		}, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields))
	
	,api: {
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		docIdType: {'default': null}
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		,stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
    	 * @property mode What mode to operate at, either document or corpus.
    	 * @choices document, corpus
    	 */
		,mode: {'default': null}
		/**
		 * @property bins How many "bins" to separate a document into.
		 * @type Integer
		 * @default 50
		 */
		,bins: {'default': 10}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 5}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded', 'DocumentTypeFrequenciesResultLoaded', 'corpusTypeSelected', 'corpusTypesSelected', 'documentTypeSelected', 'documentTypesSelected']
		,dispatchers: ['documentTypeSelected']
		
		/**
		 * @property freqsMode Determine if raw or relative frequencies are shown.
		 * @type String
		 * @default relative
		 */
		,freqsMode: {'default': 'relative'}
    }
	
	,thumb: {
		large: 'TypeFrequenciesChart.png'
	}

	,i18n : {
		title : {en: "Word Trends"}
	    ,type : {en : "Frequencies"}
	    ,relativeFrequencies : {en: 'Relative Frequencies'}
	    ,raw : {en: 'Raw'}
	    ,relative : {en: 'Relative'}
	    ,rawFrequencies : {en: 'Raw Frequencies'}
	    ,relfreq : {en: 'relative frequencies'}
	    ,rawfreq : {en: 'raw frequencies'}
	    ,'per10kwords' : {en: 'per 10,000 words'}
	    ,'in' : {en: 'in'}
	    ,'inSegment' : {en: 'in segment'}
	    ,'of' : {en: 'of'}
	    ,noResults: {en: 'No results to display.'}
		,help : {en: "This chart shows how terms are distributed across documents in a corpus (documents are shown in the order in which they were added)."}
		,corpusTitle : {en : 'Distribution of Types across Corpus'}
		,documentTitle: {en : 'Distribution of Types across Document'}
		,exportPNG: {en : 'a static PNG image in a new window'}
		,exportSVG: {en : 'a static SVG image in a new window'}
		,bins: {en: 'Segments'}
		,binsTip: {en: 'This option allows you to define how many segments should be represented in the graph (note that this option only applies to distribution within a document, not distribution across a corpus).'}
		,onlyOneDocumentInCorpus: {en: 'This tool shows distribution across documents in a corpus but you only have one document in this corpus.'}
	}
});

Ext.reg('voyeurTypeFrequenciesChart', Voyeur.Tool.TypeFrequenciesChart);

/**
 * @class Voyeur.Tool.VoyeurFooter A simple panel with credits for the Voyeur project.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.VoyeurFooter = Ext.extend(Ext.Panel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))
		Ext.applyIf(config, {
            html: '',
            autoHeight: true,
			collapsible : false,
			split : false,
			layout : 'fit',
			collapsibleMode : null,
			id : 'footer'
			,header : false
		});
		Voyeur.Tool.VoyeurFooter.superclass.constructor.apply(this, arguments);
		
		// check available size
		this.addListener('afterrender', function(panel) {
			panel.body.update(this.getFooterText(panel.body));
		}, this);
	}
});

Ext.reg('voyeurFooter', Voyeur.Tool.VoyeurFooter);

/**
 * @class Voyeur.Tool.Lava A wrapper for the Lava applet.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.Lava = Ext.extend(Ext.Panel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config,this));
		Voyeur.Tool.Lava.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			var params = this.getApiParams();
			Ext.apply(params, {
				tool: 'CorpusSummary'
				,outputFormat: 'xml'
				,template: 'corpusSummary2lavaConfig'
			});
			var content = '<div align="center"><applet code="ca.hermeneuti.lava.LavaApplet" archive="'+this.getApplication().getBaseUrl()+'resources/lib/lava/lava-0.0.1-SNAPSHOT-jar-with-dependencies.jar" width="640" height="640">'+
				'<param name="config" value="'+this.getTromboneUrl()+'?' + Ext.urlEncode(params)+ '" />'+
				'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet></div>'
			this.body.update(content);
		}, this);
	}
	,api: {
		toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded']
	}
	,thumb: {
		large: 'Lava.png'
	}
	,i18n : {
		title : {en: "Lava"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool allows you to explore multiple levels of a corpus."}
		,adaptedFrom: {en: "Balls Reader is adapted from Martin Ignacio Bereciartua's <a href=\"http://www.m-i-b.com.ar/mib/letter_pairs/eng/\" target=\"_blank\">Letter Pairs</a>."}
	}
});

Ext.reg('voyeurLava', Voyeur.Tool.Lava);

/**
 * @class Voyeur.Tool.FeatureClusters A simple panel which loads the LinksFlowers app.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.FeatureClusters = Ext.extend(Ext.Panel, {
    initialized: false,
    getFeatureClustersObjectId : function() {return this.id.replace(/-/g,'_')+'_featureClusters';},
    // configuration for the flash app
    flashvars : {
    },
    flashApp: null,
    initFeatureClusters: function() {
        if (!this.initialized) {
            var id = this.getFeatureClustersObjectId();
            var scripts = '<script type="text/javascript">'+
            'function linksFlowersClickHandler(word) {'+
            'if (window.console && console.info) console.info(word);'+
            'var linksTool = Ext.ComponentMgr.all.find(function(object){if (object.xtype==\'voyeurFeatureClusters\') {return true;} else {return false;}});'+
            'linksTool.featureClustersClickHandler(word);'+
            '}'+
            'function linksFlowersInitialized() {'+
            'var linksTool = Ext.ComponentMgr.all.find(function(object){if (object.xtype==\'voyeurFeatureClusters\') {return true;} else {return false;}});'+
            'linksTool.addInitialContent();'+
            '}'+
            '</script>';
            this.body.update(scripts+'<div id="'+id+'"></div>', true);
            var params = {
                menu: "false",
                scale: "noScale",
                allowFullscreen: "true",
                allowScriptAccess: "always",
                bgcolor: "#FFFFFF",
                wmode: 'opaque'
            };
            var attributes = {
                id: id,
                name: id
            };
            swfobject.embedSWF(this.getApplication().getBaseUrl()+"resources/lib/linksflowers/LinksFlowers.swf", id,
                '100%', '100%', "10.0.0", "expressInstall.swf", this.flashvars, params, attributes);
            this.initialized = true;
            this.flashApp = Ext.getDom(id);
        }
    },
    featureClustersClickHandler: function(word) {
        if (docIds == null) Voyeur.application.dispatchEvent('corpusTypeSelected', this, {type: word});
        else {
            var docIdTypes = [];
            for (var i = 0; i < docIds.length; i++) {
                docIdTypes.push(this.unscrubId(docIds[i])+':'+word);
            }
            Voyeur.application.dispatchEvent('documentTypesSelected', this, {docIdType: docIdTypes});
        }
    },
    constructor : function(config) {
    	if (config.flashvars) {
    		Ext.apply(this.flashvars, config.flashvars);
    		delete config.flashvars;
    	}
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        // add exporter for image
        this.exporters.img = this.localize('exportImg');
        
        Ext.applyIf(config, {
            tbar: [{
                xtype: 'typeSearch',
                width: 100,
                parentTool: this,
                emptyText: this.localize('findTerm'),
                listeners: {
            	    typeSelected: function(combo, type, record) {
                		this.addWord(type);
            	    },
            	    scope: this
                }
            },'->',{
            	xtype: 'button',
            	text: this.localize('clearTerms'),
            	handler: function() {
            		this.flashApp.removeAllWords();
            	},
            	scope: this
            }]
        });
        
        Voyeur.Tool.FeatureClusters.superclass.constructor.apply(this, arguments);

        this.addListener('afterrender', function(src, params) {
            this.initFeatureClusters();
        }, this);
        
        /**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusSummaryResultLoaded', function(src, params) {
            this.initFeatureClusters();
        }, this);
        
        /**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
        	if (src == this) {
        		this.handleCorpusTypeData(this.corpusTypeReader.readRecords(data).records);
        	}
        }, this);
        
        /**
		 * @event DocumentTypeFrequenciesResultLoaded
		 * @type listener
		 */
        this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
        	if (src == this) {
        		this.handleDocumentTypeData(this.documentTypeReader.readRecords(data).records);
        	}
        }, this);
        
        /**
		 * @event corpusDocumentSelected
		 * @type listener
		 */
        this.addListener('corpusDocumentSelected', function(src, params) {
            var urlParams = Ext.urlDecode(location.search.substring(1));
            var corpus = urlParams.corpus;
//            this.addDocument(corpus, params.docId);
        }, this);
        
        /**
		 * @event corpusTypeSelected
		 * @type listener
		 */
        this.addListener('corpusTypeSelected', function(src, params) {
            if (src != this) {
            	this.addWord(params.type);
            }
        }, this);
        
        /**
		 * @event documentTypeSelected
		 * @type listener
		 */
        this.addListener('documentTypeSelected', function(src, params) {
            var wordParams = params.docIdType.split(':');
            this.addWord(wordParams[1]);
        }, this);
        this.addListener('export', function(exp) {
            if (exp=='img') {
                var img_win = window.open('', 'Charts: Export as Image');
                var img_data = this.flashApp.getImageBinary();
                with(img_win.document) {
                    write('<html><head><title>'+this.localize('title')+'<\/title><\/head><body>' + 
                            "<img src='data:image/png;base64," + img_data + "' /><br />"+this.getFooterText()+"<p>"+this.localize("exportImgSrc")+"</p><tt>&lt;img src=\"data:image/png;base64,"+img_data+"\" alt=\"\" /&gt;</tt><\/body><\/html>")
                }
                img_win.document.close(); // stop the 'loading...' message
                img_win.focus();
            }
        }, this);
    }
    
    ,addInitialContent : function() {
        var params = this.getApiParams();
        if (params.docId || params.docIndex) {
        	if (params.docIndex) { // convert to docId
        		params.docId = [];
        		var corpus = this.getCorpus();
        		if (typeof params.docIndex == "string") {
        			params.docId.push(corpus.getDocument(parseInt(params.docIndex)).getId());
        		}
        		else {
        			for (var i=0;i<params.docIndex.length;i++) {
        				var id = corpus.getDocument(parseInt(params.docIndex[i])).getId();
        				params.docId.push(id);
        			}
        		}
        		delete params.docIndex;
        	}
        	
        	// convert to array
        	if (typeof params.docId == 'string') {
        		params.docId = [params.docId];
        	}
        	
//        	for (var i=0;i<params.docId.length;i++) {
//            	this.addDocument(params.docId[i]);
//        	}
        	
        	//params.sortBy = 'rawZscoreCorpusDelta';
        	params.sortDirection = 'DESC';
            params.extendedSortZscoreMinimum = 1;
            this.fetch('DocumentTypeFrequencies', params);
        } else {
        	params.corpus = this.getApiParamValue('corpus');
            params.limit = 25;
            params.sortBy = 'rawFreq';
            params.start = 0;
            params.sortDirection = 'DESC';
            params.sortDirection = 'DESC';
            params.extendedSortZscoreMinimum = 1;
            this.fetch('CorpusTypeFrequencies', params);
        }
    }
    
    ,addWord : function(word) {
        var params = {
            type: word,
            sortBy: 'rawZscoreCorpusDelta',
            extendedSortZscoreMinimum: 1
        };
        this.fetch('DocumentTypeFrequencies', params);
    }
    
    ,removeWord : function(word) {
        this.flashApp.removeWord(word);
    }
    
    ,handleCorpusTypeData : function (records) {
    	var words = [];
        for (var i = 0; i < records.length; i++) {
            words.push({
                label: records[i].get('type'),
                value: records[i].get('rawFreq')
            });
        }
        this.sendWords(words);
    }
    
    ,handleDocumentTypeData : function (records) {
        var words = [];
        for (var i = 0; i < records.length; i++) {
            var type = records[i].get('type');
            var docId = records[i].get('docId');
            var rawFreq = records[i].get('rawFreq');
            if (rawFreq > 0) {
                words.push({
                    label: type,
                    value: rawFreq
                });
            }
        }
        this.sendWords(words);
    }
    
    ,scrubId: function(id) {
    	return id.replace(/-/g,"__dash__").replace(/\./g,"__dot__");
    }
    
    ,unscrubId: function(id) {
    	return id.replace(/__dash__/g,"-").replace(/__dot__/g,".");
    }

    ,sendWords: function(words) {
//    	if (!this.counter) {this.counter=0}
//    	this.counter +=1;
//    	//if (this.counter>5) {return;}
//    	console.warn(words)
    	var featureNames = ['a','b','c','d','e','f','g','h'];
    	for (var i=0;i<words.length;i++) {
    		// handle character that are problematic
//    		words[i].groupId = this.scrubId(words[i].groupId);
    		var word = words[i];
    		word.features = {};
    		var hit = false;
    		for (var j = 0; j < featureNames.length; j++) {
    			if (Math.random() > 0.8) {
    				word.features[featureNames[j]] = Math.random() * 100;
    				hit = true;
    			}
    		}
    		if (!hit) {
    			word.features[featureNames[Math.floor(Math.random() * featureNames.length)]] = Math.random() * 100;
    		}
    	}
        this.flashApp.addWords(words);
    }
    
    /**
     * Fetch Voyeur results using the specified tool and the provided params (and other parameters as needed).
     * @params {String} tool the tool to fetch data (CorpusTypeFrequencies or DocumentTypeFrequencies)
     * @params {Object} params parameters to be sent to the tool (others may be provided by default if not specified).
     * @private
     */
    ,fetch : function(tool,params) {
    	var apiParams = this.getApiParams();
        if (apiParams.stopList) {params.stopList = apiParams.stopList}
        this.update({tool: tool, params: params, title: tool, renderTo: this.body});
    }
    
    ,corpusTypeReader : new Ext.data.JsonReader({
        root : 'corpusTypes.types'
        ,totalProperty : 'corpusTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields))
    ,documentTypeReader : new Ext.data.JsonReader({
        root : 'documentTypes.types'
        ,totalProperty : 'documentTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields))
    
    ,showOptions : function() {
        this.showOptionsWindow({
        	width: 480,
            items : [{
                xtype : 'form',
                labelWidth : 150,
                labelAlign : 'right',
                border : false,
                items: [],
                buttons : [{
                    text : this.localize('ok', 'tool'),
                    iconCls : 'icon-accept',
                    listeners : {
                        click : {
                            fn : function(btn) {
                                var formPanel = btn.findParentByType('form');
                                var form = formPanel.getForm();
                                var stopList = form.findField('stopList');
                                var global = form.findField('globalStopWords').getValue();
                                
                                // make sure we don't have any queries
                                if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
                                else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
                                    stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
                                    stopList.setValue(stopList.lastQuery)
                                }

                                if (form.isDirty()) {
                                	this.setApiParams({
										stopList: stopList.getValue()
									});
                                	if (global) {
    									this.getApplication().applyParamsGlobally({
    										stopList: this.getApiParamValue('stopList')
    									}, true);
    								}
                                }
                                formPanel.findParentByType('window').destroy();
                            },
                            scope : this
                        }
                    }
                }, {
                    text : this.localize('cancel', 'tool'),
                    handler : function(btn) {
                        btn.findParentByType('window').destroy();
                    }
                }]
            }]
        }, true);
    }
    
    
    ,api: {
    	/**
    	 * @property start The index in the results to start at.
    	 * @type Integer
    	 * @default 0
    	 */
    	start: {'default': 0}
	    /**
	     * @property limit The number of words to return in each call.
	     * @type Integer
	     * @default 5
	     */
    	,limit: {'default': 25}
    	/**
    	 * @property extendedSortZscoreMinimum The minimum extended sort Z score that each result should have.
    	 * @type Number
    	 * @default 1
    	 */
        ,extendedSortZscoreMinimum: {'default': 1}
        /**
         * @property docId The document ID to restrict results to.
         * @type String
         * @default null
         */
        ,docId: {'default': null}
        /**
         * @property docIndex The document index to restrict results to.
         * @type Integer
         * @default null
         */
        ,docIndex: {'default': null}
        /**
         * @property stopList The stop list to use to filter results.
         * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
         * @type String
         * @default null
         * @choices stop.en.taporware.txt, stop.fr.veronis.txt
         */
        ,stopList: {
        	'default': null,
        	'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
        }
        /**
         * @property sortDirection The direction to sort results in.
         * @type String
         * @default ASC
         * @choices ASC, DESC
         */
        ,sortDirection: {'default': 'DESC'}
        ,toolType: ['Visualization']
        ,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded', 'DocumentTypeFrequenciesResultLoaded',  'corpusDocumentSelected', 'corpusTypeSelected', 'documentTypeSelected']
    }
    
    ,thumb: {
    	large: 'FeatureClusters.png'
    }
    
    ,i18n : {
        title : {en: "Feature Clusters"}
        ,type : {en: "Visualization"}
        ,help: {
            en: ""
        }
        ,findTerm : {en: 'Find Term'}
        ,clearTerms : {en: 'Clear Terms'}
        ,adaptedFrom: {
            en: ""
        }
        ,exportImg: {en : 'a static image in a new window'}
        ,exportImgSrc: {en: 'HTML source code for this image:'}
    }
});

Ext.reg('voyeurFeatureClusters', Voyeur.Tool.FeatureClusters);

/**
 * @class Voyeur.Tool.KwicsTagger A panel that provides per-document KWIC data in a tabular format.
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.KwicsTagger = Ext.extend(Ext.grid.EditorGridPanel, {
	
	taggedWords : {} // contains keys: value pairs for tagged words
	,categories: {} // contains categories for tagging
	
	,constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		

		var store = new Ext.data.Store({
			reader: Voyeur.data.TypeKwics.reader
			,proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()})
			,listeners : {
				'beforeload' : {
					fn : function(store, options) {
						Ext.applyIf(options.params, this.getApiParams());
						if (!options.params.corpus) {return false;}
						Ext.apply(options.params, {tool: 'TypeKwics'});
					},
					scope : this
				}
				,'loadexception' : {
					fn: function(conn, proxy, response, error){
						this.alertError(response.responseText);
					}
					,scope : this
				}
			}
			,remoteSort: false
			// TODO: restore sorting of KWICS
			,sortInfo : {field : 'tokenId', direction : this.getApiParamValue('sortDirection')}
		});

		this.expander = new Ext.ux.grid.RowExpander({context: this.getApiParamValue('preview')});
		this.expander.tromboneUrl = this.getTromboneUrl();
		
		var forTool = this; /* create a reference that can be used for direct access (instead of relying on scope) */
		
		this.expander.getBodyContent = function(record,index) {
	        var body = '<div id="tmp' + record.id + '">Loading…</div>';
	        var params = forTool.getApiParams();
	        params.start = this.grid.getBottomToolbar().cursor+index;
	        params.limit = 1;
	        params.context = params.preview;
	        params.tool = 'TypeKwics';
	        Ext.Ajax.request({
	           url: forTool.getTromboneUrl()
			   ,params : params
	           ,disableCaching: true
	           ,success: function(response, options) {
	        		var data = Ext.util.JSON.decode(response.responseText);
	        		var recordsObject = Voyeur.data.TypeKwics.reader.readRecords(data);
	        		var record = recordsObject.records[0];
	        		var text = record.get('left')+" <span class='keyword'>"+record.get('middle')+' </span>'+record.get('right');
	        		text = text.replace(/\n\s*/g,'<br />');
	 	            Ext.getDom('tmp' + options.objId).innerHTML = '<div class="x-selectable">'+text+'</div>';
	           }
	           ,failure: function(response, options) {
	           }
			   ,objId: record.id
			   ,type : record.get('type')
	        });

	        return body;
		}
		
		// override class to allow cell selection
		this.expander.getRowClass = function(record, rowIndex, p, ds){
	        p.cols = p.cols-1;
	        var content = this.bodyContent[record.id];
	        if(!content && !this.lazyRender){
	            content = this.getBodyContent(record, rowIndex);
	        }
	        if(content){
	            p.body = content;
	        }
	        return 'x-selectable ' + (this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed');
	    }
		
		this.expander.beforeExpand = function(record, body, rowIndex){
	        if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){
	            body.innerHTML = this.getBodyContent(record, rowIndex);
	            return true;
	        } else{
	            return false;
	        }
	    }
		
		if (!config.plugins) {config.plugins=[]}
		config.plugins.push(this.expander)

		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
			forceFit:true,
			emptyText : this.localize('noResults','tool'),
			deferEmptyText: false
			
//			,hideGroupedColumn: true
		})
		
		
		// create reusable renderer
		Ext.util.Format.comboRenderer = function(combo){
		    return function(value){
		        var record = combo.findRecord(combo.valueField, value);
		        return record ? record.get(combo.displayField) : combo.valueNotFoundText;
		    }
		}
		var combo = new Ext.form.ComboBox({
				    typeAhead: true,
				    triggerAction: 'all',
				    lazyRender:true,
				    mode: 'local',
				    store: new Ext.data.ArrayStore({
				        id: 0,
				        fields: [
				            'myId'
				        ],
				        data: [['inconnu'], ['amitié-amour'], ['amitié-trahison'], ['amitié-confiance']]
				    }),
				    valueField: 'myId',
				    displayField: 'myId'
				 });
				 
	    var cm =  new Ext.grid.ColumnModel([
				this.expander,
				{header: 'Category', dataIndex:  'category', editor: combo,
					renderer: function(text,metadata,record) {return forTool.categories[record.get('docIndex')+'.'+record.get('tokenId')] ? forTool.categories[record.get('docIndex')+'.'+record.get('tokenId')].value : text}, width: 25, align: 'center'},
				{header : 'Left', css : 'direction: rtl;', dataIndex : 'left', toolTip : Voyeur.localization.get('tool.frequencies_document.term'),
					renderer : function(text,metadata,record,rowIndex,colIndex) {
						var counter=0; return Ext.util.Format.htmlEncode(text).replace(/(&)?([a-zA-Z\u00E0-\u00EF]+)/g, function(all,entity,word) {return entity ? all : "<span class='word"+ (forTool.taggedWords[record.get('docIndex')+'.'+record.get('tokenId')+'.'+word.toLowerCase()] ? ' keyword' : '') +"' id='w."+record.get('tokenId')+'.'+record.get('docIndex')+'.'+(counter++)+'.'+metadata.id+'.'+rowIndex+'.'+colIndex+"'>"+word+"</span>"})
					}, align : 'right'},
				{header : 'Keyword', dataIndex : 'middle', toolTip : Voyeur.localization.get('tool.frequencies_document.document_index'), renderer : function(val) {return "<span class='keyword'>" +Ext.util.Format.htmlEncode(val) + '</span>';}, align : 'center', width : 25},
				{header : 'Right', dataIndex : 'right', toolTip : Voyeur.localization.get('tool.frequencies_document.document_name'),
				renderer : function(text,metadata,record,rowIndex,colIndex) {
						var counter=0; return Ext.util.Format.htmlEncode(text).replace(/(&)?([a-zA-Z\u00E0-\u00EF]+)/g, function(all,entity,word) {return entity ? all : "<span class='word"+ (forTool.taggedWords[record.get('docIndex')+'.'+record.get('tokenId')+'.'+word.toLowerCase()] ? ' keyword' : '') +"' id='w."+record.get('tokenId')+'.'+record.get('docIndex')+'.'+(counter++)+'.'+metadata.id+'.'+rowIndex+'.'+colIndex+"'>"+word+"</span>"})
					}},
				{header : 'Position', dataIndex : 'tokenId', toolTip : Voyeur.localization.get('tool.frequencies_document.document_name'), renderer : function(val) {return val}, width : 75, hidden : true}
			])
			
			
		var currentContext = this.getApiParamValue('context');
		var currentPreview = this.getApiParamValue('preview');
		var size = this.getApiParamValue('limit');

		var pagingToolBar = new Ext.PagingToolbar({
			store : store
            ,enableOverflow: true
	        ,pageSize: size
			,displayInfo: true
			,displayMsg : "{0}-{1} of {2}"
			,items : [
				'-',
				{
					text : Voyeur.localization.get(this.xtype+'.context')
					,tooltip : Voyeur.localization.get(this.xtype+'.contextTip')
					,menu : new Ext.menu.Menu({
						items : [
							new Ext.menu.CheckItem({text : '5',checked:currentContext==5,group:'context'})
							,new Ext.menu.CheckItem({text : '10',checked:currentContext==10,group:'context'})
							,new Ext.menu.CheckItem({text : '15',checked:currentContext==15,group:'context'})
							,new Ext.menu.CheckItem({text : '20',checked:currentContext==20,group:'context'})
							,new Ext.menu.CheckItem({text : '25',checked:currentContext==25,group:'context'})
						]
						,listeners : {
							'itemclick' : {
								fn : function(item) {
									this.setApiParams({context: item.text})
									this.getStore().load();
								}
								,scope : this
							}
						}
					})
				}
				,{
					text : Voyeur.localization.get(this.xtype+'.preview')
					,tooltip : Voyeur.localization.get(this.xtype+'.previewTip')
					,menu : new Ext.menu.Menu({
						items : [
							new Ext.menu.CheckItem({text : '10',checked:currentPreview==10,group:'preview'})
							,new Ext.menu.CheckItem({text : '20',checked:currentPreview==20,group:'preview'})
							,new Ext.menu.CheckItem({text : '30',checked:currentPreview==30,group:'preview'})
							,new Ext.menu.CheckItem({text : '40',checked:currentPreview==40,group:'preview'})
							,new Ext.menu.CheckItem({text : '50',checked:currentPreview==50,group:'preview'})
							,new Ext.menu.CheckItem({text : '75',checked:currentPreview==75,group:'preview'})
							,new Ext.menu.CheckItem({text : '100',checked:currentPreview==100,group:'preview'})
							,new Ext.menu.CheckItem({text : '200',checked:currentPreview==200,group:'preview'})
							,new Ext.menu.CheckItem({text : '300',checked:currentPreview==300,group:'preview'})
							,new Ext.menu.CheckItem({text : '400',checked:currentPreview==400,group:'preview'})
							,new Ext.menu.CheckItem({text : '500',checked:currentPreview==500,group:'preview'})
						]
						,listeners : {
							'itemclick' : {
								fn : function(item) {
									this.setApiParams({preview: item.text});
									// look through rows and re-fetch needed previews
									this.getStore().each(function(record, ind) {
										if (this.expander.state[record.id]) {
											this.expander.getBodyContent(record, ind);
										}
									}, this);
								}
								,scope : this
							}
						}
					})
				},'-',{
	                xtype: 'typeSearch',
	                width: 100,
	                parentTool: this,
	                listeners: {
	            	    typeSelected: function(combo, type, record) {
	                		this.setApiParams({type: type});
	                		var docIdTypes = [];
	                		this.getCorpus().getDocuments().eachKey(function(key, doc) {
	                			var docIdType = key+':'+type;
	                			docIdTypes.push(docIdType);
	                		}, this);
	                		this.setApiParams({docIdType: docIdTypes});
							this.getStore().load();
	            	    },
	            	    scope: this
	                }
	            }
			]
		});
		
		Ext.applyIf(config, {
			view: new Ext.grid.GridView(config.viewConfig)
			,iconCls : 'table'
			,loadMask : true
			,stripeRows : true
			,cm: cm
			,store: store
	        ,clicksToEdit: 1
			,bbar : pagingToolBar
	        ,listeners: {
	        	afteredit: function(obj) {
	        		var key = obj.record.get('docIndex')+'.'+obj.record.get('tokenId');
	        		this.categories[key] = obj.value;
					Ext.Ajax.request({
			           url: this.getTromboneUrl()
					   ,params : {
					   		corpus: this.getCorpus().getId()
		        			,tool: 'ParamsData'
		        			,data: 'KwicsCategory'
		        			,key: key
		        			,docId: obj.record.get('docIndex')
		        			,docIndex: obj.record.get('docId')
		        			,tokenId: obj.record.get('tokenId')
		        			,store: true
		        			,value: obj.value
		        			,keyword: obj.record.get('middle')
	        			} // no error reporting yet
					});
	        	}
	        	,scope: this
	        }
	    })
		
		Voyeur.Tool.KwicsTagger.superclass.constructor.apply(this, arguments);
		
		this.addListener('afterrender', function(panel) {
			panel.body.addListener('click', function(e) {
				var target = e.getTarget(null,null,true);
				if (target && target.dom.tagName=='SPAN' && target.dom.className.indexOf("word")>-1) {
					var el = Ext.get(target.dom);
					el.toggleClass("keyword")
					var parts = target.dom.id.split(".")
					var tokenId = parts[1];
					var docIndex = parts[2]
	        		var params = forTool.getApiParams();
	        		var term = el.dom.innerHTML.toLowerCase();
	        		var key = parts[2]+'.'+parts[1]+'.'+term;
	        		var isTagged = el.hasClass('keyword')
	        		var recIndex = forTool.store.findBy(function(record){return record.get('docIndex')==docIndex && record.get('tokenId')==tokenId}, forTool);
	        		var record = forTool.store.getAt(recIndex);
	        		forTool.taggedWords[key] = isTagged
	        		var allElements = Ext.select(".word").elements;
	        		for (var i=0,len=allElements.length;i<len;i++) {
	        			if (allElements[i].innerHTML.toLowerCase()==term) {
	        				Ext.get(allElements[i]).addClass("possibleKeyword");
	        			}
	        		}
	        		
			        Ext.Ajax.request({
			           url: forTool.getTromboneUrl()
					   ,params : {
					   		corpus: params.corpus
		        			,tool: 'ParamsData'
		        			,data: 'KwicsTag'
		        			,key: key
		        			,tokenId: tokenId
		        			,docIndex: docIndex
		        			,docId: record.get('docId')
		        			,keyword: record.get('middle')
		        			,term: term
		        			,store: true
		        			,value: isTagged
	        			} // no error reporting yet
			        });
					
				}
			}), this;
		}, this);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			var params = this.getApiParams();
			if (params.query) {
				// convert a query into a set of types
		        Ext.Ajax.request({
		           url: this.getTromboneUrl()
				   ,params : {
				   		corpus: this.getCorpus().getId()
		    			,tool: 'CorpusTypeFrequencies'
		    			,query: params.query
		    			,limit: this.getApiParamValue('size')
				   }
				   ,success : function(response, options) {
						var store = new Ext.data.JsonStore({
							root : 'corpusTypes.types',
							totalProperty : 'corpusTypes["@totalTypes"]',
							data: Ext.util.JSON.decode(response.responseText),
							fields : Voyeur.data.CorpusTypes.fields
						})
						var types = []
						store.each(function (record) {
							types.push(record.get('type'))
						})
						this.setApiParams({type: types, query: null});
						this.fetchKwicsTags(); // get the tags and the categories before loading any data
				   }
				   ,scope: this
		        });
				
			}
			else {
				this.fetchKwicsTags(); // get the tags and the categories before loading any data
			}
		}, this);

		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType,  type: null})
			if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
			this.getStore().load();
		}, this);

		/**
		 * @event documentTypesSelected
		 * @type listener
		 */
		this.addListener('documentTypesSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType,  type: null})
			this.getStore().load();
		}, this);
		
		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data) {
			if (this.getCorpus().getSize() == 1) {
				var docIdType = this.getCorpus().getDocument(0).getId()+':'+data.type;
				this.setApiParams({docIdType: docIdType,  type: null})
				if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
				this.getStore().load();
			}
		}, this);
		
		/**
		 * @event corpusTypesSelected
		 * @type listener
		 */
		this.addListener('corpusTypesSelected', function(src, data) {
			if (this.getCorpus().getSize() == 1) {
				var docId = this.getCorpus().getDocument(0).getId();
				var docIdType = [];
				for (var i = 0; i < data.type.length; i++) {
					docIdType.push(docId+':'+data.type[i]);
				}
				this.setApiParams({docIdType: docIdType,  type: null})
				if (data.tokenIdStart) {this.setApiParams({tokenIdStart: data.tokenIdStart})}
				this.getStore().load();
			}
		}, this);
		
//		this.addListener('tokenSelected', function(src, data) {
//			this.setApiParams({docIdType: data.docIdType,  type: null})
//			this.getStore().load();
//		}, this);

	}
	
	,fetchKwicsTags: function() {
		// get the tags and then the categories and then load kwics
        Ext.Ajax.request({
           url: this.getTromboneUrl()
		   ,params : {
		   		corpus: this.getCorpus().getId()
    			,tool: 'ParamsData'
    			,data: 'KwicsTag'
    			,retrieve: true
		   }
		   ,success : function(response, options) {
		   		var data = Ext.util.JSON.decode(response.responseText);
		   		if (data.paramsData && data.paramsData.data) {
		   			var d = data.paramsData.data;
		   			for (var i=0,leni=d.length;i<leni;i++) {
		   				var entry = d[i].entries
		   				var hash = {}
		   				for (var j=0,lenj=entry.length;j<lenj;j++) {
		   					hash[entry[j][0]] = entry[j][1][0]
		   				}
		   				if (hash.value=='true') {
		   					this.taggedWords[hash.key] = hash;
		   				}
		   			}
		   		}
				this.fetchCategories();
		   }
		   ,scope: this
        });
	}
	
	,fetchCategories: function() {
		// after we have tags, get the categories then load kwics
        Ext.Ajax.request({
           url: this.getTromboneUrl()
		   ,params : {
		   		corpus: this.getCorpus().getId()
    			,tool: 'ParamsData'
    			,data: 'KwicsCategory'
    			,retrieve: true
		   }
		   ,success : function(response, options) {
		   		var data = Ext.util.JSON.decode(response.responseText);
		   		if (data.paramsData && data.paramsData.data) {
		   			var d = data.paramsData.data;
		   			for (var i=0,leni=d.length;i<leni;i++) {
		   				var entry = d[i].entries
		   				var hash = {}
		   				for (var j=0,lenj=entry.length;j<lenj;j++) {
		   					hash[entry[j][0]] = entry[j][1][0]
		   				}
		   				this.categories[hash.key] = hash;
		   			}
		   		}
				this.getStore().load(); // finally, load kwics
		   }
		   ,scope: this
        });
	}

	,api: {
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property context The number of surrounding words to display in the collapsed view.
		 * @type Integer
		 * @default 5
		 */
		,context: {'default': 20}
		/**
		 * @property preview The number of surround words to display in the expanded view.
		 * @type Integer
		 * @default 50
		 */
		,preview: {'default': 50}
		//,sortBy: {'default': 'tokenId'}
		//,sortDirection: {'default': 'desc'}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property type The corpus query to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,query: {'default': null}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property tokenIdStart The token id to start at.
		 * @type Integer
		 * @default null
		 */
		,tokenIdStart: {'default': null}
		,toolType: ['Table']
//		,visibleColumn: {'default': ['type','rawFreq','relativeFreqs']}
		,listeners: ['CorpusSummaryResultLoaded', 'documentTypeSelected', 'documentTypesSelected', 'corpusTypeSelected', 'corpusTypesSelected']
		,dispatchers: ['tokenSelected']
	}
	
	,thumb: {
		large: 'KwicsTagger.png'
	}

	// private localization variables
	,i18n : {
		title : {en: "Keywords in Context"}
		,help : {en: "This tool shows each occurrence of a selected term with some context (words to the left and right of the keyword)."}
		,context : {en: "Context"}
		,contextTip : {en: "This value determines how many words to display on the left and the right of each keyword."}
		,preview : {en: "Preview"}
		,previewTip : {en: "This value determines how many words to display on the left and the right of each keyword in the preview section (the preview is available by expanding the keyword row)."}
		,selectRowsForResults : {en: "Generate results by selecting rows from the <i>Keyword in Context</i> tool."}
	}
});

Ext.reg('voyeurKwicsTagger', Voyeur.Tool.KwicsTagger);

/**
 * @class Voyeur.Tool.PaperDrill
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Andrew MacDonald
 */
Voyeur.Tool.PaperDrill = Ext.extend(Ext.Panel, {
	initialized: false,
	
	graph: null,
	
	colors: ['#7474B5', '#8BA353', '#BD9D3C', '#AB4B4B', '#AE3D9B'],
	
	store: null,
	
	fetchingNodes: false,
	
	getObjectId : function() {
		return this.id.replace(/-/g,'_')+'_paperdrill';
	},
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
				
		Voyeur.Tool.PaperDrill.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			this.initApp();
		}, this);
	},
	
	nodeIndex: 0,
	
	createSubTree: function(parentId, orn) {
		var dataObj = {};
		if (parentId == null) {
			parentId = 'node'+this.nodeIndex;
			this.nodeIndex++;
		} else {
			dataObj = {
				'$orn': orn
			};
		}
		
		var json = {
			id: parentId,
			name: 'node '+Math.round(Math.random()*1000*Math.random()*100),
			data: dataObj,
			children: []
		};

		var limit = Math.max(2, Math.ceil(Math.random() * 5));
		for (var i = 0; i < limit; i++) {
			var c = this.colors[Math.round(Math.random()*(this.colors.length-1))];
			var name = 'node '+Math.round(Math.random()*1000*Math.random()*100);
//			var width = this.getTextWidth(name);
			json.children.push({
				id: 'node'+this.nodeIndex,
				name: name,
				data: {
//					$width: width,
					$orn: orn,
					$color: c,
					color: c
				}
			});
			this.nodeIndex++;
		}
		return json;
	},
	
	getTextWidth: function(str) {
		var size = Ext.util.TextMetrics.measure('textMetrics', str);
		return size.width;
	},
	
	data: {},
	
	initApp: function() {
		var id = this.getObjectId();
		
        var initPaperDrillApp = function() {
            try {
            	var ua = navigator.userAgent;
				var iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i);
				var typeOfCanvas = typeof HTMLCanvasElement;
				var nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function');
				var textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function');
				//I'm setting this based on the fact that ExCanvas provides text support for IE
				//and that as of today iPhone/iPad current text support is lame
				var labelType = (!nativeCanvasSupport || (textSupport && !iStuff))? 'Native' : 'HTML';
				var nativeTextSupport = labelType == 'Native';
				var useGradients = nativeCanvasSupport;
				var animate = !(iStuff || !nativeCanvasSupport);
            	
				var me = this;
				
				// set up textMetrics div
				var tm = Ext.get('textMetrics');
				tm.setStyle({
					fontFamily: 'tahoma,arial,verdana,sans-serif',
        			fontSize: '12px',
        			visibility: 'hidden'
				});
				
		        // create some data
				this.nodeIndex = 0;
				this.data = this.createSubTree(null, 'left');
				var rightTree = this.createSubTree('node0', 'right');
				for (var i = 0; i < rightTree.children.length; i++) {
					this.data.children.push(rightTree.children[i]);
				}
				
				$jit.ST.Plot.NodeTypes.implement({
					'roundedrectangle': {
						'render': function(node, canvas, animating) {
							var pos = node.pos.getc(true), data = node.data;
							var width  = node.getData('width'), height = node.getData('height');
							var algnPos = this.getAlignedPos(pos, width, height);
							var ctx = canvas.getCtx(), ort = this.config.orientation;
							ctx.beginPath();
							var r = 5; //corner radius
							var x = algnPos.x;
							var y = algnPos.y;
							var h = height;
							var w = width;
							ctx.moveTo(x + r, y);
							ctx.lineTo(x + w - r, y);
							ctx.quadraticCurveTo(x + w, y, x + w, y + r);
							ctx.lineTo(x + w, y + h - r);
							ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
							ctx.lineTo(x + r, y + h);
							ctx.quadraticCurveTo(x, y + h, x, y + h - r);
							ctx.lineTo(x, y + r);
							ctx.quadraticCurveTo(x, y, x + r, y);
							ctx.fill();
						},
						'contains': function(node, pos) {
						    var width = node.getData('width'),
						        height = node.getData('height'),
						        npos = this.getAlignedPos(node.pos.getc(true), width, height);
						    return this.nodeHelper.rectangle.contains({x:npos.x+width/2, y:npos.y+height/2}, pos, width, height);
						}
				    }
				}); 
				
            	this.graph = new $jit.ST({
	            	injectInto: id,
	            	multitree: true,
	                duration: 500,
	                transition: $jit.Trans.Quart.easeInOut,
	                levelDistance: 70,
	                constrained: false,
	                levelsToShow: 100,
	                Navigation: {
	                  enable:true,
	                  panning:true
	                },
	                Node: {
	                    autoWidth: true,
	                	height: 30,
	                    type: 'roundedrectangle',
	                    color: '#ccc',
	                    overridable: true
	                },
	                Edge: {
	                    type: 'bezier',
	                    color: '#ccc',
	                    overridable: true
	                },
	                Label: {
            			overridable: true,
            		    type: labelType,
            		    size: 11,
            		    color: '#000',
            		    textBaseline: 'middle'
            		},
            		Tips: {
            		    enable: true,
            		    onShow: function(tip, node) {
            		    	var tipHTML = '<div style="font-weight: bold;">'+node.name+'</div>';
            		        tip.innerHTML = tipHTML;
            		    }
            		},
            		onBeforePlotNode: function(node) {
            			if (node.collapsed) {
                            node.data.$color = "#ccc";
            			} else if (node.selected) {
            				node.data.$color = "#ff7";
                        } else {
                        	node.data.$color = node.data.color;
                        }
            		},
            		onBeforePlotLine: function(adj){
                        if (adj.nodeFrom.selected && adj.nodeTo.selected) {
                            adj.data.$color = "#aaa";
                            adj.data.$lineWidth = 2;
                        } else {
                            delete adj.data.$color;
                            delete adj.data.$lineWidth;
                        }
                    },
            		Events: {
            		    enable: true,
            		    onMouseEnter: function(node, eventInfo, e) {
            		    	if (!me.fetchingNodes) {
            		    		me.graph.canvas.getElement().style.cursor = 'pointer';
            		    	}
            		    },
            		    onMouseLeave: function(node, eventInfo, e) {
            		    	if (!me.fetchingNodes) {
            		    		me.graph.canvas.getElement().style.cursor = '';
            		    	}
            		    },
            		    onClick: function(node, eventInfo, e) {
            		    	if (!node) return;
            		    	if (node.collapsed) {
            		    		me.graph.op.expand(node, {
                		    		type: 'animate'
                		    	});
            		    	} else if (node.getSubnodes().length == 1){
	            		    	me.fetchingNodes = true;
	            		    	me.graph.canvas.getElement().style.cursor = 'wait';
	            		    	var orientation = node.data.$orn;
	            		    	
//	            		    	me.graph.addSubtree(me.createSubTree(node.id, orientation), 'replot');
//	            		    	me.graph.onClick(node.id, {
//	            		    		onComplete: function() {
//	            		    			me.graph.canvas.getElement().style.cursor = '';
//	            		    			me.fetchingNodes = false;
//	            		    		}
//	            		    	});
	            		    	
	            		    	me.graph.onClick(node.id, {
	            		    		onComplete: function() {
	            		    			me.graph.addSubtree(me.createSubTree(node.id, orientation), 'animate', {
	            		    				onComplete: function() {
	            		    					me.graph.canvas.getElement().style.cursor = '';
	    	            		    			me.fetchingNodes = false;
	            		    				}
	            		    			});
	            		    		}
	            		    	});
	            		    	
            		    	}
            		    	
            		    },
            		    onRightClick: function(node, eventInfo, e) {
            		    	if (!node) return;
            		    	if (node.collapsed) {
            		    		me.graph.op.expand(node, {
                		    		type: 'animate'
                		    	});
            		    	} else {
	            		    	me.graph.op.contract(node, {
	            		    		type: 'animate'
	            		    	});
            		    	}
            		    }
            		}
            	});
            	
            	this.graph.loadJSON(this.data);
            	this.graph.compute();
            	this.graph.select(this.graph.root);
            } catch(e) {
            	console.log(e);
                setTimeout(initPaperDrillApp.createDelegate(this), 500);
            }
        }
        var inserts = '<link href="'+this.getApplication().getBaseUrl()+'resources/tools/PaperDrill/PaperDrill.css" type="text/css" rel="stylesheet" />';
        if (Ext.isIE) {
        	inserts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/excanvas/excanvas.js"></script>'; 
        }
        inserts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/jit/jit_spacetree.js?cd='+Math.random()+'"></script>'+
        '<div id="'+id+'" style="height: 100%; width: 100%;"></div><div id="textMetrics"></div>';
        this.body.update(inserts, true, initPaperDrillApp.createDelegate(this));
	},
	
	api: {
		/**
		 * @property bins How many "bins" to separate a document into.
		 * @type Integer
		 * @default 50
		 */
		bins: {'default': 50}
    	/**
    	 * @property query A string to search for in a document.
    	 * @type String
    	 * @default null
    	 */
		,query: {'default': null}
		/**
		 * @property noAnchor True to search for a query anywhere in a word, false to anchor the search to the beginning of a word.
		 * @type Boolean
		 * @default null
		 */
		,noAnchor: {'default': null}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default undefined
		 */
		,docId: {'default': undefined}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default undefined
		 */
		,docIndex: {'default': undefined}
		,typeFilter: {'default': null}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
		,dispatchers: ['documentTypeSelected', 'documentTypesSelected']
	}
	

	,thumb: {
		large: 'PaperDrill.png'
	}
	
    ,i18n : {
        title : {en: 'PaperDrill'}
        ,type : {en: 'Visualization'}
        ,help: {en: ''}
        ,adaptedFrom: {en: ''}
    }
});

Ext.reg('voyeurPaperDrill', Voyeur.Tool.PaperDrill);
/**
 * @class Voyeur.Tool.DocumentInputAdd A panel that provides widgets for creating and updating a corpus.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.DocumentInputAdd = Ext.extend(Ext.Panel, {
	constructor : function(config) {
	
		Ext.apply(this, new Voyeur.Tool(config, this));

		var xtype = config.xtype;
		Ext.applyIf(config, {
			bodyStyle: 'text-align: center;',
			items: [{
				xtype: 'textarea',
				id: 'input',
				name: 'input',
				cls: 'input_box',
				hideLabel: true,
				width:'100%'
				,emptyText: this.localize('input_label')
			}, {
				xtype: 'button',
				id: 'reveal',
				text: this.localize('reveal'),
				handler: function(btn){
					var input_value = Ext.getCmp('input').getValue().replace(/^\s+/, '');
					if (this.getCorpus().isEmpty() && input_value.length == 0) {
						return this.alertError({
							msg: this.localize('empty_input_error')
							,animEl: btn.id
							,fn: function(){
								var el = Ext.getCmp('input').getEl();
								el.frame("ff0000");
							}
						});
					}
					var params = this.getApiParams(true);
					params.input = input_value;
					params.corpusCreateIfNotExists = true;
					this.update({
						tool : 'CorpusSummary'
						,params: params
					})
				},
				scope: this
			}]
			,bbar: [{
				xtype: 'tbbutton'
				,text: this.localize('upload')
				,tooltip : this.localize('uploadTip')
				,iconCls: 'tbupload'
				,handler : function(b, e) {
					if (Ext.getCmp('input').getValue()) {
						Ext.Msg.show({
							title : this.localize('uploadIgnoresInputTitle')
							,msg : this.localize('uploadIgnoresInputMsg')
							,buttons : Ext.Msg.YESNO
							,icon: Ext.MessageBox.QUESTION
							,fn: function(btn) {
								if (btn=='yes') {this.showFileUpload(b.el);}
							}
							,scope: this
						})
					}
					else {this.showFileUpload(b.el);}
				}
				,scope : this
			},
			{
				xtype: 'tbbutton'
				,text: this.localize('openCorpus')
				,tooltip : this.localize('openCorpusTip')
				,iconCls: 'icon-folder'
				,handler : function(b, e) {
					if (Ext.getCmp('input').getValue()) {
						Ext.Msg.show({
							title : this.localize('openCorpusIgnoresInputTitle')
							,msg : this.localize('openCorpusIgnoresInputMsg')
							,buttons : Ext.Msg.YESNO
							,icon: Ext.MessageBox.QUESTION
							,fn: function(btn) {
								if (btn=='yes') {this.showOpenCorpus(b.el);}
							}
							,scope: this
						})
					}
					else {this.showOpenCorpus(b.el);}
				}
				,scope : this
			}]
		});
		Voyeur.Tool.DocumentInputAdd.superclass.constructor.apply(this, arguments);
		
	}

	,showOpenCorpus : function(btnEl) {
		var win = new Ext.Window({
			modal: true
			,title: this.localize('openCorpus')
			,width: 400
			,items: [{
    				xtype : 'form',
    				labelWidth : 50,
    				labelAlign : 'right',
    				padding: '5px 0 0 0',
    				border : false,
    				items : [{
    					xtype : 'combo',
    					id : 'comparisonCorpus',
//    					value : this.getStore().baseParams.comparisonCorpus,
    					fieldLabel : '<span ext:qtip="'
    							+ this.localize('openCorpusTip') + '">'
    							+ this.localize('openCorpus') + '</span>',
    					loadingText : this.localize('loading', 'tool'),
    					width : 300,
    					store : this.getApplication().getCorporaStore()
    				    ,mode:'local'
    				    ,selectOnFocus : true
    				    ,displayField: 'label'
    				    ,triggerAction: 'all'
    				    ,valueField: 'id'
    				    ,emptyText: this.localize('none','tool')
    				}],
    				buttons : [{
    					text : this.localize('ok', 'tool'),
    					iconCls : 'icon-accept',
    					listeners : {
    						click : {
    							fn : function(btn) {
									var formPanel = btn.findParentByType('form');
									var form = formPanel.getForm();
									var comparisonCorpus = form.findField('comparisonCorpus');

									// make sure we don't have any queries
									if (comparisonCorpus.getValue() && !comparisonCorpus.getRawValue()) {comparisonCorpus.setValue('');}
									else if (comparisonCorpus.lastQuery && comparisonCorpus.lastQuery!=comparisonCorpus.getValue()) {
										comparisonCorpus.getStore().loadData({corpora: {corpora: [{id: comparisonCorpus.lastQuery, label: comparisonCorpus.lastQuery, description: ''}]}}, true);
										comparisonCorpus.setValue(comparisonCorpus.lastQuery)
									}
			
									if (form.isDirty()) {
										this.update({params: {corpus: comparisonCorpus.getValue()}, tool: 'CorpusSummary'})
									}
									formPanel.findParentByType('window').close();
						
    							},
    							scope : this
    						}
    					}
    				},{
    					text : this.localize('cancel', 'tool'),
    					handler : function(btn) {
    						btn.findParentByType('window').close();
    					}
    				}]
     			}
			]
		});
		win.show(btnEl);
	}
	
	/**
	 * Show the file upload panel.
	 */
	,showFileUpload : function(btnEl) {
		 var win = new Ext.Window({
		 width:180
		 ,minWidth:165
		 ,height:220
		 ,minHeight:200
		 ,stateful:true
		 ,modal:true
		 ,layout:'fit'
		 ,border:false
		 ,closable:true
		 ,title:this.localize('upload')
		 ,items:[{
			xtype:'uploadpanel'
			,addText:this.localize('fileAdd')
			,clickRemoveText : this.localize('clickRemoveText')
			,clickStopText : this.localize('clickStopText')
			,emptyText : this.localize('emptyText')
			,errorText : this.localize('errorText')
			,fileQueuedText : this.localize('fileQueuedText')
			,fileDoneText : this.localize('fileDoneText')
			,fileFailedText : this.localize('fileFailedText')
			,fileStoppedText : this.localize('fileStoppedText')
			,fileUploadingText : this.localize('fileUploadingText')
			,removeAllText : this.localize('removeAllText')
			,removeText : this.localize('removeText')
			,stopAllText : this.localize('stopAllText')
			,uploadText : this.localize('upload')
			,buttonsAt:'tbar'
			,id:'uppanel'
			,url:this.getTromboneUrl()
			,path:'root'
			,maxFileSize:1048576
			,enableProgress:false
			,singleUpload:true
			,forTool: this
			,listeners: {
				'render': {
					fn: function(panel) {
						
						// we need to set the corpus before uploading so that all files are added to the same corpus
						panel.uploader.baseParams = this.getApiParams();
						panel.uploader.baseParams.corpusCreateIfNotExists = true;

						// we'll handle the results ourselves because we want different parsing of results and behaviour
						panel.uploader.uploadCallback = function(options, success, response) {
							var uploader = panel.uploader;
							uploader.upCount--;
							uploader.form = false;
							var r = response.responseText;
							// for some reason FF wraps the response in <pre> tag
							r = r.replace(/<\/?pre.*?>/g,'');
							r = Ext.decode(r);
							if (r.error) {
								uploader.processFailure(options, response, r.error);
							}
							else {
								uploader.processSuccess(options, response, r);
								if (uploader.upCount==0) { // all finished
									/**
									 * @event CorpusSummaryResultLoaded
									 * @param {Voyeur.Tool.DocumentInputAdd} tool
									 * @param {Object} response
									 * @type dispatcher
									 */
									panel.forTool.getApplication().dispatchEvent('CorpusSummaryResultLoaded', panel.forTool, r);
									panel.ownerCt.destroy();
								}
							}
							uploader.fireFinishEvents(options);
						}
					}
					,scope: this
				}
			}
		 }]
		 });
		 win.show(btnEl);
	}

	,showOptions : function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
						xtype: 'combo',
						name: 'inputFormat',
					    store: new Ext.data.ArrayStore({
					        id: 0,
					        fields: [
					            'value',
					            'displayText'
					        ],
					        data: [['XML'], ['RSS2']]
					    }),
					    displayField:'value',
					    valueField: 'value',
					    mode: 'local',
					    value: this.getApiParamValue('inputFormat'),
//					    triggerAction: 'all',
					    emptyText: this.localize('emptyInputFormat'),
					    editable: false,
					    fieldLabel: '<span ext:qtip="'
							+ this.localize('inputFormatTip') + '">'
							+ this.localize('inputFormat') + '</span>'
					},{
    				xtype: 'fieldset',
                    title: 'XML Options',
                    collapsible: true,
                    items: [
						{
						    xtype     : 'textfield',
						    name      : 'xmlContentXpath',
						    value	  : this.getApiParamValue('xmlContentXpath'),
						    fieldLabel: '<span ext:qtip="'
    							+ this.localize('xmlContentXpathTip') + '">'
    							+ this.localize('xmlContentXpath') + '</span>'
						},{
						    xtype     : 'textfield',
						    name      : 'xmlAuthorXpath',
						    value	  : this.getApiParamValue('xmlAuthorXpath'),
						    fieldLabel: '<span ext:qtip="'
    							+ this.localize('xmlAuthorXpathTip') + '">'
    							+ this.localize('xmlAuthorXpath') + '</span>'
						},{
						    xtype     : 'textfield',
						    name      : 'xmlTitleXpath',
						    value	  : this.getApiParamValue('xmlTitleXpath'),
						    fieldLabel: '<span ext:qtip="'
    							+ this.localize('xmlTitleXpathTip') + '">'
    							+ this.localize('xmlTitleXpath') + '</span>'
						},{
						    xtype     : 'textfield',
						    name      : 'xmlDocumentsXpath',
						    value	  : this.getApiParamValue('xmlDocumentsXpath'),
						    fieldLabel: '<span ext:qtip="'
    							+ this.localize('xmlDocumentsXpathTip') + '">'
    							+ this.localize('xmlDocumentsXpath') + '</span>'
						}
    				]}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var inputFormat = form.findField('inputFormat');
								var xmlContentXpath = form.findField('xmlContentXpath');
								var xmlAuthorXpath = form.findField('xmlAuthorXpath');
								var xmlTitleXpath = form.findField('xmlTitleXpath');
								var xmlDocumentsXpath = form.findField('xmlDocumentsXpath');
								this.setApiParams({
									inputFormat: inputFormat.getValue(),
									xmlContentXpath: xmlContentXpath.getValue(),
									xmlAuthorXpath: xmlAuthorXpath.getValue(),
									xmlTitleXpath: xmlTitleXpath.getValue(),
									xmlDocumentsXpath: xmlDocumentsXpath.getValue()
								});
								
								formPanel.findParentByType('window').destroy();
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								var form = btn.findParentByType('form').getForm();
								form.findField('inputFormat').setValue(this.getApiParamDefaultValue('inputFormat'));
								form.findField('xmlContentXpath').setValue(this.getApiParamDefaultValue('xmlContentXpath'));
								form.findField('xmlAuthorXpath').setValue(this.getApiParamDefaultValue('xmlAuthorXpath'));
								form.findField('xmlTitleXpath').setValue(this.getApiParamDefaultValue('xmlTitleXpath'));
								form.findField('xmlDocumentsXpath').setValue(this.getApiParamDefaultValue('xmlDocumentsXpath'))
							},
							scope : this
						}
					}
	
				}]
			}]
		})

	}
	
	,api: {
		/**
		 * @property inputFormat The format of the corpus to import.  Supported formats are XML and RSS2.
		 * @type String
		 * @default null
		 * @choices TEXT, HTML, XML, RSS2, OLDBAILEY, ZIP, TAR, MSWORD, MSWORDX, PDF, RTF
		 */
		'inputFormat': {
			'default': null,
			'choices': ['XML', 'RSS2']
		}
		/**
		 * @property xmlContentPath An XPath for locating the corpus content.
		 * @type String
		 * @default null
		 */
		,'xmlContentXpath': {'default': null}
		/**
		 * @property xmlAuthorXpath An XPath for locating the corpus author.
		 * @type String
		 * @default null
		 */
		,'xmlAuthorXpath': {'default': null}
		/**
		 * @property xmlTitleXpath An XPath for locating the corpus title.
		 * @type String
		 * @default null
		 */
		,'xmlTitleXpath': {'default': null}
		/**
		 * @property xmlDocumentsXpath An XPath for locating the corpus documents.
		 * @type String
		 * @default null
		 */
		,'xmlDocumentsXpath': {'default': null}
		,toolType: ['Document']
		,dispatchers: ['CorpusSummaryResultLoaded']
	}
	
	,thumb: {
		large: 'DocumentInputAdd.png'
	}
	
	// private localization variables
	,i18n : {
		title : {en: "Add Texts"}
		,input_label : {en: "Type in one ore more URLs on separate lines or paste in a full text."}
		,reveal : {en: "reveal"}
		,inputFormat: {en: 'Input Format'}
		,inputFormatTip: {en: 'Defines the input format. In most cases the format can be reliably guessed (by filename or content type). However, in some cases the format needs to be specified.'}
		,emptyInputFormat: {en: 'Leave blank for auto-detect'}
		,xmlContentXpath : {en: "XPath to Content"}
		,xmlContentXpathTip : {en: "The XPath query used to specify which nodes should be used for content (by default all nodes are used). Note that this XPath is relative to each document when using split documents within one file."}
		,xmlTitleXpath : {en: "XPath to Title"}
		,xmlTitleXpathTip : {en: "The XPath query used to specify the node that contains the title of the document(s) – by default the filename is used for single documents and the start of the content is used for split documents. Note that this XPath is relative to each document when using split documents within one file."}
		,xmlAuthorXpath : {en: "XPath to Author"}
		,xmlAuthorXpathTip : {en: "The XPath query used to specify the node that contains the author of the document(s) – by default the author remains unknown. Note that this XPath is relative to each document when using split documents within one file."}
		,xmlDocumentsXpath : {en: "XPath to Documents"}
		,xmlDocumentsXpathTip : {en: "A single file can be split into multiple documents according to an XPath query defined by this option (an example would be taking an RSS feed and dividing each post into a separate document)."}
		,upload : {en: "Upload"}
		,uploadTip : {en : "Click this to upload one or more files from your local computer."}
		,uploadIgnoresInputTitle : {en: "Confirmation Required"}
		,uploadIgnoresInputMsg : {en: "You cannot upload a file <b>and</b> specify other text(s) to be added. Do you wish to continue with uploading one or more files (the contents of the <i>Add Texts</i> box will be ignored)?"}
		,openCorpus : {en: "Open"}
		,openCorpusTip : {en : "Click this to open a pre-defined corpus."}
		,openCorpusIgnoresInputTitle : {en: "Confirmation Required"}
		,openCorpusIgnoresInputMsg : {en: "You cannot open an existing corpus <b>and</b> specify other text(s) to be added. Do you wish to continue with opening a corpus (the contents of the <i>Add Texts</i> box will be ignored)?"}
		,fileAdd : {en: "Add"}
		,clickRemoveText : {en: "Click to remove"}
		,clickStopText : {en: "Click to stop"}
		,emptyText : {en: "No files"}
		,errorText : {en: "Error"}
		,fileQueuedText : {en: "File <b>{0}</b> is queued for upload"}
		,fileDoneText : {en: "File <b>{0}</b> has been successfully uploaded"}
		,fileFailedText : {en: "File <b>{0}</b> failed to upload"}
		,fileStoppedText : {en: "File <b>{0}</b> stopped by user"}
		,fileUploadingText : {en: "Uploading file <b>{0}</b>"}
		,removeAllText : {en: "Remove All"}
		,removeText : {en: "Remove"}
		,stopAllText : {en: "Stop All"}
		,empty_input_error : {en: "Please provide input before continuing."}
		,help : {en: "<p>This tool allows you to specify the origin of the texts to be analyzed.</p><p>You can provide one or more URLs (on separate lines). Each available URL will be fetched and added as a document. Supported formats include plain text (.txt), HTML (.html), XML (.xml), MS Word (.doc, .docx), RTF (.rtf), and PDF (.pdf).</p><p>You can also copy and paste a single text into the box (this may be preferable if you're experiencing problems with character encoding). You will be able to add additional texts later.</p>"}
	}
});

Ext.reg('voyeurDocumentInputAdd', Voyeur.Tool.DocumentInputAdd);

/**
 * @class Voyeur.Tool.EntitiesBrowser Browse named entities using a force directed layout.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.EntitiesBrowser = Ext.extend(Ext.Panel, {
	initialized: false,
	
	getObjectId: function() {
		return this.id.replace(/-/g,'_')+'_entities';
	},
	
	graph: null,
	
	data: null,
	
	fetchingNodes: false,
	
	getCategory: function(category) {
		for (var i = 0; i < this.data.children.length; i++) {
			var c = this.data.children[i];
			if (c.id == category) {
				return c;
			}
		}
		return null;
	},
	
	colourMapping: {
		'Organization': '#921B2A',
		'Company': '#A42D37',
		'Facility': '#AD363E',
		'Person': '#B94147',
		
		'Technology': '#A83D29',
		'FieldTerminology': '#B44732',
		
		'Country': '#3A6B44',
		'StateOrCounty': '#4E7F57',
		'City': '#4E7F57',
		
		'TelevisionShow': '#79851C',
		'PrintMedia': '#79851C',
		
		'HealthCondition': '#AA8B1C'
	},
	
	defaultColour: '#1E4176',
	
	getColour: function(category) {
		return this.colourMapping[category] || this.defaultColour;
	},
	
	processRecords: function(records) {
		var hasPerson = false;
		var maxNodes = {value: 0};
		
    	var rootNode = {
    		id: 'Entities',
    		name: 'Entities',
    		data: {
    			root: true,
    			'$color': this.getColour(null)
    		},
    		children: []
    	};
    	
    	// build categories
    	var categories = {};
    	for (var i=0,len=records.length;i<len;i++) {
    		var category = records[i].get('category');
    		if (category == 'Person') hasPerson = true;
    		if (categories[category] == null) categories[category] = [];
    		categories[category].push(records[i]);
    		if (categories[category].length > maxNodes.value) {
    			maxNodes = {category: category, value: categories[category].length};
    		}
    	}
    	
    	for (var category in categories) {
    		var entities = categories[category];
    		var catNode = {
    			id: category,
    			name: category,
    			expand: (hasPerson && category == 'Person') || (!hasPerson && category == maxNodes.category),
    			data: {
    				'$color': this.getColour(category)
    			},
    			children: []
    		};
    		
    		for (var i = 0; i < entities.length; i++) {
    			var e = entities[i];
    			catNode.children.push({
    				id: e.get('text'),
    				name: e.get('text'),
    				data: {
    					rawFreq: e.get('rawFreq'),
    					relevance: e.get('relevance'),
    					disambiguate: e.get('@disambiguate'),
    					'$color': this.getColour(e.get('category'))
    				}
    			});
    		}
    		rootNode.children.push(catNode);
    		
    	}
    	return rootNode;
    },
    
    initApp: function(records) {
    	var id = this.getObjectId();
        
        var initEntitiesApp = function() {
            try {
            	// from thejit.org force directed example
				var ua = navigator.userAgent;
				var iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i);
				var typeOfCanvas = typeof HTMLCanvasElement;
				var nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function');
				var textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function');
				//I'm setting this based on the fact that ExCanvas provides text support for IE
				//and that as of today iPhone/iPad current text support is lame
				var labelType = (!nativeCanvasSupport || (textSupport && !iStuff))? 'Native' : 'HTML';
				var nativeTextSupport = labelType == 'Native';
				var useGradients = nativeCanvasSupport;
				var animate = !(iStuff || !nativeCanvasSupport);
				
            	var iterations = 50;
            	if (Ext.isWebKit || Ext.isOpera) iterations = 300;
            	else if (Ext.isGecko) iterations = 150;
            	
            	if (this.data == null) {
	            	this.data = this.processRecords(records);
            	}
            	var rootClone = Ext.ux.clone(this.data);
            	for (var i = 0; i < rootClone.children.length; i++) {
            		var category = rootClone.children[i];
            		if (!category.expand) {
            			delete category.children;
            		}
            	}
            	
            	var me = this;
            	
            	this.graph = new $jit.ForceDirected({
            		injectInto: id,
            		Navigation: {
            		    enable: true,
            		    //Enable panning events only if we're dragging the empty
            		    //canvas (and not a node).
            		    panning: 'avoid nodes',
            		    zooming: 50 //zoom speed. higher is more sensible
            		},
            		// Change node and edge styles such as
            		// color and width.
            		// These properties are also set per node
            		// with dollar prefixed data-properties in the
            		// JSON structure.
            		Node: {
            		    overridable: true,
            		    type: 'circle',
            		    dim: 3
            		},
            		Edge: {
            		    overridable: true,
            		    color: '#527CBB',
            		    lineWidth: 0.4
            		},
            		//Native canvas text styling
            		Label: {
            			overridable: true,
            		    type: labelType, //Native or HTML
            		    size: 10,
            		    color: '#000000'
            		},
            		//Add Tips
            		Tips: {
            		    enable: true,
            		    onShow: function(tip, node) {
            		        //display node info in tooltip
            		    	var tipHTML = '<div style="font-weight: bold;">'+node.name+'</div>';
            		    	if (node.data.rawFreq) {
	        		            tipHTML += '<div>'+me.localize('rawFrequency')+': '+node.data.rawFreq+'</div>'
	        		        	+ (node.data.relevance ? '<div>'+me.localize('relevance')+': '+node.data.relevance+'</div>' : '');
            		    	} else {
            		    		//count connections
                		        var count = 0;
                		        node.eachAdjacency(function() { count++; });
                		        tipHTML += '<div>'+me.localize('connections')+': '+count+'</div>'
            		    	}
            		        tip.innerHTML = tipHTML;
            		    }
            		},
            		// Add node events
            		Events: {
            		    enable: true,
            		    //Change cursor style when hovering a node
            		    onMouseEnter: function() {
            		        me.graph.canvas.getElement().style.cursor = 'pointer';
            		    },
            		    onMouseLeave: function() {
            		    	if (!me.fetchingNodes) {
            		    		me.graph.canvas.getElement().style.cursor = '';
            		    	}
            		    },
            		    //Update node positions when dragged
//            		    onDragMove: function(node, eventInfo, e) {
//        		            var pos = eventInfo.getPos();
//        		            node.pos.setc(pos.x, pos.y);
//        		            this.graph.plot();
//            		    },
            		    //Add also a click handler to nodes
            		    onClick: function(node) {
            		        if(!node) return;
            		        var count = 0;
            		        for (var k in node.adjacencies) {
            		        	count++;
            		        }
            		        if (count == 1) {
            		        	var json = me.getCategory(node.id);
            		        	if (json != null) {
            		        		me.fetchingNodes = true;
            		        		me.graph.canvas.getElement().style.cursor = 'wait';
            		        		// defer so the cursor has a chance to change
	            		        	me.graph.op.sum.defer(250, me.graph.op, [json, {  
	            		        		type: 'fade:seq',
	            		        		duration: 1000,
	            		        		hideLabels: false,
	            		        		transition: $jit.Trans.Quart.easeOut,
	            		        		onComplete: function(){
	            		        			me.graph.canvas.getElement().style.cursor = '';
	            		        			me.fetchingNodes = false;
	            		        		}
	            		        	}]);
            		        	} else {
            		        		/**
            		        		 * @event corpusTypeSelected
            		        		 * @param {Voyeur.Tool.EntitiesBrowser} tool
            		        		 * @param {String} type
            		        		 */
                    		        Voyeur.application.dispatchEvent(
	                		            'corpusTypeSelected',
	                		            me,
	                		            {type : node.name}
                    		        );
            		        	}
            		        } else {
            		        	// node is already expanded
            		        }
            		    }
            		},
            		//Number of iterations for the FD algorithm
            		iterations: iterations,
            		//Edge length
            		levelDistance: 150,
            		// Add text to the labels. This method is only triggered
            		// on label creation and only for DOM labels (not native canvas ones).
            		onCreateLabel: function(domElement, node){
            		    domElement.innerHTML = node.name;
            		    var style = domElement.style;
            		    style.fontSize = "0.8em";
            		    style.color = "#ddd";
            		    //$jit.util.addEvent(domElement, 'click', function () {
	        	        //    me.graph.onClick(node.id);
	        	        //});
            		},
            		// Change node styles when DOM labels are placed
            		// or moved.
            		onPlaceLabel: function(domElement, node){
            		    var style = domElement.style;
            		    var left = parseInt(style.left);
            		    var top = parseInt(style.top);
            		    var w = domElement.offsetWidth;
            		    style.left = (left - w / 2) + 'px';
            		    style.top = (top + 10) + 'px';
            		    style.display = '';
            		}
        		});
        		// load JSON data.
            	var progBar = new Ext.ProgressBar({
            		width: 180,
            		text: this.localize('loading', 'app')
            	});
            	var progressWin = new Ext.Window({
    	            layout: 'fit',
    	            width: 200,
    	            autoHeight: true,
    	            closable: false,
    	            resizable: false,
    	            modal: true,
    	            plain: false,
    	            items: [progBar],
    	            listeners: {
    					beforedestroy: function(win) {
							// IE isn't happy unless the bar is reset
							var bar = win.items.get(0).reset();
    					},
    					show: {
    						fn: function(win) {
	    						this.graph.loadJSON(rootClone);
	    						var prop = animate ? 'end' : ['end', 'start', 'current'];
	    						this.graph.computeIncremental({
	    		            		iter: 10,
	    		            		property: prop,
	    		            		onStep: function(perc){
	    		            			progBar.updateProgress(perc*0.01, perc + '%', true);
	    		            		},
	    		            		onComplete: function(){
	    		            			win.close();
	    		            			if (animate) {
		    		            		    me.graph.animate({
		    		            		        modes: ['linear'],
		    		            		        transition: $jit.Trans.Quart.easeOut,
		    		            		        duration: 2500
		    		            		    });
	    		            			} else {
	    		            				me.graph.plot();
	    		            			}
	    		            		}
	    		        		});
    						},
    						scope: this,
    						delay: 500 // give time for the window to show
    					}
    				}
            	});
    			progressWin.show();
                this.initialized = true;
            } catch(e) {
                setTimeout(initEntitiesApp.createDelegate(this), 500);
            }
        }
        var inserts = '<link href="'+this.getApplication().getBaseUrl()+'resources/tools/EntitiesBrowser/EntitiesBrowser.css" type="text/css" rel="stylesheet" />';
        if (Ext.isIE) {
        	inserts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/excanvas/excanvas.js"></script>'; 
        }
        inserts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/jit/jit_forcedirected.js"></script>'+
        '<div id="'+id+'" style="height: 100%; width: 100%;"></div>';
        this.body.update(inserts, true, initEntitiesApp.createDelegate(this));
    },
    
    initComponent: function() {
        Voyeur.Tool.EntitiesBrowser.superclass.initComponent.call(this);
    },
    

    constructor: function(config) {
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        Voyeur.Tool.EntitiesBrowser.superclass.constructor.apply(this, arguments);
        
        /**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			var params = this.getApiParams();
			// for now only use corpus entities
			if (!params.mode) {
				params.mode = params.docIndex || params.docId ? 'document' : 'corpus';
				this.setApiParams({mode: params.mode})
			}
			this.update({params : params, tool : params.mode=='corpus' ? 'CorpusEntitiesLister' : 'DocumentEntitiesLister'})
        }, this);
        
        /**
         * @event DocumentEntitiesListerResultLoaded
         * @type listener
         */
        this.addListener('DocumentEntitiesListerResultLoaded', function(src, data) {
        	var records = Voyeur.data.DocumentEntities.reader.readRecords(data).records
        	this.initApp(records);
        }, this);
        
        /**
         * @event CorpusEntitiesListerResultLoaded
         * @type listener
         */
        this.addListener('CorpusEntitiesListerResultLoaded', function(src, data) {
        	var records = Voyeur.data.CorpusEntities.reader.readRecords(data).records
        	this.initApp(records);
        }, this);
        
        this.addListener('afterrender', function(src) {
//        	this.initApp();
        }, this);
    },

    api: {
    	/**
    	 * @property mode What mode to operate at, either document or corpus.
    	 * @choices document, corpus
    	 */
    	mode: {
    		'default': null,
    		'choices': ['document', 'corpus']
    	}
	    /**
	     * @property docIndex The document index to restrict results to.
	     * @type Integer
	     * @default null
	     */
		,docIndex: {'default': null}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default null
		 */
		,docId: {'default': null}
    	,listeners: ['CorpusSummaryResultLoaded']
		,dispatchers: ['corpusTypeSelected']
    },
    
    i18n: {
        title : {en: 'Entities Browser'}
        ,type : {en: "Visualization"}
        ,help: {en: 'This tool uses a force directed layout graph to navigate named entities'}
        ,adaptedFrom: {en: 'This tool makes extensive use of the <a href="http://thejit.org/">The JavaScript InfoVis Toolkit<a/>.'}
        ,rawFrequency: {en: 'Frequency'}
        ,relevance: {en: 'Relevance'}
        ,connections: {en: 'Connections'}
    }
});

Ext.reg('voyeurEntitiesBrowser', Voyeur.Tool.EntitiesBrowser);

/**
 * Clone Function
 */
Ext.ux.clone = function(obj)
{
   if(obj == null || typeof(obj) != 'object') 
      return obj;
      
   var cloneArray = function(arr)
   {
      var len = arr.length;
      var out = [];
      if (len > 0)
      {
         for (var i = 0; i < len; ++i)
            out[i] = Ext.ux.clone(arr[i]);
      }
      return out;
      
   };
      
   var c = new obj.constructor();
   for (var prop in obj)
   {
      var p = obj[prop];
      if (Ext.isArray(p))
         c[prop] = cloneArray(p);
      else if (typeof p == 'object')
         c[prop] = Ext.ux.clone(p);
      else
         c[prop] = p;
   }
   return c;
};
/**
 * @class Voyeur.Tool.DocumentTypeFrequenciesGrid A panel for displaying type frequencies for individual documents in a tabular format.
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.DocumentTypeFrequenciesGrid = Ext.extend(Ext.grid.GridPanel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))

		var xtypePrefix = config.xtype+'.';

		var store = new Ext.data.GroupingStore({
			reader: new Ext.data.JsonReader(
				{
					root : 'documentTypes.types'
					,totalProperty : 'documentTypes["@totalTypes"]'
				},
				Ext.data.Record.create(Voyeur.data.DocumentTypes.fields))
			,proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()})
			,listeners : {
				'beforeload' : {
					fn : function(store, options) {
						if (this.hidden || this.getCorpus().isEmpty() || (!this.getApiParamValue('docIdType') && !this.getApiParamValue('docIndex') && !this.getApiParamValue('type') && !this.getApiParamValue('docId'))) {return false;}
						Ext.applyIf(options.params, this.getApiParams());
						var clean = ['docId', 'docIdType','type','query'];
						for (var i=0; i<clean.length;i++) {if (!options.params[clean[i]]) {delete options.params[clean[i]]}}
						Ext.apply(options.params, {tool: 'DocumentTypeFrequencies'});
					},
					scope : this
				},
				load: {
					fn: function(store, records, options) {
						if (options && options.params) {
							if ((options.params.docId && typeof options.params.docId == 'string') || (options.params.docIndex && typeof options.params.docIndex == 'string')) {
								this.getStore().clearGrouping();
							}
						}
							
					}
					,scope : this
				},
				'loadexception' : {
					fn: function(conn, proxy, response, error){
						var alert = Ext.MessageBox.alert("Error", "An error occurred while loading data:<pre style='overflow: auto; clear: both'>\n" + response.responseText+"</pre>");
						alert.setIcon(Ext.MessageBox.ERROR);
					}
					,scope : this
				}
			}
			,remoteSort: true
//			,baseParams : {
//				tool : 'DocumentTypeFrequencies'
//				,bins : 10
//				,extendedSortZscoreMinimum : 1
//			}
			,sortInfo : {field : this.getApiParamValue('sortBy'), direction: this.getApiParamValue('sortDirection')}
			,groupField: this.getApiParamValue('groupBy')
			,groupOnSort : false
		});
		store.paramNames.sort='sortBy';
		store.paramNames.dir='sortDirection'

		this.pagingToolBar = new Ext.PagingToolbar({
			store : store
            ,enableOverflow: true
	        ,pageSize: 30
			,displayInfo: true
			,displayMsg : "{0}-{1} of {2}"
			,listeners : {
				'render' : function(tb) {
					tb.first.hide();
					tb.last.hide();
					tb.refresh.hide();
					var seps = tb.findByType('tbseparator');
					for (var i=0;i<3;i++) {
						seps[i].hide();
					}
				}
			}
			,items: ['-',{
            	xtype: 'button',
            	text: this.localize('reset'),
            	tooltip: this.localize('resetTip'),
            	handler: function() {
            		var docId = this.getApiParamValue('docId') || this.getCorpus().getDocument(0).getId();
            		this.adjustForDocumentsSize(1);
        			this.resetFilteringApiParams();
        			this.setApiParams({docId: docId});
        			this.getStore().load();
            	},
            	scope: this
            },'-',{
                xtype: 'typeSearch',
                width: 100,
                parentTool: this,
                listeners: {
            	    typeSelected: function(combo, type, record) {
            	    	var types = this.getApiParamValue('type') || [];
            	    	if (typeof types == 'string') types = [types];
                		types.push(type);
                		this.setApiParams({type: types});
						this.store.load({params: {type: type}});
            	    },
            	    scope: this
                }
            }]
		});

		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
			forceFit: true,
			emptyText: this.localize('noResults','tool'),
			deferEmptyText: false
			,hideGroupedColumn: true			
		})

		if (!config.plugins) {config.plugins=[]}
		config.plugins.push(new Ext.ux.grid.Favs({
			exporter: function(records) {
				var vals = [];
				for (var i=0;i<records.length;i++) {
					vals.push(records[i].get('docId')+':'+records[i].get('type'))
				}
				return {docIdType: vals}
			}
		}))
		
		var sm = new Ext.grid.CheckboxSelectionModel({
			listeners: {
				selectionchange: {
					fn: function() {this.fireSelectionChange();}
					,scope: this
				}
			}

		})

		var panel = this;
		Ext.applyIf(config, {
			view :  new Ext.grid.GroupingView(config.viewConfig)
			,iconCls : 'table'
//			viewConfig :  config.viewConfig
			,store : store
			,stripeRows : true
			,loadMask : true
			,bbar : this.pagingToolBar
			,autoExpandColumn : this.getId()+'-column-docLabel'
			,sm : sm
			,colModel : new Ext.grid.ColumnModel([sm
				,{header : this.localize('docLabel'), dataIndex : 'docIndex', id : this.getId()+'-column-docLabel', sortable: true, toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val) {return panel.getCorpus().getDocument(val).getShortLabel()}}
  				,{header : this.localize('docTitle'), dataIndex : 'docIndex', id : this.getId()+'-column-docTitle', hidden: true, toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val) {return panel.getCorpus().getDocument(val).get('title')}}
  				,{header : this.localize('docAuthor'), dataIndex : 'docIndex', id : this.getId()+'-column-Author', hidden: true, toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val) {return panel.getCorpus().getDocument(val).get('author')}}
  				,{header : this.localize('docTime'), dataIndex : 'docIndex', id : this.getId()+'-column-Time', hidden: true, toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val) {return panel.getCorpus().getDocument(val).getDate()}}
				,{header : 'Type', dataIndex : 'type', sortable : true, toolTip : Voyeur.localization.get('tool.frequencies_document.term'), renderer : function(val,cell,record) {return "<span class='keyword'>"+val+"</span>"}}
				,{header : Voyeur.localization.get(xtypePrefix+'rawFreq'), dataIndex : 'rawFreq', sortable : true, tooltip : Voyeur.localization.get(xtypePrefix+'rawFreqTip'), renderer : Ext.util.Format.numberRenderer('0,000'), width: 60}
				,{header : this.localize('rawZscore'), dataIndex : 'rawZscore', sortable : true, hidden: true, tooltip : this.localize('rawZscoreTip'), renderer : function(val) {return isNaN(val) ? '–' : "<span class='"+ (val < 0 ? 'negative' : 'positive') + "'>"+ Ext.util.Format.number(val,'0,000.00') + '</span>'}}
				//,{header : this.localize('rawZscoreCorpusDelta'), dataIndex : 'rawZscoreCorpusDelta', sortable : true, hidden: true, tooltip : this.localize('rawZscoreCorpusDeltaTip'), renderer : function(val) {return isNaN(val) ? '–' : "<span class='"+ (val < 0 ? 'negative' : 'positive') + "'>"+ Ext.util.Format.number(val,'0,000.00') + '</span>'}}
				,{header : this.localize('relativeFreqCorpusDelta'), dataIndex : 'relativeFreqCorpusDelta', sortable : true, hidden: true, tooltip : this.localize('relativeFreqCorpusDeltaTip'), renderer : function(val) {return isNaN(val) ? '–' : "<span class='"+ (val < 0 ? 'negative' : 'positive') + "'>"+ Ext.util.Format.number(val,'0,000.0000') + '</span>'}}
				,{header : Voyeur.localization.get(xtypePrefix+'relativeFreq'), dataIndex : 'relativeFreq', sortable: true, tooltip : Voyeur.localization.get(xtypePrefix+'relativeFreqTip'), renderer : function(val) {return Ext.util.Format.number(val * 10000, '0,000.00')}, width: 60}	
				// TODO: fix distribution mean in document frequencies ,{header : Voyeur.localization.get(xtypePrefix+'distributionMean'), dataIndex : 'distributionMean', hidden : true, tooltip : Voyeur.localization.get(xtypePrefix+'distributionMeanTip'), renderer : function(val) {return Ext.util.Format.number(val,'0,000.0')}, width: 60}
				,{header : Voyeur.localization.get(xtypePrefix+'rawDistributionStdDev'), dataIndex : 'rawDistributionStdDev', sortable: true, hidden : true, tooltip : Voyeur.localization.get(xtypePrefix+'rawDistributionStdDevTip'), renderer : function(val) {return isNaN(val) ? '–' : Ext.util.Format.number(val,'0,000.000')}, width: 60}
				,{header : Voyeur.localization.get(xtypePrefix+'rawDistributionKurtosis'), dataIndex : 'rawDistributionKurtosis', sortable: true, hidden : true, tooltip : Voyeur.localization.get(xtypePrefix+'rawDistributionKurtosisTip'), renderer : function(val) {return isNaN(val) ? '–' : "<span class='"+ (val < 0 ? 'negative' : 'positive') + "'>"+ Ext.util.Format.number(val,'0,000.00') + '</span>'}, width: 60}
				,{header : Voyeur.localization.get(xtypePrefix+'rawDistributionSkewness'), dataIndex : 'distributionSkewness', sortable: true, hidden: true, tooltip : Voyeur.localization.get(xtypePrefix+'rawDistributionSkewnessTip'), renderer : function(val) {return isNaN(val) ? '–' : "<span class='"+ (val < 0 ? 'negative' : 'positive') + "'>"+ Ext.util.Format.number(val,'0,000.00') + '</span>'}, width: 60}
				,{header : Voyeur.localization.get(xtypePrefix+'rawFreqs'), dataIndex : 'rawFreqs', tooltip : Voyeur.localization.get(xtypePrefix+'rawFreqsTip'), renderer: function(val,cell,record,rowIndex,colIndex) {
						return panel.getSparkLine(val, panel.getColumnModel().getColumnWidth(colIndex))
					}, width : 100}
			])
		});
		
		Voyeur.Tool.DocumentTypeFrequenciesGrid.superclass.constructor.apply(this, arguments);

		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			this.getStore().load();
		}, this);
		
		/**
		 * @event DocumentTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
			this.getStore().loadData(data);
		}, this);

		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			if (src != this) {
				this.adjustForDocumentsSize(1);
				this.resetFilteringApiParams();
				var docId = data.docIdType.split(':')[0];
				this.setApiParams({docId: docId, docIdType: data.docIdType});
				this.getStore().load();
			}
			else {return false;} // make sure we didn't respond
		}, this);

		/**
		 * @event documentTypesSelected
		 * @type listener
		 */
		this.addListener('documentTypesSelected', function(src, data) {
			if (src != this) {
				var docIdTypes = data.docIdType;
				var docs = {};
				var counter = 0;
				for (var i=0;i<docIdTypes.length;i++) {
					var parts = docIdTypes[i].split(":");
					if (!docs[parts[0]]) {
						counter++;
						docs[parts[0]] = true;
					}
				}
				this.adjustForDocumentsSize(counter);
				this.resetFilteringApiParams();
				this.setApiParams({docIdType: data.docIdType});
				this.getStore().load();
			}
		}, this);

		this.addListener('rowclick', function(src, grid, rowIndex, e) {
			this.fireSelectionChange();
		}, this);
		
		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data, record) {
			this.adjustForDocumentsSize(this.getCorpus().getSize());
			this.resetFilteringApiParams();
			this.setApiParams({type: data.type});
			this.getStore().load();
		}, this);
		
		/**
		 * @event corpusTypesSelected
		 * @type listener
		 */
		this.addListener('corpusTypesSelected', function(src, data, records) {
			this.adjustForDocumentsSize(this.getCorpus().getSize());
			this.resetFilteringApiParams();
			this.setApiParams({type: data.type});
			this.getStore().load();
		}, this);
		
		/**
		 * @event corpusDocumentSelected
		 * @type listener
		 */
		this.addListener('corpusDocumentSelected', function(src, params) {
			this.adjustForDocumentsSize(1);
			this.resetFilteringApiParams();
			this.setApiParams({docId: params.docId});
			this.getStore().load();
		}, this);
		
		/**
		 * @event corpusDocumentsSelected
		 * @type listener
		 */
		this.addListener('corpusDocumentsSelected', function(src, params) {
			var ids = params.docId;
			this.adjustForDocumentsSize(ids.length);
			this.resetFilteringApiParams();
			this.setApiParams({docId: ids});
			this.getStore().load();
		}, this);
	}

	,resetFilteringApiParams : function(query) {
		this.setApiParams({type: undefined, docId: undefined, docIdType: undefined, query: undefined});
	}
	,adjustForDocumentsSize : function(size) {
		var store = this.getStore();
		var cm = this.getColumnModel();
		var isMultiple = size>1;
		cm.setHidden(cm.findColumnIndex('docIndex'), !isMultiple);
		if (isMultiple) {store.groupBy('type');}
		else {store.clearGrouping();}		
	}
	,fireSelectionChange : function() {
		var time = new Date().getMilliseconds();
		this.lastSelectionTime=time;
		var me = this;
		setTimeout(function() {if (me.lastSelectionTime == time) {
			records = me.getSelectionModel().getSelections();
			docIdType = []
			for (var i=0;i<records.length;i++) {
				docIdType.push(records[i].get('docId')+':'+records[i].get('type'));
			}
			if (docIdType.length==1) {
				/**
				 * @event documentTypeSelected
				 * @param {Voyeur.Tool.DocumentTypeFrequenciesGrid} tool
				 * @param {Object} params <ul>
				 * <li><b>docIdType</b> : String</li>
				 * <li><b>type</b> : String</li>
				 * <li><b>record</b> : Ext.data.Record</li>
				 * </ul>
				 * @type dispatcher
				 */
				Voyeur.application.dispatchEvent('documentTypeSelected', me, {docIdType : docIdType[0], type: records[0].get('type'), record: records[0]});
			}
			else if (docIdType.length>1) {
				/**
				 * @event documentTypesSelected
				 * @param {Voyeur.Tool.DocumentTypeFrequenciesGrid} tool
				 * @param {Object} params <ul>
				 * <li><b>docIdType</b> : Array</li>
				 * <li><b>record</b> : Array</li>
				 * </ul>
				 * @type dispatcher
				 */
				Voyeur.application.dispatchEvent('documentTypesSelected', me, {docIdType : docIdType, record: records});
			}
		}}, 1000);
	}
	,lastSelectionTime : 0
	,handleCorpusTermRowClick : function(sm) {
	}

	,showOptions : function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
					xtype : 'numberfield',
					id : 'extendedSortZscoreMinimum',
					value : this.getStore().baseParams.extendedSortZscoreMinimum,
					fieldLabel : '<span ext:qtip="'
							+ this.localize('extendedSortZscoreMinimumTip','tool')
							+ '">'
							+ this.localize('extendedSortZscoreMinimum','tool')
							+ '</span>'
				}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();

								if (stopList.getValue() && !stopList.getRawValue()) stopList.setValue('');
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery)
								}
									
								if (form.isDirty()) {
									this.setApiParams({
										extendedSortZscoreMinimum: form.findField('extendedSortZscoreMinimum').getValue()
										,stopList: stopList.getValue()
									});
									if (global) {
										this.getApplication().applyParamsGlobally({
											stopList: this.getApiParamValue('stopList')
										}, true);
									}
									else {
										this.getStore().load();
									}
								}
								formPanel.findParentByType('window').destroy();
								
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								var form = btn.findParentByType('form').getForm();
								form.findField('stopList').setValue(this.getApiParamDefaultValue('stopList'));
								form.findField('extendedSortZscoreMinimum').setValue(this.getApiParamDefaultValue('extendedSortZscoreMinimum'));
							},
							scope : this
						}
					}
	
				}]
			}]
		}, true);

	}

	,api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		,start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property sortBy The property to sort results by.
		 * @type String
		 * @default rawFreq
		 * @choices docIndex, type, rawZscore, rawZscoreCorpusDelta, relativeFreq, rawDistributionStdDev, rawDistributionKurtosis, distributionSkewness, rawFreqs
		 */
		,sortBy: {'default': 'rawFreq'}
		/**
		 * @property sortDirection The direction to sort results in.
		 * @type String
		 * @default ASC
		 * @choices ASC, DESC
		 */
		,sortDirection: {'default': 'DESC'}
		/**
		 * @property groupBy The property to group results by.
		 * @type String
		 * @default type
		 * @choices docIndex, type, rawZscore, rawZscoreCorpusDelta, relativeFreq, rawDistributionStdDev, rawDistributionKurtosis, distributionSkewness, rawFreqs
		 */
		,groupBy: {'default': 'type'}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default undefined
		 */
		,docIdType: {'default': undefined}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default undefined
		 */
		,type: {'default': undefined}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default undefined
		 */
		,docId: {'default': undefined}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default undefined
		 */
		,docIndex: {'default': undefined}
		/**
		 * @property extendedSortZscoreMinimum The minimum extended sort Z score that each result should have.
		 * @type Number
		 * @default 1
		 */
		,extendedSortZscoreMinimum: {'default': null}
		,toolType: ['Table', 'Document']
//		,visibleColumn: {'default': ['type', 'rawCollocateFreq', 'relativelCollocateFreq', 'relativeRatio']}
		,listeners: ['CorpusSummaryResultLoaded', 'DocumentTypeFrequenciesResultLoaded', 'corpusDocumentSelected', 'corpusDocumentsSelected', 'corpusTypeSelected', 'corpusTypesSelected', 'documentTypeSelected', 'documentTypesSelected']
		,dispatchers: ['documentTypeSelected', 'documentTypesSelected']
	}
	
	,thumb: {
		large: 'DocumentTypeFrequenciesGrid.png'
	}
	
	// private localization variables
	,i18n : {
		title : {en: "Words in Documents"}
		,help : {en: "This table shows word frequencies for each document in the corpus. Hover over the column headers or toolbar buttons for more information."}
		,docLabel : {en : 'Document'}
		,docTitle : {en : 'Title'}
		,docAuthor : {en : 'Author'}
		,docTime : {en : 'Time'}
		,type : {en : "Frequencies"}
		,typeTip : {en: "The type from the document for which data is being provided."}
		,rawFreq : {en: "Count"}
		,rawFreqTip : {en: "The raw count of this term in the document."}
		,rawZscore : {en: "Z-Score"}
		,rawZscoreTip : {en: "This is the z-score – or <a href='http://en.wikipedia.org/wiki/Standard_score' target='_blank'>standard score</a> – of the total raw frequencies for the type in the document, compared to other types; it is a normalized version of the raw frequency, showing the number of standard deviations a value is above or below the mean of type frequencies."}
		,rawZscoreCorpusDelta : {en: "Z-Score Difference"}
		,relativeFreqCorpusDeltaTip : {en: "This is the difference between the z-score of the word in the document and the z-score of the word in the corpus (positive values mean the word is more frequent in the document than in the corpus as a whole). The z-score – <a href='http://en.wikipedia.org/wiki/Standard_score' target='_blank'>standard score</a> is a normalized version of the raw frequency, showing the number of standard deviations a value is above or below the mean of type frequencies."}
		,relativeFreqCorpusDelta : {en: "Relative Difference"}
		,rawZscoreCorpusDeltaTip : {en: "This is the difference between the relative frequency of the term in the document and its relative frequency in the corpus."}
		,relativeFreq : {en: "Relative"}
		,relativeFreqTip : {en: "The relative count of this type in the document (per 10,000 words)."}
		,rawDistributionMean : {en: "Mean"}
		,rawDistributionMeanTip : {en: "This is the mean of distribution values for the document."}
		,rawDistributionStdDev : {en: "Std. Dev."}
		,rawDistributionKurtosis : {en: "Peakedness"}
		,rawDistributionSkewness : {en: "Skew"}
		,rawFreqs : {en: "Trend"}
		,rawFreqsTip : {en: "This graph represents the values of the mean relative counts across the corpus (every document has a represented value)."}
		,bins : {en: "Distribution"}
		,binsTip : {en: "This graph represents the distribution of the term across the document."}
		,add_terms : {en: "Add terms"}
		,add_termsMsg : {en: "You can add new search terms below, one per line."}
		,selectRowsForResults : {en: "Generate results by selecting rows from the <i>Document Types</i> tool"}
		,reset : {en: 'Reset'}
		,resetTip : {en: 'Resets the grid, fetching the top frequency words for the current document.'}
	}
});

Ext.reg('voyeurDocumentTypeFrequenciesGrid', Voyeur.Tool.DocumentTypeFrequenciesGrid);

/**
 * @class Voyeur.Tool.DocumentTypeCollocateFrequenciesGrid A panel that provides per-document collocate data in a tabular format.
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.DocumentTypeCollocateFrequenciesGrid = Ext.extend(Ext.grid.GridPanel, {
	constructor: function(config){
		Ext.apply(this, new Voyeur.Tool(config, this))
		var reader = new Ext.data.JsonReader({
			root: 'documentTypeCollocateFrequencies.types',
			totalProperty: 'documentTypeCollocateFrequencies["@totalTypes"]'
		}, Voyeur.data.DocumentTypeCollocates.fields);
		var store = new Ext.data.GroupingStore({
			reader: reader
			,fields: Voyeur.data.DocumentTypeCollocates.fields
			,groupField: this.getApiParamValue('groupBy')
			,proxy: new Ext.data.HttpProxy({url: this.getTromboneUrl()})
			,listeners : {
				'beforeload' : {
					fn : function(store, options) {
						if (this.hidden || this.getCorpus().isEmpty() || (!this.getApiParamValue('docIdType') && !this.getApiParamValue('type'))) {return false;}
						Ext.applyIf(options.params, this.getApiParams());
						Ext.apply(options.params, {tool: 'DocumentTypeCollocateFrequencies'});
					},
					scope : this
				},
				'loadexception' : {
					fn: function(conn, proxy, response, error){
						var alert = Ext.MessageBox.alert("Error", "An error occurred while loading data:<pre style='overflow: auto; clear: both'>\n" + response.responseText+"</pre>");
						alert.setIcon(Ext.MessageBox.ERROR);
					}
					,scope : this
				}
			}
			,remoteSort: true
			,sortInfo : {field : this.getApiParamValue('sortBy'), direction : this.getApiParamValue('sortDirection')}
		});
		store.paramNames.sort='sortBy';
		store.paramNames.dir='sortDirection'
		
		var xtypePrefix = this.getXType() + '.';
		
		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
			forceFit: true,
			emptyText : this.localize('noResults','tool'),
			deferEmptyText: false
			,hideGroupedColumn: true
		})
		
		if (!config.plugins) {config.plugins=[]}
		config.plugins.push(new Ext.ux.grid.Favs());
		
		var context = this.getApiParamValue('context');
		var visibleColumn = this.getApiParamValue('visibleColumn');
		
		var panel = this;
		
		var pagingToolbar = new Ext.PagingToolbar({
			store: store,
            enableOverflow: true,
			pageSize: this.getApiParamValue('limit'),
			displayInfo: true
			,items : [
				'-',
				{
					text : this.localize('context')
					,tooltip : this.localize('contextTip')
					,menu : new Ext.menu.Menu({
						items : [
							new Ext.menu.CheckItem({text : '1',checked:context==1,group:'context'})
							,new Ext.menu.CheckItem({text : '2',checked:context==2,group:'context'})
							,new Ext.menu.CheckItem({text : '3',checked:context==3,group:'context'})
							,new Ext.menu.CheckItem({text : '4',checked:context==4,group:'context'})
							,new Ext.menu.CheckItem({text : '5',checked:context==5,group:'context'})
							,new Ext.menu.CheckItem({text : '7',checked:context==7,group:'context'})
							,new Ext.menu.CheckItem({text : '10',checked:context==10,group:'context'})
							,new Ext.menu.CheckItem({text : '15',checked:context==15,group:'context'})
							,new Ext.menu.CheckItem({text : '20',checked:context==20,group:'context'})
							,new Ext.menu.CheckItem({text : '25',checked:context==25,group:'context'})
						]
						,listeners : {
							'itemclick' : {
								fn : function(item) {
									this.setApiParams({context: item.text})
									this.getStore().load();
								}
								,scope : this
							}
						}
					})
				}, '-',
				{
	                xtype: 'typeSearch',
	                width: 100,
	                parentTool: this,
	                listeners: {
	            	    typeSelected: function(combo, type, record) {
	                		this.setApiParams({type: type});
	                		var docIdTypes = [];
	                		this.getCorpus().getDocuments().eachKey(function(key, doc) {
	                			var docIdType = key+':'+type;
	                			docIdTypes.push(docIdType);
	                		}, this);
	                		this.setApiParams({docIdType: docIdTypes});
							this.getStore().load();
	            	    },
	            	    scope: this
	                }
	            }
			]
		});
		
		Ext.applyIf(config, {
			view: new Ext.grid.GroupingView(config.viewConfig)
			,iconCls : 'table'
			,loadMask: true,
			sm: new Ext.grid.RowSelectionModel(),
			stripeRows: true,
			colModel: new Ext.grid.ColumnModel({
				columns: [
					{
						header: this.localize('docLabel'), tooltip : this.localize('docLabelTip'), dataIndex: 'docIndex', hidden: visibleColumn.indexOf('docIndex')==-1,
						renderer : function(val) {return panel.getCorpus().getDocument(val).getShortLabel()}
					}
					,{
						header: this.localize('keywordDocument'), tooltip : this.localize('keywordDocumentTip'), dataIndex: 'keyword_docId', sortable: false, hidden: visibleColumn.indexOf('keyword_docId')==-1,
						renderer : function(val, meta, record) {return '<span class="keyword">'+record.get('keyword')+'</span> in '+panel.getCorpus().getDocument(record.get('docIndex')).getShortTitle()}
					},{
						header: this.localize('keyword'), tooltip : this.localize('keywordTip'), dataIndex: 'keyword', sortable: true, hidden: visibleColumn.indexOf('keyword')==-1,
						renderer : function(val) {return '<span class="keyword">'+val+'</span>'}
					},{
						header: this.localize('type'), tooltip : this.localize('typeTip'), dataIndex: 'type', sortable: true, hidden: visibleColumn.indexOf('type')==-1,
						renderer : function(val) {return '<span class="keyword">'+val+'</span>'}
					}
					,{
						header: this.localize('rawCollocateFreq'), tooltip : this.localize('rawCollocateFreqTip'), dataIndex: 'rawCollocateFreq', sortable: true, hidden: visibleColumn.indexOf('rawCollocateFreq')==-1,
						renderer : function (val, meta, record) {return val + ' ('+ record.get('rawFreq') + ')'}
					}
					,{
						header: this.localize('relativeCollocateFreq'), tooltip : this.localize('relativeCollocateFreqTip'), sortable: true, dataIndex: 'relativeCollocateFreq', hidden: visibleColumn.indexOf('relativeCollocateFreq')==-1,
						renderer : function (val, meta, record) {return Ext.util.Format.number(val*10000,'0,000.0') + ' ('+Ext.util.Format.number(record.get('relativeFreq')*10000,'0,000.0')+')'}
					}
					,{
						header: this.localize('rawCollocateFreqRatio'), tooltip : this.localize('rawCollocateFreqRatioTip'), sortable: true,  dataIndex: 'rawCollocateFreqRatio', hidden: visibleColumn.indexOf('rawCollocateFreqRatio')==-1,
						renderer : function(val) {return "<span class='" + (val < 0 ? 'negative' : 'positive') + "'>" + Ext.util.Format.number(val*100,'0,000.0') + '</span>'}
					}
				],
				listeners: {
					hiddenchange: {
						// FIXME this code moved from Voyeur.Tool as being too specific
						// also in CorpusTypeFrequenciesGrid
						fn: function(cm) {
							var columns = [];
							var size = cm.getColumnCount();
							var dataIndex;
							for (var i=0;i<size;i++) {
								if (!cm.isHidden(i)) {
									dataIndex = cm.getDataIndex(i);
									if (dataIndex) {columns.push(cm.getDataIndex(i));}
								}
							}
							this.setApiParams({visibleColumn: columns});
							if (this.store && this.store.groupField) {this.setApiParams({groupBy: this.store.groupField})}
						}
						,scope: this
					}
				}
			}),
			store: store,
			bbar: pagingToolbar
		});
		Voyeur.Tool.DocumentTypeCollocateFrequenciesGrid.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			this.getStore().load();
		}, this);

		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType});
			this.getStore().load();
		}, this);

		/**
		 * @event documentTypesSelected
		 * @type listener
		 */
		this.addListener('documentTypesSelected', function(src, data) {
			this.setApiParams({docIdType: data.docIdType});
			this.getStore().load();
		}, this);
	}
	
	,api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		,start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property context The number of surrounding words to examine for collocates.
		 * @type Integer
		 * @default 3
		 */
		,context: {'default': 3}
		/**
		 * @property sortBy The property to sort results by.
		 * @type String
		 * @default rawCollocateFreqRatio
		 * @choices docIndex, keyword, type, rawCollocateFreq, relativeCollocateFreq, rawCollocateFreqRatio
		 */
		,sortBy: {
			'default': 'rawCollocateFreqRatio',
			'choices': ['docIndex', 'keyword', 'type', 'rawCollocateFreq', 'relativeCollocateFreq', 'rawCollocateFreqRatio']
		}
		/**
		 * @property sortDirection The direction to sort results in.
		 * @type String
		 * @default DESC
		 * @choices ASC, DESC
		 */
		,sortDirection: {'default': 'DESC'}
		/**
		 * @property groupBy The property to group results by.
		 * @type String
		 * @default keyword_docId
		 * @choices docIndex, keyword_docId, keyword, type, rawCollocateFreq, relativeCollocateFreq, rawCollocateFreqRatio
		 */
		,groupBy: {
			'default': 'keyword_docId',
			'choices': ['docIndex', 'keyword_docId', 'keyword', 'type', 'rawCollocateFreq', 'relativeCollocateFreq', 'rawCollocateFreqRatio']
		}
		/**
		 * @property query A string to search for in a document.
		 * @type String
		 * @default null
		 */
		,query: {'default': null}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property visibleColumn The columns to display.
		 * @type Array
		 * @default ['type', 'rawCollocateFreq', 'relativelCollocateFreq', 'rawCollocateFreqRatio']
		 * @choices docIndex, keyword_docId, keyword, type, rawCollocateFreq, relativeCollocateFreq, rawCollocateFreqRatio
		 */
		,visibleColumn: {'default': ['type', 'rawCollocateFreq', 'relativelCollocateFreq', 'rawCollocateFreqRatio']}
		,toolType: ['Table', 'Document']
		,listeners: ['CorpusSummaryResultLoaded', 'documentTypeSelected', 'documentTypesSelected']
	}
	
	,thumb: {
		large: 'DocumentTypeCollocateFrequenciesGrid.png'
	}

	// private localization variables
	,i18n : {
		title : {en: "Collocates"}
		,help : {en: "This table shows the frequencies of words that appear near a specified term in a document."}
		,context : {en: "Context"}
		,contextTip : {en: "This value determines how many words to count on the left and the right of each keyword."}
		,term : {en: "Term"}
		,termTip : {en: "The term from the corpus for which data is being provided."}
		,docLabel: {en: 'Document'}
		,docLabelTip: {en: "The document in which the collocate is found."}
		,keywordDocument: {en: 'Keyword/Document'}
		,keywordDocumentTip: {en: "Indicates the keyword and document for the collocate."}
		,keyword: {en: 'Keyword'}
		,keywordTip: {en: 'This is the keyword for which collocates are shown.'}
		,type : {en : "Frequencies"}
		,typeTip: {en: 'This is the collocate that occurs near the keyword.'}
		,rawCollocateFreq: {en: 'Raw Frequency'}
		,rawCollocateFreqTip: {en: 'This shows the frequency of the collocate near the keyword, and then in parentheses the frequency of the collocate in the entire document.'}
		,relativeCollocateFreq: {en: 'Relative Frequency'}
		,relativeCollocateFreqTip: {en: 'This shows the relative frequency per 10,000 words of the collocate near the keyword, and then in parentheses the relative frequency of the collocate in the entire document.'}
		,rawCollocateFreqRatio: {en: 'Ratio'}
		,rawCollocateFreqRatioTip: {en: "This indicates the difference between the relative frequency of the collocates near the keywords and the relative frequency of the collocates in the entire document. Higher positive values mean that the collocate is more closely associated with the keyword than in the rest of the document."}
	}
});

Ext.reg('voyeurDocumentTypeCollocateFrequenciesGrid', Voyeur.Tool.DocumentTypeCollocateFrequenciesGrid);

/**
 * @class Voyeur.Tool.Mandala This panel holds the Mandala Java Applet for use in Voyeur for the Weme project.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Matthew Gooding
 */
Voyeur.Tool.Mandala = Ext.extend(Ext.Panel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))

		Voyeur.Tool.Mandala.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			var content = '<div align="center"><applet code="org.humviz.mandala.core.AppletWrapper" archive="'+this.getToolDirectoryUrl()+'Mandala.jar" width="1300" height="700">'+
 						'<param name="input" value="'+this.getTromboneUrl()+'?tool=DocumentExporter&outputFormat=xml&template=docExport2xml&corpus=' + this.getCorpus().getId() + '" />'+
 						'<param name="title" value="'+this.localize('title')+' in '+this.localize('title','app')+'" />'+
						'<param name="JitrURI" value="'+this.getApiParamValue('input')+'"/>'+
 						'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet><div>'+this.localize('adaptedFrom')+'</div></div>'
				this.body.update(content);
		}, this)


		/**
		 * This method listens for a Voyeur event containing the text of any
		 * selected dots in Mandala. For now the method only displays the text
		 * in an alert box.
		 */
		this.addListener('mandalaTextSelected', function(src, params) {
			alert(arguments[1]);
		}, this)

		
		/**
		 * This method listens for a Voyeur event containing information about
		 * the most recently executed search. For now that information is simply
		 * displayed in an alert box. 
		 */
		this.addListener('mandalaSearchActivated', function(src, params) {
			//Argument 1 = Search Terms
			//Argument 2 = Search Field
			//Argument 3 = Positive/negative search
			search = arguments[1] + arguments[2] + arguments[3];
			alert(search);
		}, this)

		
		/**
		 * This method listens for a Voyeur event that contains information 
		 * about the dot that is currently being dragged. For now this 
		 * information is displayed in an alert box. 
		 */
		this.addListener('mandalaDotDragged', function(src, params) {
//			search = arguments[1] + arguments[2] + arguments[3] + arguments[4] + arguments[5] + arguments[6] + arguments[7];
//				alert(search);
		}, this)
		
	}

	,api: {
		/**
		 * @property input A URI for the content to load.
		 * @type String
		 * @default null
		 */
		input: {
			'default': null
		}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'mandalaTextSelected', 'mandalaSearchActivated', 'mandalaDotDragged']
	}
	
	,thumb: {
		large: 'Mandala.png'
	}

	,i18n : {
		title : {en: "Mandala"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool can be used to performed advanced searches in xml files."}
		,adaptedFrom: {en: "Mandala is adapted from I don't know what to put here."}
	}
});

Ext.reg('voyeurMandala', Voyeur.Tool.Mandala);

/**
 * @class Voyeur.Tool.Centroid This panel holds the Centroid Java Applet for use in Voyeur for the Weme project.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Matthew Gooding
 */
Voyeur.Tool.Centroid = Ext.extend(Ext.Panel, {
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))

		Voyeur.Tool.Centroid.superclass.constructor.apply(this, arguments);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			if (this.rendered) {this.fireEvent('afterrender', this);}
		}, this)
		
		/**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
			var corpusTypeReader = new Ext.data.JsonReader({
		        root : 'corpusTypes.types'
		        ,totalProperty : 'corpusTypes["@totalTypes"]'
		    }, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields));
			var records = corpusTypeReader.readRecords(data).records;
			var types = [];
			for (var i=0, len=records.length;i<len;i++) {
				types.push(records[i].get('type'));
			}
			var params = this.getApiParams();			
			params.type = types;
			this.createCentroid(params);
		}, this);

		this.addListener('afterrender', function(src, params) {
			if (this.getCorpus().getSize()>0) {
				var params = this.getApiParams();
				if (params.type) {
					this.createCentroid(params);
				}
				else {
					this.update({params: params, tool: 'CorpusTypeFrequencies'})
				}
			}
		}, this)

	}

	,createCentroid : function(params) {
		var dir = this.getToolDirectoryUrl();
		Ext.apply(params, {
			tool: ['CorpusSummary','DocumentTypeFrequencies']
			,includeOffsets: 1
			,template: 'docFreqs2weightedCentroid.xsl'
			,outputFormat: 'xml'
		});
		var url = this.getTromboneUrl()+'?' + Ext.urlEncode(params);
		var content = '<div align="center"><applet code="org.humviz.centroid.Centroid" archive="'+dir+'centroid.jar,'+dir+'core.jar,'+dir+'proxml_utf8.jar" width="800" height="600">'+
			'<param name="uri" value="'+url +'" />'+
			'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet></div>'
		this.body.update(content);
	}
		
	,api: {
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 25
		 */
		limit: {
			'default': 25
		}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {
			'default': null
		}
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		,stopList: {
			'default': null
		}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
	}

	,i18n : {
		title : {en: "Centroid"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool can be used to perform advanced searches in XML files."}
	}
});

Ext.reg('voyeurCentroid', Voyeur.Tool.Centroid);

/**
 * @class Voyeur.Tool.Bubbles This tool displays the Bubbles applet.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.Bubbles = Ext.extend(Ext.Panel, {
	constructor : function(config) {
	
		// inherit Voyeur Tool
		Ext.apply(this, new Voyeur.Tool(config, this))

		// call superclass
		Voyeur.Tool.Bubbles.superclass.constructor.apply(this, arguments);
		
		// create applet when the corpus is loaded
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function() {
			if (this.rendered) {this.fireEvent('afterrender', this);}
		}, this);
		
		// create applet when the corpus is loaded
		this.addListener('afterrender', function() {
			
			var content = '';
			var corpus = this.getCorpus();
			var size = corpus.getSize();
			if (size>0) {
				
				
				// set core parameters
				var params = this.getApiParams();
				
				// check to make sure we don't have too many words
				if (size>1 && !params.docIndex && !params.docId) {
					var docIndex = [];
					var words = 0;
					for (var i=0;i<size;i++) {
						var doc = corpus.getDocument(i);
						docIndex.push(doc.getIndex())
						words += doc.get('totalWordTokens');
						if (words>100000) {
							params.docIndex = docIndex;
							break;
						}
					}
				}
				
				// create applet
				content = '<div align="center"><applet code="ca.hermeneuti.processing.ballsreader.BallsReader" archive="'+this.getApplication().getBaseUrl()+'resources/lib/balls-reader/ballsreader-0.0.1-SNAPSHOT-jar-with-dependencies.jar" width="800" height="600">'+
					'<param name="source" value="'+this.getTromboneUrl()+'?tool=DocumentExporter&outputFormat=text&' + Ext.urlEncode(params)+ '" />'+
					'<param name="title" value="'+this.localize('title')+' in '+this.localize('title','app')+'" />';
				if (params.stopList) {
					content += '<param name="stopList" value="'+params.stopList+'" />';
				}
				content +=	'To view this content, you need to install Java from <A HREF="http://java.com">java.com</A></applet><div>'+this.localize('adaptedFrom')+'</div></div>'
			}
			
			// update body contents	with applet
			this.body.update(content);
			
		}, this)
	}

	,api: {
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default null
		 */
		'docIndex': {
			'default': null
			,'type': Number
			,'required': false
			,'value': null
			,'multiple': true
			,'example': 0
		}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default null
		 */
		,'docId': {
			'default': null
			,'type': String
			,'required': false
			,'value': null
			,'multiple': true
			,'example': 'a_valid_document_id'
		}
		/**
		 * @property template The template to use with DocumentExporter.
		 * @type String
		 * @default docExport2plainText
		 */
		,'template': {
			'default': 'docExport2plainText'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
		}
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		,'stopList': {
			'default': null
		}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded']
	}
	
	,thumb: {
		large: 'Bubbles.png'
	}

	// localization variables
	,i18n : {
		title : {en: "Bubbles"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool reads the words in a document and displays the highest frequency words proportionately large bubbles."}
		,adaptedFrom: {en: "Bubbles is adapted from Martin Ignacio Bereciartua's <a href=\"http://www.m-i-b.com.ar/mib/letter_pairs/eng/\" target=\"_blank\">Letter Pairs</a>."}
	}
});

Ext.reg('voyeurBubbles', Voyeur.Tool.Bubbles);

/**
 * @class Voyeur.Tool.ScatterPlot A scatter plot visualization.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.ScatterPlot = Ext.extend(Ext.Panel, {
	
    chart: null,
    caStore: null,
    pcaStore: null,
    typeStore: null,
    newType: null,
    
    lastHoveredWord: null, // last point object that was hovered over
    pointHighlights: {}, // object containing svg elements for highlighting selections
    
    labelsMode: 0, // 0 all labels, 1 word labels, 2 no labels
    
    useFreqForSize: true,
    sizeLimit: 10,
    
    WORD_COLOUR: '69, 114, 167',
    PART_COLOUR: '205, 117, 22',
    colors: ['116,116,181', '139,163,83', '189,157,60', '171,75,75', '174,61,155'],
    
    constructor : function(config) {
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        var analysis = this.getApiParamValue('analysis');
        this.analysisButton = new Ext.Button({
			text: this.localize('analysis'),
			iconCls: 'icon-calculator',
			menu: new Ext.menu.Menu({
				items: [
				         new Ext.menu.CheckItem({text: this.localize('pca'), checked:analysis=='pca', group:'analysis'}),
				         new Ext.menu.CheckItem({text: this.localize('ca'), checked:analysis=='ca', group:'analysis'})
					],
					listeners: {
						itemclick: function(item) {
							if (item.text == this.localize('pca')) {
								this.setApiParams({analysis: 'pca'});
							} else {
								this.setApiParams({analysis: 'ca'});
								if (this.getCorpus().getSize() == 3) {
									this.setApiParams({dimensions: 2});
					            	this.dimsButton.menu.items.get(0).setChecked(true); // need 1-2 docs or 4+ docs for 3 dimensions
								}
							}
							this.loadStore();
						},
						scope: this
					}
			})
		});
        
        var freqType = this.getApiParamValue('freqType');
        this.freqButton = new Ext.Button({
        	text: this.localize('freqType'),
			iconCls: 'table',
			menu: new Ext.menu.Menu({
				items: [
				         new Ext.menu.CheckItem({text: this.localize('relFreq'), checked:freqType=='relative', group:'freqType'}),
				         new Ext.menu.CheckItem({text: this.localize('rawFreq'), checked:freqType=='raw', group:'freqType'})
					],
					listeners: {
						itemclick: function(item) {
							if (item.text == this.localize('relFreq')) {
								this.setApiParams({freqType: 'relative'});
							} else {
								this.setApiParams({freqType: 'raw'});
							}
							this.loadStore();
						},
						scope: this
					}
			})
        });
        
        var terms = parseInt(this.getApiParamValue('limit'));
        this.termsButton = new Ext.Button({
			text: this.localize('terms'),
			iconCls: 'icon-page-white-text',
			menu: new Ext.menu.Menu({
				items: [
				         new Ext.menu.CheckItem({text: '10', checked:terms==10, group:'terms'}),
				         new Ext.menu.CheckItem({text: '20', checked:terms==20, group:'terms'}),
				         new Ext.menu.CheckItem({text: '30', checked:terms==30, group:'terms'}),
				         new Ext.menu.CheckItem({text: '40', checked:terms==40, group:'terms'}),
				         new Ext.menu.CheckItem({text: '50', checked:terms==50, group:'terms'}),
				         new Ext.menu.CheckItem({text: '60', checked:terms==60, group:'terms'}),
				         new Ext.menu.CheckItem({text: '70', checked:terms==70, group:'terms'}),
				         new Ext.menu.CheckItem({text: '80', checked:terms==80, group:'terms'}),
				         new Ext.menu.CheckItem({text: '90', checked:terms==90, group:'terms'}),
				         new Ext.menu.CheckItem({text: '100', checked:terms==100, group:'terms'})
					],
					listeners: {
						itemclick: function(item) {
							this.typeStore.removeAll();
							this.setApiParams({limit: item.text});
							this.loadStore();
						},
						scope: this
					}
			})
		});
        
        var clusters = parseInt(this.getApiParamValue('clusters'));
        this.clustersButton = new Ext.Button({
			text: this.localize('clusters'),
			iconCls: 'icon-clusters',
			menu: new Ext.menu.Menu({
				items: [
				         new Ext.menu.CheckItem({text: '1', checked:clusters==1, group:'clusters'}),
				         new Ext.menu.CheckItem({text: '2', checked:clusters==2, group:'clusters'}),
				         new Ext.menu.CheckItem({text: '3', checked:clusters==3, group:'clusters'}),
				         new Ext.menu.CheckItem({text: '4', checked:clusters==4, group:'clusters'}),
				         new Ext.menu.CheckItem({text: '5', checked:clusters==5, group:'clusters'})
					],
					listeners: {
						itemclick: function(item) {
							var clusters = item.text;
							if (clusters == 1) clusters = null;
							this.setApiParams({clusters: clusters});
							this.loadStore();
						},
						scope: this
					}
			})
		});
        
        var dims = parseInt(this.getApiParamValue('dimensions'));
        this.dimsButton = new Ext.Button({
			text: this.localize('dimensions'),
			iconCls: 'icon-layers',
			menu: new Ext.menu.Menu({
				items: [
				         new Ext.menu.CheckItem({text: '2', checked:dims==2, group:'dims'}),
				         new Ext.menu.CheckItem({text: '3', checked:dims==3, group:'dims'})
					],
					listeners: {
						itemclick: function(item) {
							var dims = item.text;
							
							var numDocs = this.getCorpus().getSize();
							var analysis = this.getApiParamValue('analysis');
							if (dims == 3 && analysis == 'ca' && numDocs == 3) {
								dims = 2;
								this.alertInfo(this.localize('minDocsForDims'));
								return false;
							}
							
							this.setApiParams({dimensions: dims});
							this.loadStore();
						},
						scope: this
					}
			})
		});
        
        this.caStore = new Ext.data.JsonStore({
    		root : 'ca.types',
    		totalProperty : 'ca["@totalTypes"]',
    		fields: [
    		    {name: 'type', mapping: '@type'},
    		    {name: 'rawFreq', mapping: '@rawFreq', type: 'int'},
    		    {name: 'relativeFreq', mapping: '@relativeFreq', type: 'float'},
    		    {name: 'category', mapping: '@category'},
    		    {name: 'cluster', mapping: '@cluster', type: 'int'},
    		    {name: 'clusterCenter', mapping: '@clusterCenter', type: 'boolean'},
    		    {name: 'coordinates', mapping : 'vector'}
    		],
    		proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()}),
    		listeners: {
    			beforeload: function(store, options) {
					if (this.hidden || this.getCorpus().isEmpty()) {
						return false;
					}
					Ext.applyIf(options.params, this.getApiParams());
					var clean = ['docId', 'docIdType', 'type', 'query', 'typeFilter'];
					for (var i=0; i<clean.length;i++) {
						if (!options.params[clean[i]]) {
							delete options.params[clean[i]];
						}
					}
					Ext.apply(options.params, {tool: 'CA'});
				},
				load: this.prepareData,
    			scope: this
    		}
    	});
        
        this.pcaStore = new Ext.data.JsonStore({
    		root : 'pca.types',
    		totalProperty : 'pca["@totalTypes"]',
    		fields: [
    		    {name: 'type', mapping: '@type'},
    		    {name: 'rawFreq', mapping: '@rawFreq', type: 'int'},
    		    {name: 'relativeFreq', mapping: '@relativeFreq', type: 'float'},
    		    {name: 'cluster', mapping: '@cluster', type: 'int'},
    		    {name: 'clusterCenter', mapping: '@clusterCenter', type: 'boolean'},
    		    {name: 'coordinates', mapping : 'vector'}
    		],
    		proxy: new Ext.data.HttpProxy({url:this.getTromboneUrl()}),
    		listeners: {
    			beforeload: function(store, options) {
					if (this.hidden || this.getCorpus().isEmpty()) {
						return false;
					}
					Ext.applyIf(options.params, this.getApiParams());
					var clean = ['docId', 'docIdType', 'type', 'query', 'typeFilter'];
					for (var i=0; i<clean.length;i++) {
						if (!options.params[clean[i]]) {
							delete options.params[clean[i]];
						}
					}
					Ext.apply(options.params, {tool: 'PCA'});
				},
				load: this.prepareData,
    			scope: this
    			
    		}
    	});
        
        this.typeStore = new Ext.data.JsonStore({
        	root : 'types',
    		fields: [
    		    {name: 'type', mapping: '@type'},
    		    {name: 'rawFreq', mapping: '@rawFreq', type: 'int'},
    		    {name: 'relFreqs', mapping: 'relFreqs'},
    		    {name: 'coordinates', mapping : 'vector'}
    		],
    		sortInfo: {field: 'rawFreq', direction: 'DESC'}
        });
        
        this.wordsList = new Ext.list.ListView({
			autoScroll: true,
			columns: [{
				header: this.localize('words'),
				dataIndex: 'type'
			},{
				header: this.localize('rawFreq'),
				dataIndex: 'rawFreq'
			}],
			store: this.typeStore,
			multiSelect: true,
			listeners: {
				click: function(dv, index, node, event) {
					var records = dv.getSelectedRecords();
					var types = [];
					for (var i = 0; i < records.length; i++) {
						types.push(records[i].get('type'));
					}
					if (types.length == 1) {
						/**
						 * @event corpusTypeSelected
						 * @param {Voyeur.Tool.ScatterPlot} tool
						 * @param {Object} params <ul>
						 * <li><b>type</b> : String</li>
						 * </ul>
						 * @type dispatcher
						 */
						Voyeur.application.dispatchEvent('corpusTypeSelected', this, {type: types[0]});
					} else if (records.length > 1) {
						/**
						 * @event corpusTypesSelected
						 * @param {Voyeur.Tool.ScatterPlot} tool
						 * @param {Object} params <ul>
						 * <li><b>type</b> : Array</li>
						 * </ul>
						 * @type dispatcher
						 */
						Voyeur.application.dispatchEvent('corpusTypeSelected', this, {type: types});
					}
					this.selectPoints(types);
				},
				scope: this
			}
        });
        
        Ext.apply(config, {
        	iconCls : 'chart-line',
        	layout: 'border',
        	items: [{
        		region: 'center',
        		html: '<span class="x-grid-empty">'+this.localize('loading')+'</span>'
        	},{
        		width: 200,
        		region: 'east',
        		collapsible: true,
        		title: this.localize('words'),
        		layout: 'fit',
        		items: this.wordsList,
	            tbar: [{
					xtype: 'button',
	    			text: this.localize('nearby'),
	    			iconCls: 'icon-arrow-out',
					handler: function(button) {
						var selected = this.wordsList.getSelectedIndexes();
						if (selected.length == 1) {
							var index = selected[0];
							var type = this.wordsList.getStore().getAt(index).get('type');
							var limit = Math.max(2000, Math.round(this.getCorpus().getTotalWordTokens() / 100));
							this.setApiParams({limit: limit, target: type});
							this.loadStore();
						} else if (selected.length > 1) {
							this.alertError({msg: this.localize('oneWordOnly'), width: 300});
						} else {
							this.alertError({msg: this.localize('noWordSelected'), width: 300});
						}
					},
					scope: this
	    		},'-',{
	    			xtype: 'button',
					text: this.localize('remove'),
					iconCls: 'icon-delete',
					handler: function(button) {
						var indexes = this.wordsList.getSelectedIndexes();
						if (indexes.length > 0) {
							var store = this.wordsList.getStore();
							var toRemove = [];
							for (var i = 0; i < indexes.length; i++) {
								var record = store.getAt(indexes[i]);
								toRemove.push(record);
								var type = record.get('type');
								var point = this.chart.get(type);
								if (point) point.remove(false, false);
							}
							store.remove(toRemove);
							this.chart.redraw();
						} else {
							this.alertError({msg: this.localize('noWordSelected'), width: 300});
						}
					},
					scope: this
				}],
				listeners: {
					collapse: function(p) {
						var box = this.getComponent(0).body.getBox();
				    	var size = Math.min(box.width, box.height);
				    	this.chart.setSize(size, size);
					},
					expand: function(p) {
						var box = this.getComponent(0).body.getBox();
				    	var size = Math.min(box.width-173, box.height);
				    	this.chart.setSize(size, size);
					},
					scope: this
				}
        	}],
			tbar: [this.analysisButton,'-',this.freqButton,'-',this.termsButton,'-',this.clustersButton,'-',this.dimsButton,'-',{
				xtype: 'button',
				text: this.localize('labels'),
				handler: function() {
					this.labelsMode++;
					if (this.labelsMode > 2) this.labelsMode = 0;
					this.doLabels();
				},
				scope: this
			},'-',{
                xtype: 'typeSearch',
                width: 110,
                parentTool: this,
                emptyText: this.localize('addWord'),
                listeners: {
                	typeSelected: function(combo, type, record) {
                		if (this.typeStore.findExact('type', type) == -1) {
	            	    	this.typeStore.loadData({'types': [{'@type': type, '@rawFreq': record.get('rawFreq')}]}, true);
	            	    	var sortState = this.typeStore.getSortState();
	            	    	this.typeStore.sort(sortState.field, sortState.direction);
	            	    	this.newType = type;
	                		this.loadStore();
                		}
            	    },
            	    scope: this
                }
            },'->',{
				xtype: 'button',
				text: this.localize('reset'),
				handler: function(button) {
					this.typeStore.removeAll();
					this.setApiParams({
						type: this.getApiParamDefaultValue('type'),
						limit: this.getApiParamDefaultValue('limit'),
						docId: this.getApiParamDefaultValue('docId'),
						dimensions: this.getApiParamDefaultValue('dimensions'),
						stopList: this.getApiParamDefaultValue('stopList'),
						analysis: this.getApiParamDefaultValue('analysis')
					});
					this.analysisButton.menu.items.get(1).setChecked(true);
					this.termsButton.menu.items.get(4).setChecked(true);
					this.dimsButton.menu.items.get(1).setChecked(true);
					this.loadStore();
				},
				scope: this
			}]
		});
        
        Voyeur.Tool.ScatterPlot.superclass.constructor.apply(this, arguments);
        
		this.exporters.png = this.localize('exportPNG');
		this.exporters.svg = this.localize('exportSVG');
        
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
        this.addListener('CorpusSummaryResultLoaded', function(src, data) {
        	var numDocs = this.getCorpus().getSize();
        	var analysis = this.getApiParamValue('analysis');
            if (analysis == 'ca' && numDocs == 3) {
            	this.setApiParams({dimensions: 2});
            	this.dimsButton.menu.items.get(0).setChecked(true); // need 1-2 docs or 4+ docs for 3 dimensions
            }
        	this.loadStore();
        }, this);
        
        /**
		 * @event corpusDocumentSelected
		 * @type listener
		 */
        this.addListener('corpusDocumentSelected', function(src, params) {
        	this.typeStore.removeAll();
			this.setApiParams({docId: params.docId});
			this.loadStore();
        }, this);
        
        this.addListener('export', function(exp) {
			if (exp=='png') {
				this.chart.exportChart();
			} else if (exp=='svg') {
				this.chart.exportChart({
					type: 'image/svg+xml'
				});
			}
		}, this);
    },
    
    loadStore: function() {
    	var types = [];
    	this.typeStore.each(function(record) {
    		types.push(record.get('type'));
    	});
    	var limit = Math.max(types.length, this.getApiParamValue('limit'));
    	this.setApiParams({type: types, limit: limit});

    	// check if the limit matches one of the terms values
    	this.termsButton.menu.items.each(function(item) {
    		item.setChecked(limit == parseInt(item.text), true);
    	});
    	
    	if (this.getApiParamValue('analysis') == 'pca') {
    		this.pcaStore.load();
    	} else {
    		this.caStore.load();
    	}
    },
    
    prepareData: function(store, records, options) {
    	var dims = this.getApiParamValue('dimensions');
    	var summary = '';
    	
    	if (this.getApiParamValue('analysis') == 'pca') {
    		// calculate the percentage of original data represented by the dominant principal components
			var pcs = store.reader.jsonData.pca.principalComponents;
			var eigenTotal = 0;
			for (var i = 0; i < pcs.length; i++) {
				var pc = pcs[i];
				eigenTotal += parseFloat(pc['@eigenValue']);
			}
			summary = this.localize('pcTitle')+'<br/>';
			var pcMapping = ['xAxis', 'yAxis', 'fill'];
			for (var i = 0; i < pcs.length; i++) {
				if (i >= dims) break;
				
				var pc = pcs[i];
				var eigenValue = parseFloat(pc['@eigenValue']);
				var percentage = eigenValue / eigenTotal * 100;
				summary += this.localize('pc')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%<br/>';
			}
    	} else {
    		summary = this.localize('caTitle')+'<br/>';
    		var pcMapping = ['xAxis', 'yAxis', 'fill'];
    		
    		var dimensions = store.reader.jsonData.ca.dimensions;
    		for (var i = 0; i < dimensions.length; i++) {
    			if (i >= dims) break;
    			
    			var percentage = parseFloat(dimensions[i]['@percentage']);
    			summary += this.localize('dimension')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%<br/>';
    		}
    	}
    	
    	var labels = {items: [{
			html: summary,
			style: {
				fontSize: '11px',
				left: '10px',
				top: '10px'
			}
		}]};
    	
    	var data = [{data: []},{data: []}];
		var fillMin = null;
		var fillMax = null;
		var freqMin = null;
		var freqMax = null;
		if (this.useFreqForSize || dims == 3) {
			for (var i = 0; i < records.length; i++) {
				var r = records[i];
				if (this.useFreqForSize) {
					var freq = r.get('rawFreq');
					if (r.get('category') != 'part') {
						if (freqMin == null || freq < freqMin) freqMin = freq;
						if (freqMax == null || freq > freqMax) freqMax = freq;
					}
				}
				if (dims == 3) {
					var z = r.get('coordinates')[2];
					if (fillMin == null || z < fillMin) fillMin = z;
					if (fillMax == null || z > fillMax) fillMax = z;
				}
			}
		}
		var maxZoom = Math.round((Math.abs(fillMax) + Math.abs(fillMin)) / 10) / 10;
		
		var target = this.getApiParamValue('target');
		var targetMatch = target == null;
		var newTypeMatch = this.newType == null;
		
		for (var i = 0; i < records.length; i++) {
			var r = records[i];
			
			var name = r.get('type');
			var category = r.get('category');
			var shortName = name;
			
			if (!targetMatch) {
				targetMatch = name == this.getApiParamValue('target');
			}
			if (!newTypeMatch) {
				newTypeMatch = name == this.newType;
			}
			
			var c = r.get('coordinates');
			var a = 0.65;
			if (dims == 3 && c[2] != null) {
				a = this.mapValue(c[2], fillMin, fillMax, 0.0, 1.0);
			}
			var color = this.WORD_COLOUR;
			var cluster = r.get('cluster');
			if (cluster >= 0) {
				color = this.colors[cluster];
			} else if (r.get('category') == 'part') color = this.PART_COLOUR;
			
			var radius = 2.5;
			if (this.useFreqForSize) {
				if (category != 'part') {
					radius = this.mapValue(r.get('rawFreq'), freqMin, freqMax, 2.5, this.sizeLimit);
				} else {
					radius = 5;
				}
			}
			
			if (category != 'part' && this.typeStore.findExact('type', name) == -1) {
				this.typeStore.addSorted(r);
			}
			
			if (name.length > 10) {
				var match = false;
				for (var j = 10; j > 0; j--) {
					var sep = name.substring(j).search(/\W/);
					if (sep != -1 && sep+j <= 10) {
						shortName = name.substring(0, j+sep);//+unescape("%u2026");
						match = true;
						// look ahead
						var nextSep = name.substring(j-1).search(/\W/);
						if (nextSep >= sep) {
							shortName = name.substring(0, (j-1)+nextSep);
							break;
						}
					}
				}
				if (!match) shortName = name.substring(0, 10);//+unescape("%u2026");
			}

			var point = {
				id: name,
				name: name,
				shortName: shortName,
				rawFreq: r.get('rawFreq'),
				relativeFreq: r.get('relativeFreq') * 10000,
				type: category, // can't name the property category due to conflict
				x: c[0],
				y: c[1] || i, // if for some reason there's only 1 dimension
				marker: {
					lineColor: 'rgb('+color+')',
					fillColor: 'rgba('+color+','+a+')',
					radius: radius,
					states: {
						hover: {
							radius: radius + 0.5
						},
						select: {
							radius: radius
						}
					}
				}
			};
			if (r.get('clusterCenter')) {
				point.marker.symbol = 'triangle';
			}
			
			if (category != 'part') {
				data[0].data.push(point);
			} else {
				data[1].data.push(point);
			}			
		}
		
		this.drawScatterPlot(data, labels, maxZoom);
    	
		if (!targetMatch) {
			this.alertInfo({msg: this.localize('noWordsNear')+' '+target+'.', width: 300});
			var index = this.wordsList.getStore().findExact('type', target);
			this.typeStore.removeAt(index);
			this.setApiParams({target: null, limit: 50});
		} else if (target != null) {
    		this.selectPoints([target]);
    		var index = this.wordsList.getStore().findExact('type', target);
			this.wordsList.select(index);
    		this.setApiParams({target: null, limit: 50});
		} else if (!newTypeMatch) {
			this.alertInfo({msg: this.localize('noMatchesFor')+' '+this.newType+'.', width: 300});
			this.newType = null;
    	} else if (this.newType != null) {
    		this.selectPoints([this.newType]);
    		var index = this.wordsList.getStore().findExact('type', this.newType);
			this.wordsList.select(index);
    		this.newType = null;
    	}
    },
    
    drawScatterPlot: function(series, labels, maxZoom) {
    	var box = this.getComponent(0).body.getBox();
    	var size = Math.min(box.width, box.height);
    	
    	var axisConfig = [{
    		title: {text: ''},
    		maxZoom: maxZoom,
			tickLength: 0,
			lineWidth: 1,
	        gridLineWidth: 0,
	        startOnTick: true,
	        endOnTick: true,
	        tickPixelInterval: size / 10
		},{
	    	opposite: true,
	    	lineWidth: 1,
	    	maxZoom: 10,
	    	title: {text: ''}
	    }];
    	
    	var that = this;
    	
    	if (this.chart) this.chart.destroy();
    	
    	this.chart = new Highcharts.Chart({
    		chart: {
    			renderTo: this.getComponent(0).body.dom,
    			defaultSeriesType: 'scatter',
    			margin: [25, 25, 25, 45],
    			zoomType: 'xy',
    			width: size,
    			height: size
    		},
    		title: {
    			text: ''
    		},
    		subtitle: {
    			text: ''
    		},
    		xAxis: axisConfig,
    		yAxis: axisConfig,
    		tooltip: {
    			formatter: function() {
    				var tip = '<b>'+this.point.name+'</b><br/><b>';
    				if (this.point.type == 'part') {
    					tip += that.localize('wordCount')+'</b><br/>'+this.point.rawFreq;
    				} else {
    					tip += that.localize('rawFreq')+'</b><br/>'+this.point.rawFreq+'<br/><b>'+that.localize('relFreq')+'</b><br/>'+this.point.relativeFreq;
    				}
    				return tip;
    			}
    		},
    		legend: {
    			layout: 'vertical',
    			align: 'left',
    			verticalAlign: 'top',
    			x: 100,
    			y: 70,
    			floating: true,
    			backgroundColor: '#FFFFFF',
    			borderWidth: 1,
    			enabled: false
    		},
    		plotOptions: {
    			scatter: {
    				marker: {
						lineWidth: 0.5,
    					states: {
    						hover: {
    							enabled: true,
    							lineWidth: 1,
    							lineColor: '#555'
    						},
    						select: {
    							enabled: true,
    							fillColor: '#FFFF00',
    							lineColor: '#000',
    							lineWidth: 1
    						}
    					}
    				},
    				dataLabels: {
    					enabled: true,
    					color: 'rgba(128, 128, 128, 1)',
    					y: -5,
    					formatter: function() {
    						return this.point.shortName;
    					}
    				},
    				stickyTracking: false,
    				point: {
    	    			events: {
    	    				mouseOver: function(event) {
    	    					document.body.style.cursor = 'pointer';
    	    					that.lastHoveredWord = this;
    	    				},
    	    				mouseOut: function(event) {
    	    					that.lastHoveredWord = null;
    	    					document.body.style.cursor = 'auto';
    	    				},
    	    				click: function(event) {
    							var type = event.point.config.name;
    							var category = event.point.config.type;
    							if (category != 'part') {
    								/**
    								 * @event corpusTypeSelected
    								 * @param {Voyeur.Tool.ScatterPlot} tool
    								 * @param {Object} params <ul>
    								 * <li><b>type</b> : String</li>
    								 * </ul>
    								 * @type dispatcher
    								 * @private
    								 */
    								Voyeur.application.dispatchEvent('corpusTypeSelected', that, {type: type});
    								var index = that.wordsList.getStore().findExact('type', type);
        							that.wordsList.select(index);
    							}
    							event.point.select(true, false);
    						}
    	    			}
    	    		}
    			}
    		},
    		labels: labels,
    		series: series,
    		exporting: {
    			enabled: false
    		},
    		credits: {
    			enabled: false
    		}
    	});
    	
    	this.getComponent(0).body.on('contextmenu', function(event, el, obj) {
			event.preventDefault();
			event.stopPropagation();
			if (this.lastHoveredWord != null) {
				var name = this.lastHoveredWord.name;
				var menu = new Ext.menu.Menu({
					floating: true,
					items: [{
						text: this.localize('nearby'),
						handler: function() {
							var limit = Math.max(2000, Math.round(this.getCorpus().getTotalWordTokens() / 100));
							this.setApiParams({limit: limit, target: name});
							this.loadStore();
						},
						scope: this
					},{
						text: this.localize('remove')+' <b>'+name+'</b>',
						handler: function() {
							var index = this.typeStore.findExact('type', name);
							this.typeStore.removeAt(index);
    						this.chart.get(name).remove(true, true);
						},
						scope: this
					}]
				});
				menu.showAt(event.getXY());
			}
		}, this);
    	
    	// swap the labels and the data points so that roll overs work in crowded areas
    	var seriesGroup = Ext.get(this.chart.container).child('g[class=highcharts-series-group]');
    	var dataLabels = Ext.DomQuery.jsSelect('g[class=highcharts-highcharts-data-labels]', this.chart.container);
    	for (var i = 0; i < dataLabels.length; i++) {
    		var label = Ext.get(dataLabels[i]);
    		label.set({zIndex: 2}); // svg doesn't support zIndex yet, but we'll do this anyhow
    		label.insertBefore(seriesGroup);
    	}
    	
    	this.doLabels();
    },
    
    doLabels: function() {
    	var text = Ext.get(this.chart.container).child('text');
		switch (this.labelsMode) {
			case 0:
				text.show();
				for (var i = 0; i < this.chart.series.length; i++) {
					this.chart.series[i].dataLabelsGroup.show();
				}
				break;
			case 1:
				text.hide();
				for (var i = 0; i < this.chart.series.length; i++) {
					this.chart.series[i].dataLabelsGroup.show();
				}
				break;
			case 2:
				text.hide();
				for (var i = 0; i < this.chart.series.length; i++) {
					this.chart.series[i].dataLabelsGroup.hide();
				}
				break;
		}
    },
    
    selectPoints: function(ids) {
    	for (var id in this.pointHighlights) {
			if (ids.indexOf(id) == -1) {
				this.pointHighlights[id].destroy();
				delete this.pointHighlights[id];
			}
		}
    	
    	var prevPoints = this.chart.getSelectedPoints();
    	if (prevPoints.length > 0) {
    		for (var i = 0; i < prevPoints.length; i++) {
    			var prevPoint = prevPoints[i];
    			if (ids.indexOf(prevPoint.id) == -1) {
    				prevPoint.select(false, false);
    			}
    		}
    	}
    	for (var i = 0; i < ids.length; i++) {
    		var id = ids[i];
	    	var point = this.chart.get(id);
	    	if (point && !point.selected) {
		    	var x = point.plotX + this.chart.plotLeft;
		    	var y = point.plotY + this.chart.plotTop;
		    	var r = point.options.marker.radius;
		    	if (!this.pointHighlights[id]) {
			    	var highlight = this.chart.renderer.circle(x, y, r+10);
			    	highlight.attr({fill: 'rgba(255,255,0,0.5)', 'stroke-width': 0.5, stroke: 'rgba(0,0,0,0.5)'});
			    	highlight.add();
			    	this.pointHighlights[id] = highlight;
			    	this.animatePointHighlight.defer(40, this, [20, r+20, 0.5, id]);
		    	}
	    	}
    	}
    },
    
    animatePointHighlight: function(step, radius, alpha, id) {
    	step--;
    	radius--;
    	alpha += 0.025;
    	var highlight = this.pointHighlights[id];
    	if (highlight) {
	    	highlight.attr({fill: 'rgba(255,255,0,'+alpha+')', r: radius, stroke: 'rgba(0,0,0,'+alpha+')'});
	    	if (step > 0) {
	    		this.animatePointHighlight.defer(35, this, [step, radius, alpha, id]);
	    	} else {
	    		highlight.destroy();
	    		delete this.pointHighlights[id];
	    		this.chart.get(id).select(true, true);
	    	}
    	} else {
    		this.chart.get(id).select(false, false);
    	}
    },
    
    mapValue: function(value, istart, istop, ostart, ostop) {
        return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
    },
    
    showOptions : function() {
        this.showOptionsWindow({
            items : [{
                xtype : 'form',
                labelWidth : 175,
                labelAlign : 'right',
                border : false,
                items : [{
                	xtype: 'checkbox',
                	name: 'rawFreqSizing',
                	fieldLabel: this.localize('rawFreqSizing'),
                	checked: this.useFreqForSize
                }],
                buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								var rawFreqSizing = form.findField('rawFreqSizing');
								
								if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery);
								}

								if (form.isDirty()) {
									var initValue = this.getApiParamValue('stopList');
									if (initValue != stopList.getValue()) {
										this.typeStore.removeAll();
									}
									this.setApiParams({
										stopList: stopList.getValue()
									});
									this.useFreqForSize = rawFreqSizing.getValue();
									if (global) {
										this.getApplication().applyParamsGlobally({
											stopList: this.getApiParamValue('stopList')
										}, true);
									}
									else {
										this.loadStore();
									}
								}
								formPanel.findParentByType('window').destroy();
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}]
            }]
        }, true);
    }
	
	,api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		,analysis: {'default': 'ca', 'choices': ['pca', 'ca']}
		,freqType: {'default': 'relative', 'choices': ['relative', 'raw']}
		,target: {'default': null}
		,clusters: {'default': null}
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		,start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		,dimensions: {'default': 3}
		/**
		 * @property sortBy The property to sort results by.
		 * @type String
		 * @default rawFreq
		 */
		,sortBy: {'default': 'rawFreq'}
		/**
		 * @property sortDirection The direction to sort results in.
		 * @type String
		 * @default ASC
		 * @choices ASC, DESC
		 */
		,sortDirection: {'default': 'DESC'}
		/**
		 * @property bins How many "bins" to separate a document into.
		 * @type Integer
		 * @default 10
		 */
		,bins: {'default': 10}
		/**
		 * @property type The corpus type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,type: {'default': null}
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default undefined
		 */
		,docId: {'default': undefined}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIdType: {'default': null}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default undefined
		 */
		,docIndex: {'default': undefined}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'corpusDocumentSelected']
		,dispatchers: ['corpusTypeSelected']
	}
	
	,thumb: {
		large: 'ScatterPlot.png'
	}
	
    ,i18n : {
        title : {en: 'ScatterPlot'}
        ,type : {en: 'Visualization'}
        ,pcTitle : {en : 'Percentage of Total Variation Explained by Each Component'}
        ,pc : {en : 'PC'}
        ,caTitle : {en : 'Percentage of Total Assocation Explained by Each Dimension'}
        ,dimension : {en : 'Dimension'}
        ,xAxis : {en : 'X Axis'}
        ,yAxis : {en : 'Y Axis'}
        ,fill : {en : 'Fill'}
        ,analysis : {en : 'Analysis'}
        ,pca : {en : 'Principal Components Analysis'}
        ,ca : {en : 'Correspondence Analysis'}
        ,freqType : {en: 'Frequency Type'}
        ,terms : {en: 'Terms'}
        ,clusters : {en: 'Clusters'}
        ,dimensions : {en: 'Dimensions'}
        ,labels : {en: 'Labels'}
        ,rawFreqSizing : {en: 'Use Raw Frequency For Sizing'}
        ,nearby : {en: 'Nearby'}
        ,noWordsNear : {en: 'No words found near'}
        ,noMatchesFor : {en: 'No matches for'}
        ,reset : {en: 'Reset'}
        ,rawFreq: {en : 'Raw Frequency'}
        ,relFreq: {en : 'Relative Frequency'}
        ,wordCount: {en : 'Word Count'}
        ,words: {en : 'Words'}
        ,addWord: {en : 'Add Word'}
        ,remove: {en : 'Remove'}
        ,noWordSelected: {en : 'No word selected.'}
        ,oneWordOnly: {en : 'Please select one word when performing this action.'}
        ,exportPNG: {en : 'a static PNG image in a new window'}
		,exportSVG: {en : 'a static SVG image in a new window'}
		,loading: {en : 'Loading'}
		,minDocsForDims: {en: 'Because of the nature of Correspondence Analysis, you can only use 2 dimensions with 3 documents.'}
        ,help: {en: 'This tool displays the results of a statistical analysis using a scatter plot visualization. '+
        	'There are two types of analysis available: Principal Component Analysis and Correspondence Analysis.<br/><br/>'+
        	'<b>Principal Component Analysis</b> is a technique which takes data in a multidimensional space and optimizes it, reducing the dimensions to a manageable subset. '+
        	'It is a way of transforming the data with respect to its own structure, so that associations between data points become more readily apparent.<br/><br/>'+
        	'For example, consider a table of word frequencies for a corpus of ten documents. '+
        	'Each document can be thought of as a dimension, and each word frequency as a data point. '+
        	'Since we cannot visualize a ten dimensional space, we can apply PCA to reduce the number of dimensions to something feasible, like two or three. '+
        	'This is accomplished by transforming the data into a new space wherein the first few dimensions (or components) represent the largest amount of variability in the data. '+
        	'By discarding all but the first two or three dimensions, we will be left with a new data set which ideally contains most of the information in the original, but which is easy to visualize. '+
        	'In the resulting visualization, words that are grouped together are associated, i.e. they follow a similar usage in the corpus.<br/><br/>'+
        	'<b>Correspondence Analysis</b> is conceptually similar to PCA, but handles the data in such a way that both the rows and columns are analyzed. '+
        	'This means that given a table of word frequencies, both the words themselves and the document segments will be plotted in the resulting visualization.'
        }
        ,adaptedFrom: {en: ''}
    }
});

Ext.reg('voyeurScatterPlot', Voyeur.Tool.ScatterPlot);

/**
 * @class Voyeur.Tool.CorpusGrid A panel that provides an overview of the current corpus and provides widgets for updating the corpus. 
 * @extends_ext Ext.grid.GridPanel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.CorpusGrid = Ext.extend(Ext.grid.GridPanel, {
	
	corpusLastModified : 0
	
	,constructor : function(config) {
	
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		this.exporters.dhq = this.localize('exportDhq');

		var store = new Ext.data.GroupingStore({
			reader : new Ext.data.JsonReader(
				{root : 'corpus.documents'}
				,Ext.data.Record.create(Voyeur.data.Document.fields)
			)
			,sortInfo : {field : this.getApiParamValue('sortBy'), direction : this.getApiParamValue('sortDirection')}
			,groupField: this.getApiParamValue('author')
		})
		
		var xtypePrefix = config.xtype+'.';
		
		config.viewConfig = config.viewConfig ? config.viewConfig : {};
		Ext.applyIf(config.viewConfig, {
			forceFit:false
//			,autoFill:true
			,emptyText : this.localize('noResults','tool')
			,deferEmptyText: false
			,enableGroupingMenu : true
			,enableGrouping: true
			,showGroupName : false
		})
		
		var sm = new Ext.grid.CheckboxSelectionModel({});
		
		// store a reference to the panel
		var panel = this;

		Ext.applyIf(config, {
			view : new Ext.grid.GroupingView(config.viewConfig)
			,iconCls : 'corpus'
			,stripeRows : true
			,sm : sm
			,enableColumnMove : false
			,autoExpandColumn : this.getId()+'-column-label'
//			,enableDragDrop: true
//			,ddGroup: 'testing'
			,colModel : new Ext.grid.ColumnModel([
// sm
//				{header : this.localize('documentIndex'), dataIndex : 'index', sortable : true, hidden: true, tooltip : this.localize('documentIndexTip')}
				{header : this.localize('documentLabel'), dataIndex : 'id', id : this.getId()+'-column-label', sortable : true, tooltip : this.localize('documentLabelTip'), renderer:function(val) {return panel.getCorpus().getDocument(val).getLabel()}}
				,{header : this.localize('documentTitle'), dataIndex : 'title', id : this.getId()+'-column-title', hidden: true, sortable : true, tooltip : this.localize('documentTitleTip')}
				,{header : this.localize('documentAuthor'), dataIndex : 'author', id : this.getId()+'-column-author', hidden: true, sortable : true, tooltip : this.localize('documentAuthorTip'), renderer: function(val) {return val ? val : '?'}}
				,{header : this.localize('documentTime'), dataIndex : 'timeInMillis', id : this.getId()+'-column-time', hidden: true, sortable : true, tooltip : this.localize('documentTimeTip'), width: 110, renderer: function(val){return new Date(val).format("Y-m-d H:i")}}
				,{header : this.localize('totalDocumentWordTokens'), dataIndex : 'totalWordTokens', sortable : true, tooltip : this.localize('totalDocumentWordTokensTip'), width: 70, renderer: Ext.util.Format.numberRenderer('0,000')}
				,{header : this.localize('totalDocumentWordTypes'), dataIndex : 'totalWordTypes', sortable : true, tooltip : this.localize('totalDocumentWordTypesTip'), width: 70, renderer: Ext.util.Format.numberRenderer('0,000')}
				,{header : this.localize('totalDocumentWordDensity'), dataIndex : 'wordDensity', sortable : true, tooltip : this.localize('totalDocumentWordDensityTip'), width: 70, renderer: Ext.util.Format.numberRenderer('0,000.0')}
			]),
			store : store
			,tbar : [{
				xtype: 'tbtext'
				,text: '&nbsp;'
			}]
		});
		Voyeur.Tool.CorpusGrid.superclass.constructor.call(this, config);

		this.addListener('rowclick', function() {this.fireSelectionChange()}, this)
		this.addListener('headerclick', function(src, grid, rowIndex) {if (rowIndex==0) this.fireSelectionChange();}, this);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			if (this.rendered) {this.fireEvent('afterrender', this)};
		}, this);


		this.addListener('afterrender', function(src, params) {
			this.showResults()
		}, this)
		
		this.addListener('export', function(exp) {
	    	 	if (exp=='dhq') {
	        	 	var params = {
    	    	 		corpus : panel.getCorpus().getId()
    	    	 	}
    	    	 	var url = panel.getTromboneUrl();
	    	 		var sm = this.getSelectionModel();
	    	 		var count = sm.getCount();
	    	 		if (count!=1) {
	    	 			return this.alertError({msg: this.localize('selectExactlyOneForExport')});
	    	 		}
	    	 		Ext.applyIf(params, {
	    	 			outputFormat: 'xml'
	    	 			,template : 'docExport2dhqAuthor'
	    	 			,docId: sm.getSelected().get('id')
	    	 		})
	    	 		Ext.applyIf(params, {tool: 'DocumentExporter'})
					var win = window.open(url+'?'+Ext.urlEncode(params));
					if (win) {win.focus();}
	    	 	}
			
		}, this);
		
		this.addListener('sortchange', function(panel, sortInfo) {
			this.setApiParams({sortBy: sortInfo.field, sortDirection: sortInfo.direction});
		}, this);
	}

	,showResults: function() {
		var corpus = this.getCorpus();
		var documents = corpus.getDocuments();
		var records = [];
		documents.each(function(item) {records.push(item.record)});
		this.getStore().removeAll();
		this.getStore().add(records);
		var el = this.getTopToolbar().items.get(0).setText(documents.getCount()+" documents with "+
				Ext.util.Format.number(corpus.get('totalWordTokens'),'000,0')+' tokens and '+
				Ext.util.Format.number(corpus.get('totalWordTypes'),'000,0')+' types');
		// determine if we have authors and if not remove grouping
		if (corpus.getAuthors().length<2) {this.getStore().clearGrouping();}
		this.corpusLastModified = corpus.get('lastModified')
	}
	
	,lastSelectionTime : 0
	,fireSelectionChange : function() {
		var time = new Date().getMilliseconds();
		this.lastSelectionTime=time;
		var me = this;
		setTimeout(function() {if (me.lastSelectionTime == time) {
			documentIds = [];
			records = me.getSelectionModel().getSelections();
			for (var i = 0; i < records.length; i++) {
				documentIds.push(records[i].get('id'))
			}
			if (documentIds.length == 1) {
				/**
				 * @event corpusDocumentSelected
				 * @param {Voyeur.Tool.CorpusGrid} tool
				 * @param {Object} params <ul>
				 * <li><b>docId</b> : String</li>
				 * <li><b>record</b> : {Ext.data.Record}</li>
				 * </ul>
				 * @type dispatcher
				 */
				Voyeur.application.dispatchEvent('corpusDocumentSelected', this, {docId: documentIds[0], record: records[0]});
			}
			else if (documentIds.length > 1) {
				/**
				 * @event corpusDocumentsSelected
				 * @param {Voyeur.Tool.CorpusGrid} tool
				 * @param {Object} params <ul>
				 * <li><b>docId</b> : Array</li>
				 * <li><b>records</b> : {Array}</li>
				 * </ul>
				 * @type dispatcher
				 */
				Voyeur.application.dispatchEvent('corpusDocumentsSelected', this, {docId: documentIds, record: records});
			}
		}}, 1000);
	}
	
	,api: {
		/**
		 * @property sortBy The property to sort results by.
		 * @type String
		 * @default index
		 * @choices index, id, title, author, timeInMillis, totalWordTokens, totalWordTypes, wordDensity
		 */
		'sortBy': {
			'default': 'index'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
			,'choices': ['index','id','title','author','timeInMillis','totalWordTokens','totalWordTypes','wordDensity']
		}
		/**
		 * @property sortDirection The direction to sort results in.
		 * @type String
		 * @default ASC
		 * @choices ASC, DESC
		 */
		,'sortDirection': {
			'default': 'ASC'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
			,'choices': ['ASC','DESC']
		}
		/**
		 * @property group The property to group results with.
		 * @type String
		 * @choices author, timeInMillis
		 */
		,'group': {
			'default': 'author'
			,'type': String
			,'required': false
			,'value': null
			,'multiple': false
			,'choices': ['author','timeInMillis']
		}
		,toolType: ['Table', 'Corpus']
		,listeners: ['CorpusSummaryResultLoaded']
		,dispatchers: ['corpusDocumentSelected', 'corpusDocumentsSelected']
	}
	
	,thumb: {
		large: 'CorpusGrid.png'
	}
	
	// private localization variables
	,i18n : {
		title : {en: "Corpus"}
	    ,type : {en: "Summary"}
		,documentLabel: {en: "Document Label"}
		,documentLabelTip : {en: "This provides a compact representation of the document position, author, title, and modification time, as available."}
		,documentTitle : {en: "Title"}
		,documentTitleTip : {en: "The document's title."}
		,documentAuthor : {en: "Author"}
		,documentAuthorTip : {en: "The document's author (note that in many cases this may not be known)."}
		,documentTime : {en: "Time"}
		,documentTimeTip : {en: "The document's apparent publication time (note that in many cases the real time may not be known, so the document is assigned the time at which it was analyzed)."}
		,totalDocumentWordTokens : {en: "Tokens"}
		,totalDocumentWordTokensTip : {en: "The total number of word tokens in the document (all words)."}
		,totalDocumentWordTypes : {en: "Types"}
		,totalDocumentWordTypesTip : {en: "The total number of word types in the document (all unique words)."}
		,totalDocumentWordDensity : {en: "Density"}
		,totalDocumentWordDensityTip : {en: "<p>A simple measure of the document's word density the higher the value, the richer the vocabulary. Formula: <pre>(number of types / number of tokens) * 1000</pre>"}
		,help : {en : "<p>This tool shows an overview of the corpus, including each document's title, number of word tokens (total words), number or word types (unique words), and lexical density (the ratio of tokens to types).</p>"}
		,exportDhq: {en: 'TEI (XML)'}
		,exportDhq: {en: 'DHQAuthor (XML)'}
		,selectExactlyOneForExport: {en: 'Please select exactly one document for exporting.'}
	}
});

Ext.reg('voyeurCorpusGrid', Voyeur.Tool.CorpusGrid);



/**
 * @class Voyeur.Tool.Flowerbed A visualization tool for displaying text and features.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Andrew MacDonald
 */
Voyeur.Tool.Flowerbed = Ext.extend(Ext.Panel, {
	
	INTERVAL: Ext.isGecko ? 150 : 100, // milliseconds between each redraw
	DISMISS_DELAY: 2500, // milliseconds before tooltip auto hides
	
	colors: ['116,116,181', '139,163,83', '189,157,60', '171,75,75', '174,61,155'],
	pi2: Math.PI * 2,
	
	textAngle: -1.35, // in radians
	
	fontSize: 16,
	fontFamily: 'Arial',
	
	overToken: null,
	
	tokens: null,
	canvas: null,
	ctx: null,
	
	maxFreq: 0,
//	maxFreqChanged: false,
	
	selectedDocs: null,
	
	cache: new Ext.util.MixedCollection(),
	
	getObjectId : function() {
		return this.id.replace(/-/g,'_')+'_flowerbed';
	},
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		Ext.applyIf(config, {
            border: false,
            items: {
            	xtype: 'container',
            	autoEl: 'div',
            	id: 'canvasParent'
            	//,autoScroll: true
            },
            tbar: [{
                tooltip: this.localize('first'),
                iconCls: 'x-tbar-page-first',
                handler: this.getFirst,
                scope: this
            },{
                tooltip: this.localize('previous'),
                iconCls: 'x-tbar-page-prev',
                handler: this.getPrevious,
                scope: this
            },{
                tooltip: this.localize('next'),
                iconCls: 'x-tbar-page-next',
                handler: this.getNext,
                scope: this
            },{
                tooltip: this.localize('last'),
                iconCls: 'x-tbar-page-last',
                handler: this.getLast,
                scope: this
            },'-',{
            	xtype: 'documentSelector',
            	max: 2,
            	listeners: {
            		documentsSelected: function(docIds) {
            			this.setApiParams({docId: docIds});
            			this.filterDocuments();
            			this.getTokens();
            		},
            		scope: this
            	}
            },'-',{
				xtype: 'combo',
				name: 'languages',
				emptyText: this.localize('languages'),
				width: 130,
				height: 300,
				allowBlank: false,
				editable: true,
				forceSelection: true,
				mode: 'local',
				displayField: 'lang',
				valueField: 'code',
				store: this.getLocalization().langCodesStore
			},{
				xtype: 'button',
				text: this.localize('translate'),
				tooltip: this.localize('translateTip'),
				anchor: '100%',
				handler: function(button) {
					var lang = button.previousSibling().getValue();
					var docIds = this.getApiParamValue('docId');
					if (typeof docIds == 'string') docIds = [docIds];
					var docId = docIds[0];
					var docIndex = this.getCorpus().getDocument(docId).getIndex();
					this.update({
						tool: 'Translator',
						params: {
							referrer: this.getApplication().getBaseUrl(),
//							source: this.getLocalization().getLang(),
							target: lang,
							corpus: this.getCorpus().getId(),
							docIndex: docIndex
						}
					});
				},
				scope: this
			}]
		});
		
		Voyeur.Tool.Flowerbed.superclass.constructor.apply(this, [config]);
		
		// hack to add legend to graph, right before export
		for (var i = 0; i < this.tools.length; i++) {
			var tool = this.tools[i];
			if (tool.id == 'save') {
				var that = this;
				var me = tool;
				var oldHandler = me.handler;
				tool.handler = function(event, tool, panel) {
					that.drawGraph(true);
					oldHandler.apply(me.scope, [event, tool, panel]);
				}
			}
		}
		
		// add css
//		var link = document.createElement('link');
//		link.href = this.getToolDirectoryUrl()+'Flowerbed.css';
//		link.rel = 'stylesheet';
//		link.type = 'text/css';
//		document.getElementsByTagName('head')[0].appendChild(link);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			this.filterDocuments();
			
			var docIds = this.getApiParamValue('docId');
			if (typeof docIds == 'string') docIds = [docIds];
			this.getTopToolbar().findByType('documentSelector')[0].populate(docIds, true);
			
			var container = Ext.getCmp('canvasParent');
			
			var height = container.ownerCt.getHeight() - 30;
			var width = container.ownerCt.getWidth();
			
			var limit = Math.floor(width / 30); // 30 is the average token width
			this.setApiParams({limit: limit});
			
			var id = this.getObjectId();
			container.add({
				width: width,
				height: height,
//				autoScroll: true,
				html: '<canvas id="'+id+'" width="'+width+'" height="'+height+'"></canvas>',
				border: false,
            	listeners: {
            		afterrender: {
            			fn: function(cnt) {
        					this.canvas = document.getElementById(id);
            				this.ctx = this.canvas.getContext('2d');
            				this.canvas.addEventListener('click', this.clickHandler.createDelegate(this), false);
            				this.canvas.addEventListener('mousedown', this.mouseDownHandler.createDelegate(this), false);
            				this.canvas.addEventListener('mouseup', this.mouseUpHandler.createDelegate(this), false);
            				this.canvas.addEventListener('mousemove', this.moveHandler.createDelegate(this), false);
            				
            				this.getTokens();
            			},
            			single: true,
            			scope: this
            		}
            	}
			});
			container.doLayout();
		}, this);
		
		this.addListener('TranslatorResultLoaded', function(src, data) {
			this.setApiParams({docId: null, start: 0}); // remove old docIds
			
			var corpusId = new Date().getTime()+'.'+parseInt(Math.random()*10000);
			var translatedTexts = data.translator.texts;
			translatedTexts.splice(0, 0, data.translator.source);
			var inputs = [];
			for (var i = 0; i < translatedTexts.length; i++) {
				var t = translatedTexts[i]
				var text = t['@text'];
				var code = t['@lang'];
				var lang = this.getLocalization().getLangName(code);
				inputs.push(text);
			}
			this.update({
				tool: 'CorpusSummary',
				params: {
					corpus: corpusId,
					input: inputs,
					corpusCreateIfNotExists: true
				}
			}, this);
		}, this);
		
		this.addListener('TokensResultLoaded', function(src, data) {
			this.tokens = data.tokens;
			this.cache.clear();
			for (var i = 0; i < this.tokens.documents.length; i++) {
				var doc = this.tokens.documents[i];
				var inv = i % 2 != 0;
				this.cacheTokens(doc, inv);
				if (i >= 1) break;
			}
			this.drawGraph();
		}, this);
		
		this.addListener('exportComplete', function() {
			this.drawGraph();
		}, this);
	},
	
	filterDocuments: function() {
		var docIds = this.getApiParamValue('docId');
		if (typeof docIds == 'string') docIds = [docIds];
		
		if (docIds == null) {
			this.selectedDocs = this.getCorpus().getDocuments().clone();
			var count = this.selectedDocs.getCount();
			if (count > 2) {
				for (var i = 2; i < count; i++) {
					this.selectedDocs.removeAt(2);
				}
			}
			docIds = [];
			this.selectedDocs.eachKey(function(docId, doc) {
				docIds.push(docId);
			}, this);
			this.setApiParams({docId: docIds});
		} else {
			this.selectedDocs = this.getCorpus().getDocuments().filterBy(function(doc, docId) {
				return docIds.indexOf(docId) != -1;
			}, this);
		}
	},
	
	cacheTokens: function(doc, inversed) {
		var textAngle = inversed ? -this.textAngle : this.textAngle;
		
		var currX = 5;
		var y = 250;
		if (inversed) y += 19;
		this.ctx.save();
        this.ctx.textBaseline = 'alphabetic';
        this.ctx.font = this.fontSize + 'px '+ this.fontFamily;
		
        var height = Math.ceil(this.ctx.measureText('m').width * 1.15);
		var sin = Math.sin(textAngle);
		var cos = Math.cos(textAngle);
		
		for (var j = 0; j < doc.tokens.length; j++) {
			var token = doc.tokens[j];
			var text = token['@token'];
			var relativeFreq = parseFloat(token['@relativeFreq']) * 2500;
			if (relativeFreq < 1) relativeFreq = 1;
			if (inversed) relativeFreq = -relativeFreq;
			
			if (relativeFreq > this.maxFreq) {
				this.maxFreq = relativeFreq;
			}
			
			var features = [];
			for (var k = 0; k < this.colors.length; k++) {
				if (Math.random() > 0.5) {
					features.push(this.colors[k]);
				}
			}
			
			var width = this.ctx.measureText(text).width;
			
			var bb = this.getBoundingBox(width, height, sin, cos);
			
			var tokenObj = {
				token: text,
				relativeFreq: relativeFreq,
				features: features,
				width: bb.width,
				height: bb.height,
				x: currX,
				y: y,
				inversed: inversed
			};
			this.cache.add(tokenObj);
			currX += tokenObj.width + 5;
		}
		
		this.ctx.restore();
	},
	
	getBoundingBox: function(width, height, sin, cos) {
		var x1 = width * cos - (-height) * sin;
		var y1 = width * sin + (-height) * cos;
		var x2 = width * cos - height * sin;
		var y2 = width * sin + height * cos;
		
		var realWidth = Math.max(Math.abs(x1), Math.abs(x2));
		var realHeight = Math.max(Math.abs(y1), Math.abs(y2));
		
		return {width: realWidth, height: realHeight};
	},
	
	drawGraph: function(includeLegend) {
		this.clearCanvas();
		
		this.ctx.textBaseline = 'alphabetic';
        this.ctx.font = this.fontSize + 'px '+ this.fontFamily;
		
        // earth
		this.ctx.fillStyle = 'rgba(126, 73, 38, 1)';
		this.ctx.fillRect(5, this.cache.get(0).y-1, this.canvas.width-5, 20);
		
		this.cache.each(function(item, index, length) {
			this.ctx.save();
			this.ctx.translate(item.inversed ? item.x : item.x+10, item.y-item.relativeFreq);
			this.ctx.rotate(item.inversed ? -this.textAngle : this.textAngle);
			this.ctx.fillStyle = 'rgba(59, 101, 51, 1.0)';
			this.ctx.fillText(item.token, item.inversed ? 2 : 0, 0);
			this.ctx.restore();
			
			var gradient = this.ctx.createLinearGradient(item.x, item.y, item.x, item.y-item.relativeFreq);
			gradient.addColorStop(0.65, 'rgba(59, 101, 51, 1)');
			gradient.addColorStop(1, 'rgba(59, 101, 51, 0)');
			this.ctx.fillStyle = gradient;
			
			this.ctx.fillRect(item.x, item.y-item.relativeFreq, item.width, item.relativeFreq);
			
			this.drawFlower(item, item.y-item.relativeFreq);
		}, this);
	},
	
	drawFlower: function(item, cy) {
		var rotInc = Math.PI / this.colors.length;
		if (item.inversed) rotInc = -rotInc;
		var width = item == this.overToken ? 7.5 : 5;
		var height = item == this.overToken ? 15 :10;
		var cx = item.x+(item.width*0.5);
		this.ctx.save();
		this.ctx.translate(cx, item.inversed ? cy+item.height : cy-item.height);
		this.ctx.rotate(Math.PI*0.5+(rotInc*0.5 * (this.colors.length-item.features.length)));
		for (var i = 0; i < item.features.length; i++) {
			var color = item.features[i];
			this.ctx.fillStyle = 'rgba('+color+', 0.8)';
			this.drawEllipse(0, height, width, height);
			this.ctx.rotate(rotInc);
		}
		this.ctx.restore();
	},
	
	drawEllipse: function(centerX, centerY, width, height) {
		this.ctx.beginPath();
		this.ctx.moveTo(centerX, centerY - height);
		this.ctx.bezierCurveTo(
			centerX + width, centerY - height,
			centerX + width, centerY + height,
			centerX, centerY + height
		);
		this.ctx.bezierCurveTo(
			centerX - width, centerY + height,
			centerX - width, centerY - height,
			centerX, centerY - height
		);
		this.ctx.fill();
		this.ctx.closePath();
	},
	
	drawLegend: function() {
		
	},
	
	drawToolTip: function() {
		
	},
	
	moveHandler: function(event) {
		var x = event.layerX;
		var y = event.layerY;
		var over = null;
		
		this.cache.each(function(item, index, length) {
			if (x >= item.x && x < item.x + item.width) {
				if (item.inversed) {
					if (y > item.y && y <= item.y - (-item.height + item.relativeFreq - 20)) {
						over = item;
						return false;
					}
				} else {
					if (y <= item.y && y > item.y - (item.height + item.relativeFreq + 20)) {
						over = item;
						return false;
					}
				}
			} else {
				return true;
			}
		}, this);
		
		if (over != null) {
			document.body.style.cursor = 'pointer';
		} else {
			document.body.style.cursor = 'auto';
		}
		
		if (over != this.overToken) {
			this.overToken = over;
			this.drawGraph();
		} else {
			this.overToken = over;
		}
	},
	
	mouseDownHandler: function(event) {
		var x = event.layerX;
		var y = event.layerY;
	},
	
	mouseUpHandler: function(event) {
	},
	
	clickHandler: function(event) {
		var x = event.layerX;
		var y = event.layerY;
	},
	
	clearCanvas: function() {
		var width = this.canvas.width;
		var count = this.cache.getCount()-1;
		if (count > 0) {
			var last = this.cache.get(count);
			width = last.width + last.x;
		}
		this.canvas.width = width;
	},
    
	getFirst: function() {
		var start = this.getApiParamValue('start');
		var limit = this.getApiParamValue('limit');
		this.setApiParams({start: 0});
		this.getTokens();
	},
	
	getPrevious: function() {
		var start = this.getApiParamValue('start');
		var limit = this.getApiParamValue('limit');
		this.setApiParams({start: Math.max(0, start-limit)});
		this.getTokens();
	},
	
	getNext: function() {
		var max = this.findMaxTokens();
		var start = this.getApiParamValue('start');
		var limit = this.getApiParamValue('limit');
		var newStart = start + limit;
		if (newStart >= max) {
			newStart = max - limit;
		}
		this.setApiParams({start: newStart});
		this.getTokens();
	},
	
	getLast: function() {
		var max = this.findMaxTokens();
		var start = this.getApiParamValue('start');
		var limit = this.getApiParamValue('limit');
		this.setApiParams({start: max - limit});
		this.getTokens();
	},
	
	findMaxTokens: function() {
		var maxTokens = -1;
		this.selectedDocs.each(function(doc, index, length) {
			var tokens = doc.getTotalWordTokens();
			if (tokens > maxTokens) maxTokens = tokens;
		}, this);
		return maxTokens;
	},
	
    getTokens: function() {
    	this.update({tool: 'Tokens', params: this.getApiParams()});
    },
	
    showOptions: function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var comparisonCorpus = form.findField('comparisonCorpus');
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								
								if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery)
								}
								
								if (form.isDirty()) {
									this.setApiParams({
										stopList: stopList.getValue()
									});
									if (global) {
										this.getApplication().applyParamsGlobally({
											stopList: this.getApiParamValue('stopList')
										}, true);
									}
									else {
										this.getTokens();
									}
								}
								formPanel.findParentByType('window').destroy();
							},
							scope: this
						}
					}
				}]
			}]
		}, true);
    },
    
	api: {
		/**
		 * @property docId The document ID to restrict results to.
		 * @type String
		 * @default null
		 */
		docId: {'default': undefined}
		/**
		 * @property docIdType The document type(s) to restrict results to.
		 * @type String|Array
		 * @default null
		 */
		,docIndex: {'default': 0}
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		,start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 50
		 */
		,limit: {'default': 50}
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		,stopList: {
			'default': null,
			'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		,wordsOnly: {
			'default': true
		}
		,toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'CorpusTypeFrequenciesResultLoaded']
		,dispatchers: ['documentTypeSelected', 'documentTypesSelected']
	}
	
    ,i18n : {
        title : {en: 'Flowerbed'}
        ,type : {en: 'Visualization'}
        ,findTerm : {en: 'Find Term'}
        ,clearTerms : {en: 'Clear Terms'}
        ,segments : {en: 'Segments'}
        ,total : {en: 'Total'}
        ,first : {en: 'First'}
        ,next : {en: 'Next'}
        ,previous : {en: 'Previous'}
        ,last : {en: 'Last'}
        ,languages : {en: 'Languages'}
        ,translate : {en: 'Translate'}
        ,translateTip : {en: 'Translates the selected document into the specified language.'}
        ,corpusTooSmall : {en: 'The provided corpus is too small for this tool.'}
        ,help: {en: ''}
        ,adaptedFrom: {en: ''}
    }
});

Ext.reg('voyeurFlowerbed', Voyeur.Tool.Flowerbed);
/**
 * @class Voyeur.Tool.VoyeurHeader 
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.VoyeurHeader = Ext.extend(Ext.Panel, {
	
	singleTool : false, // is there a single tool being used?
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))
		Ext.apply(config, {
			id : 'logo',
			title: '',
			html : '<div id="logo-container"></div>',
			bodyStyle: {'text-align': 'center'},
            collapseMode : undefined,
			collapsible: true,
			animCollapse: false,
			titleCollapse: false,
			floatable: false,
			split: false,
			header: true,
			layout : 'fit',
			tools: [],
			cmargins: '0'
		});
		if (config.singleTool != null) {
			this.singleTool = config.singleTool;
		}

		Voyeur.Tool.VoyeurHeader.superclass.constructor.apply(this, arguments);
		
		this.addListener('beforecollapse', function(panel) {
			if (this.getApplication().iframe && this.singleTool) {
				panel.ownerCt.layout.north.getCollapsedEl().setHeight(16);
			}
		});
		this.addListener('collapse', function(panel) {
			var collapsed = panel.ownerCt.layout.north.collapsedEl;
			if (this.getApplication().iframe && this.singleTool) {
				collapsed.addClass('small-logo');
			}
			var collapsedEl = collapsed.first();
			if (collapsedEl.id == collapsed.last().id) {
				collapsed.update(''); // get rid of what's there
				
				// catch clicks on the header
				collapsed.on('click', function(ev) {
					ev.stopPropagation();
					Ext.Msg.confirm(this.localize('header'),this.localize('confirmRestart'), function(btn, text) {
					    if (btn == 'yes'){
					    	window.location.reload();
					    }
					}, this);
				}, this);
				
				var headerTipEl;
				if (this.singleTool) {
					headerTipEl = collapsed;
				} else {
					// add help button
					var help = Ext.DomHelper.append(collapsed, '<div class="x-tool x-tool-help" style="margin-top: 2px;"> </div>', true);
					Ext.QuickTips.register({
					    target: help,
					    text: this.localize('helpTip')
					});
					help.addListener('click', function (ev) {
						ev.stopPropagation();
						Ext.Msg.show({
							title: this.localize('helpTitle'),
							msg: this.localize('helpTip'),
							icon: Ext.Msg.INFO,
							width: 300
						})
					}, this);
					
					// add save button
					var save = Ext.DomHelper.append(collapsed, '<div class="x-tool x-tool-save" style="margin-top: 2px;"> </div>', true);
					Ext.QuickTips.register({
					    target: save,
					    text: this.localize('saveTip')
					});
					save.addListener('click', function (ev) {
						ev.stopPropagation();
						var url = this.getURL(false);
						var builderUrl = this.getURL(true);
						Ext.Msg.show({
							title: this.localize('saveTitle'),
							msg: new Ext.Template(this.localize('saveMsg')).apply([url, builderUrl]),
							width: 300
						})
					}, this);
					
					headerTipEl = Ext.DomHelper.append(collapsed, '<div style="margin-right: 35px; height: 20px;"> </div>', true);
				}
				
				Ext.QuickTips.register({
				    target: headerTipEl,
				    text: this.localize('headerTip')
				});
			}
		}, this);
	},
	
	getURL: function(builder) {
		var url = this.getApplication().getBaseUrl();
		if (!builder) url = url.replace(/\.\.\//g, ''); // get rid of parent directory strings (from tool skin)
		url += '?corpus='+this.getCorpus().getId();
		
		if (builder) {
			url += '&skin=builder'
		} else {
			var params = Ext.urlDecode(window.location.search.substring(1));
			if (params.skin) {
				url += '&skin='+params.skin;
				if (params.layout) url += '&layout='+encodeURI(params.layout);
			}
		}
		
		return url;
	},
	
	i18n: {
		title: ''
		,header: {en: 'Voyants Tools: Reveal Your Texts'}
		,headerTip: {en: 'Click here to start over'}
		,confirmRestart: {en: 'Are you sure you wish to start over?'}
		,helpTitle : {en : 'Help'}
		,helpTip : {en : '<p>Voyeur is the web-based tool for reading and analyzing your digital texts.</p><p>Visit <a href=\'http://hermeneuti.ca/voyeur/\'>http://hermeneuti.ca/voyeur/</a> for more information about Voyeur, to access tutorial materials, and to submit feedback and bug reports.</p>'}
		,saveTitle : {en : 'Save'}
		,saveTip : {en : 'Click here to generate a URL to access this corpus again.'}
		,saveMsg : {en : '<p>You can click or copy the following URL to open the current corpus again:</p><div><a href="{0}" target="_blank">{0}</a></div><p>Alternately, you can export the corpus to the layout builder and construct a custom layout with which to view the corpus:</p><div><a href="{1}" target="_blank">{1}</a></div><p>Please note that this corpus may be removed after a couple of days of inactivity or may be invalidated by an update to Voyeur.</p>'}
	}
});

Ext.reg('voyeurHeader', Voyeur.Tool.VoyeurHeader);

/**
 * @class Voyeur.Tool.ToolBrowserLarge A simple panel which lists available tools.
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.ToolBrowserLarge = Ext.extend(Ext.Panel, {
    
	toolsList: [
	    'Bubblelines',
        'Bubbles',
        //'Centroid',
        'Cirrus',
        'CorpusGrid',
        'CorpusSummary',
        'CorpusTypeFrequenciesGrid',
        'DocumentInputAdd',
        'DocumentTypeCollocateFrequenciesGrid',
        'DocumentTypeFrequenciesGrid',
        'DocumentTypeKwicsGrid',
        //'Equalizer',
        'Knots',
        'Lava',
        'Links',
        'Mandala',
        'NetVizApplet',
        'Reader',
        //'Ticker',
        //'TokensViz',
        'TypeFrequenciesChart',
        //'WordCloud',
        'WordCountFountain'
    ],
    
    initComponent : function() {
        Voyeur.Tool.ToolBrowserLarge.superclass.initComponent.call(this);
        
        var link = document.createElement('link');
		link.href = this.getToolDirectoryUrl()+'ToolBrowserLarge.css';
		link.rel = 'stylesheet';
		link.type = 'text/css';
		document.getElementsByTagName('head')[0].appendChild(link);
        
        this.addEvents({
        	toolsloaded: true,
        	tooldblclick: true
        });
    },
    
    getToolDirectoryUrlForXtype : function(xtype) {
    	return this.getApplication().getBaseUrl()+'resources/tools/'+xtype.replace(/^voyeur/,'')+'/';
    },
    
    constructor : function(config) {
        // inherit Voyeur Tool
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        Ext.apply(config, {
            layout: 'border',
            bodyStyle: 'background-color: #fff;',
        	items: [{
            	id: 'toolBrowser',
            	margins: '10 5 10 10',
            	region: 'center',
            	autoScroll: true,
            	border: false,
            	items: []
        	},{
        		margins: '10 10 10 0',
        		region: 'east',
        		width: 200,
        		border: false,
        		defaults: {width: 200, border: false},
        		layout: 'vbox',
        		items: [{
        			height: 20,
            		xtype: 'textfield',
            		emptyText: this.localize('searchText', 'tool'),
            		enableKeyEvents: true,
            		listeners: {
            			keyup: function(textfield, event) {
            				var q = textfield.getRawValue();
            				this.showMatches(q);
            			},
            			scope: this
            		}
            	},{
            		flex: 1,
        			html: '<div id="tools-menu" />'
        		}]
        	}]
        });
        
        // call superclass
        Voyeur.Tool.ToolBrowserLarge.superclass.constructor.apply(this, [config]);
        
        this.addListener('afterrender', function(src, params){
        	var scripts = '';
    		for (var i = 0; i < this.toolsList.length; i++) {
    			var tool = this.toolsList[i];
    			scripts += '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/tools/'+tool+'/'+tool+'.js"></script>';
    		}
            this.body.update(scripts, true, this.initTools.createDelegate(this));
        }, this);
    },

    initTools: function() {
    	try {
	        var i = 0;
	        var typeEntry = null;
	        var lang = Voyeur.application.getLocalization().getLang();
	        var defaultThumbSmall = this.getToolDirectoryUrl()+'thumb_small.png';
	        var defaultThumbLarge = this.getToolDirectoryUrl()+'thumb_large.png';
	        var processedTools = [];
	        for (var i = 0; i < this.toolsList.length; i++) {
	            var name = this.toolsList[i];
	            var toolClass = Voyeur.Tool[name];
                var xtype = toolClass.xtype;
                var proto = toolClass.prototype;
                var dir = this.getToolDirectoryUrlForXtype(xtype);
                if (proto.i18n && proto.api) {
                    var types = proto.api.toolType ? proto.api.toolType : ['Default'];
                    var icontype = '';
                    if (types.indexOf('Visualization') != -1) {
                    	icontype = 'viz';
                    } else if (types.indexOf('Table') != -1) {
                    	icontype = 'table';
                    } else if (types.indexOf('Corpus') != -1) {
                    	icontype = 'corpus';
                    } else if (types.indexOf('Document') != -1) {
                    	icontype = 'document';
                    }
                    var title = proto.i18n.title ? proto.i18n.title[lang] || this.localize('noTitle') : this.localize('noTitle');
                    var desc = proto.i18n.help ? proto.i18n.help[lang] || this.localize('noDescription') : this.localize('noDescription');
                    var listeners = proto.api ? proto.api.listeners || [] : [];
                    var dispatchers = proto.api ? proto.api.dispatchers || [] : [];
                    var thumblarge = defaultThumbLarge;
                    var thumbsmall = defaultThumbSmall;
                    if (proto.thumb) {
                    	if (proto.thumb.large) thumblarge = dir + proto.thumb.large;
                    	if (proto.thumb.small) thumbsmall = dir + proto.thumb.small;
                    }
                    for (var j = 0; j < types.length; j++) {
                    	var type = types[j];
                    	var info = {
                            type: type,
                            xtype: xtype,
                            title: title,
                            desc: desc,
                            listeners: listeners,
                            dispatchers: dispatchers,
                            icontype: icontype,
                            thumblarge: thumblarge,
                            thumbsmall: thumbsmall,
                            collapsed: false
                        };
                    	typeEntry = null;
                        for (var t in processedTools) {
                            if (processedTools[t].type == type) {
                                typeEntry = processedTools[t];
                                break;
                            }
                        }
                        if (typeEntry == null) {
                        	processedTools.push({
                            	id: 'type-'+processedTools.length,
                                type: type,
                                icontype: icontype,
                                tools: [info]
                            });
                        } else {
                            typeEntry.tools.push(info);
                        }
                    }
	            }
	        }
	        
	        var tb = Ext.getCmp('toolBrowser');
	        for (var i = 0; i < processedTools.length; i++) {
	        	var typeEntry = processedTools[i];
	        	var dv = this.buildDataView(typeEntry);
	        	var store = new Ext.data.JsonStore({
					fields: ['type', 'xtype', 'title', 'desc', 'listeners', 'dispatchers', 'icontype', 'thumblarge', 'thumbsmall', 'collapsed'],
					data: typeEntry.tools
		        });
	        	dv.store = store;
	        	tb.add(dv);
	        }
	        tb.doLayout();
	        
	        var tpl = new Ext.XTemplate(
	        	'<tpl for="."><li><a href="#{id}"><img src="'+Ext.BLANK_IMAGE_URL+'" class="{icontype}" />{type}</a></li></tpl>'
	        );
	        
	        tpl.overwrite('tools-menu', processedTools);
	
	        this.fireEvent('toolsloaded', this);
    	} catch (e) {
    		setTimeout(this.initTools.createDelegate(this), 250);
    	}
    },
    
    
    buildDataView: function(type) {
    	return {
			xtype: 'dataview',
        	singleSelect: true,
        	itemSelector: 'dd',
        	overClass: 'over',
        	//selectedClass: 'selected',
        	tpl: new Ext.XTemplate(
    	        '<div class="type-view">',
    		        '<a name="'+type.id+'"></a>',
    		        '<h2><div><img src="'+Ext.BLANK_IMAGE_URL+'" class="'+type.icontype+'" />'+type.type+'</div></h2>',
    		        '<dl>',
    		            '<tpl for=".">',
    		            	'<tpl if="values.collapsed == true">',
    		            		'<dd class="collapsed"><h4>{title}</h4></dd>',
    		            	'</tpl>',
    		            	'<tpl if="values.collapsed == false">',
    		                '<dd ext:qtip="'+this.localize('doubleClick')+'">',
    		                	'<img src="{thumblarge}"/>',
    		                    '<div><h4>{title}</h4><p>{desc}</p></div>',
    		                '</dd>',
    		                '</tpl>',
    		            '</tpl>',
    		        '<div style="clear:left"></div>',
    		        '</dl>',
    	        '</div>'
            ),
            listeners: {
            	click: function(dv, index, node, event) {
            		dv.getRecord(node).set('collapsed', false);
            	},
            	dblclick: function(dv, index, node, event) {
            		var record = dv.getRecord(node);
            		var name = record.get('xtype').replace('voyeur', '');
            		var corpus = this.getCorpus();
            		var url = this.getApplication().getBaseUrl()+'tool/'+name+'/';
            		if (!corpus.isEmpty()) {
            			url += '?corpus='+corpus.getId();
            		}
            		window.open(url);
            	},
            	containerclick: function(dv, event) {
            		var group = event.getTarget('h2', 3, true);
                    if (group) {
                        group.up('div').toggleClass('collapsed');
                    }
            	},
            	scope: this
            }
    	}
    },
    
    showMatches: function(query) {
    	var r = new RegExp(query, 'i');
    	var dvs = Ext.getCmp('toolBrowser').findByType('dataview');
    	for (var i = 0; i < dvs.length; i++) {
    		var dv = dvs[i];
    		var store = dv.getStore();
    		var match = false;
    		store.each(function(record) {
    			if (query == '' || r.test(record.get('title')) || r.test(record.get('desc'))) {
    				record.set('collapsed', false);
    				match = true;
    			} else {
    				record.set('collapsed', true);
    			}
    		}, this);
    		store.sort('collapsed', 'ASC');
    		var viewDiv = dv.getEl().down('div[class=type-view]');
    		if (!match) {
    			viewDiv.addClass('collapsed');
    		} else {
    			viewDiv.removeClass('collapsed');
    		}
    	}
    },
    
//    showRelatedTools : function(type, xtype) {
//    	var data = this.getToolData(type, xtype);
//    	var senders = []
//    	var receivers = [];
//    	if (data.listeners.length > 0) {
//    		senders = this.getToolsMatchingEvents(data.listeners, 'dispatchers');
//    	}
//    	if (data.dispatchers.length > 0) {
//    		receivers = this.getToolsMatchingEvents(data.dispatchers, 'listeners');
//    	}
//    	var tabs = Ext.getCmp('tool-tabs');
//    	var tab = tabs.getComponent('related');
//    	tab.items.each(function(item, index, length) {
//    		if ((item.initialConfig.data && item.initialConfig.data.xtype != xtype) || item.initialConfig.data == null) {
//    			item.ownerCt.remove(item);
//    		} else {
//    			item.hide(); // need to hide this so panelproxy still has a dom to work with
//    		}
//    	}, this);
//    	if (receivers.length > 0) {
//    		tab.add({
//	    		layout: 'fit',
//	            unstyled: true,
//	            style: 'float: left;',
//	            border: false,
//	            html: '<div class="related">'+this.localize('receiveEvents')+' '+data.title+'</div>'
//	    		
//	    	});
//    		tab.add(this.createToolPanels(receivers));
//    	}
//    	if (senders.length > 0) {
//    		tab.add({
//	    		layout: 'fit',
//	            unstyled: true,
//	            style: 'float: left;',
//	            border: false,
//	            html: '<div class="related>'+this.localize('sendEvents')+' '+data.title+'</div>'
//	    		
//	    	});
//    		tab.add(this.createToolPanels(senders));
//    	}
//    	tabs.unhideTabStripItem(tab);
//    	tab.doLayout();
//    	tabs.activate(tab);
//    	
//    },
//    
//    getToolsMatchingEvents : function(events, category) {
//    	var matchingTools = [];
//		var typeEntry = this.toolsList[0]; // All tools
//		for (var j = 0; j < typeEntry.tools.length; j++) {
//			var tool = typeEntry.tools[j];
//			if (matchingTools.indexOf(tool) == -1) {
//    			for (var k = 0; k < events.length; k++) {
//    				var e = events[k];
//    				if (tool[category].indexOf(e) != -1) {
//    					matchingTools.push(tool);
//    					break;
//    				}
//    			}
//			}
//		}
//    	return matchingTools;
//    },
//    
//    getToolData : function(type, xtype) {
//    	var data = null;
//        for (var i = 0; i < this.toolsList.length; i++) {
//            if (data) break;
//            var typeEntry = this.toolsList[i];
//            if (typeEntry.type == type || type == null) {
//                for (var j = 0; j < typeEntry.tools.length; j++) {
//                    var tool = typeEntry.tools[j];
//                    if (tool.xtype == xtype) {
//                        data = tool;
//                        break;
//                    }
//                }
//            }
//        }
//        return data;
//    },
    
    api: {
    	toolType: ['Meta']
    }

    // localization variables
    ,i18n : {
        title : {en: "Tool Browser"},
        help: {en: "This tool displays all other tools."},
        noTitle: {en: 'No title'},
        noDescription: {en: 'No description'},
        relatedTools: {en: 'Related Tools'},
        receiveEvents: {en: 'Receive Events From'},
        sendEvents: {en: 'Send Events To'},
        doubleClick: {en: 'Double-click to preview'}
    }
});

Ext.reg('voyeurToolBrowserLarge', Voyeur.Tool.ToolBrowserLarge);

/**
 * @class Voyeur.Tool.WordCloud 
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.WordCloud = Ext.extend(Ext.Panel, {
	tool: 'CorpusTypeFrequencies'
	,targetTool: 'WordCloud'
	,targetMode: 'DocumentTypeCollocateFrequencies'
	,constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this))
		
		Ext.applyIf(config, {
			html: '<div>&nbsp;</div>'
			,bodyStyle: {'background-color': 'white'}
			,frame: true
			,border: false
			,autoScroll: true
			,bbar: [{
				xtype: 'tbtext'
				,text: this.localize('concept')
			}]
		});

		Voyeur.Tool.WordCloud.superclass.constructor.apply(this, arguments);
		
		this.addListener('render', function(panel) {
			panel.body.addListener('click', function(e) {
				var target = e.getTarget(null,null,true);
				if (target && target.dom.tagName=='A' && target.hasClass("word")) {
					if (this.targetTool=='WordCloud') {
						params = {
								corpus: this.getCorpus().get('id')
								,tool: 'DocumentTypeCollocateFrequencies'
								,docIdType: target.val
								,stopList : this.getApiParamValue('stopList')
						}
//						if (this.tool=='CorpusTypeFrequencies') {params.type = target.val}
						if (this.tool=='DocumentTypeFrequencies' || this.tool=='DocumentTypeCollocateFrequencies') {params.docIdType = target.getAttribute('val')+':'+target.dom.innerHTML}
						var url = this.getToolDirectoryUrl()+'?'+Ext.urlEncode(params)
						var win = window.open(url);
						if (win) {win.focus();}
						else {
							Ext.Msg.alert(this.localize('app.error'), Ext.DomHelper.createTemplate(this.localize('app.noOpenWin')).applyTemplate([url, tool]));
						}
					}
				}
			}, this);
		}, this);
		
		/**
		 * @event CorpusTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusTypeFrequenciesResultLoaded', function(src, data) {
			var store = new Ext.data.JsonStore({
				root : 'corpusTypes.types',
				totalProperty : 'corpusTypes["@totalTypes"]',
				remoteSort : false,
				fields : Voyeur.data.CorpusTypes.fields,
				data: data
			});
			var words = [];
			var max = store.getAt(0).get('rawFreq');
			var min = store.getAt(store.getCount()-1).get('rawFreq');
			var adjustedMax = max-min;
			var freq, rel;
			store.each(function (item) {
				freq = item.get('rawFreq');
				rel = parseInt((freq-min)*10/adjustedMax);
				words.push({type: item.get('type'), val: item.get('type'), rel: rel})
			}, this);
			words = words.sort(function(a,b) {
				return a.type.toLowerCase() > b.type.toLowerCase() ? 1 : -1;
			})
			var wordsS = [];
			for (var i=0;i<words.length;i++) {wordsS.push("<a href='#' class='word _"+words[i].rel+"'>"+words[i].type+"</a>")}
			this.body.update("<h1>"+this.localize('corpusTypes')+"</h1>"+wordsS.join(" "))
		}, this);
		
		/**
		 * @event DocumentTypeFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('DocumentTypeFrequenciesResultLoaded', function(src, data) {
			var store = new Ext.data.JsonStore({
				root : 'documentTypes.types',
				remoteSort : false,
				fields : Voyeur.data.DocumentTypes.fields,
				data: data
			});
			store.sort('rawFreq', 'DESC');
			var maxSize = store.getAt(0).get('rawFreq');
			var minSize = store.getAt(store.getCount()-1).get('rawFreq');
			var adjustedMaxSize = maxSize-minSize;
			store.sort('rawZscoreCorpusDelta', 'DESC');
			var maxOpacity = store.getAt(0).get('rawZscoreCorpusDelta');
			var minOpacity = store.getAt(store.getCount()-1).get('rawZscoreCorpusDelta');
			var adjustedMaxOpacity = maxOpacity-minOpacity;
			store.sort('type', 'ASC');
			var content = '';
			var tpl = new Ext.Template(this.localize('wordLink'))
			store.each(function (item, index) {
				if (index==0) {content+='<h1>'+new Ext.Template(this.localize('documentTypes')).apply([this.getCorpus().getDocument(item.get('docId')).getShortTitle()])+'</h1>'}
				content+=tpl.apply([item.get('type'),'',parseInt((item.get('rawFreq')-minSize)*10/adjustedMaxSize),parseInt((item.get('rawZscoreCorpusDelta')-minOpacity)*10/adjustedMaxOpacity),item.get('docId')])
			}, this)
			this.body.update('<div style="margin: 1em;">'+content+'</div>');
		}, this);
		
		/**
		 * @event DocumentTypeCollocateFrequenciesResultLoaded
		 * @type listener
		 */
		this.addListener('DocumentTypeCollocateFrequenciesResultLoaded', function(src, data) {
			var store = new Ext.data.JsonStore({
				root: 'documentTypeCollocateFrequencies.types',
				totalProperty: 'documentTypeCollocateFrequencies["@totalTypes"]',
				remoteSort : false,
				fields : Voyeur.data.DocumentTypeCollocates.fields,
				data: data
			});
			store.sort('rawCollocateFreq', 'DESC');
			var maxSize = store.getAt(0).get('rawCollocateFreq');
			var minSize = store.getAt(store.getCount()-1).get('rawCollocateFreq');
			var adjustedMaxSize = maxSize-minSize;
			store.sort('relativeRatio', 'DESC');
			var maxOpacity = store.getAt(0).get('relativeRatio');
			var minOpacity = store.getAt(store.getCount()-1).get('relativeRatio');
			var adjustedMaxOpacity = maxOpacity-minOpacity;
			store.sort('type', 'ASC');
			var content = '';
			var tpl = new Ext.Template(this.localize('wordLink'))
			store.each(function (item, index) {
				if (index==0) {content+='<h1>'+new Ext.Template(this.localize('documentTypeCollocates')).apply([item.get('keyword'),this.getCorpus().getDocument(item.get('docId')).getShortTitle()])+'</h1>'}
				content+=tpl.apply([item.get('type'),'',parseInt((item.get('rawCollocateFreq')-minSize)*10/adjustedMaxSize),parseInt((item.get('relativeRatio')-minOpacity)*10/adjustedMaxOpacity),item.get('docId')])
			}, this)
			this.body.update('<div style="margin: 1em;">'+content+'</div>');
		}, this);
		
		this.addListener('WordCloudBootstrap', function(src, params) {
			Ext.applyIf(params, {
				limit: 500
			})
			this.update({
					params : params,
					tool : ['CorpusSummary',this.tool]
				});
			
		}, this);

	}

	,api:  {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		stopList: {
			'default': null
			,'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		},
		/**
		 * @property extendedSortZscoreMinimum The minimum extended sort Z score that each result should have.
		 * @type Number
		 * @default 1
		 */
		extendedSortZscoreMinimum: {
			'default': 1
		},
		toolType: ['Visualization']
		,listeners: ['CorpusTypeFrequenciesResultLoaded', 'DocumentTypeFrequenciesResultLoaded', 'DocumentTypeCollocateFrequenciesResultLoaded', 'WordCloudBootstrap']
	}

	,i18n : {
		title : {en: "Word Cloud"}
	    ,type : {en: "Visualization"}
		,help: {en: "This tool presents words as a word cloud."}
		,wordLink: {en: '<a href="#" class="word size_{2} opacity_{3}" ext:qtip="{1}" val="{4}">{0}</a> '}
		,corpusTypes: {en: "Words in the Entire Corpus"}
		,documentTypes: {en: 'Words in the Document "{0}"'}
		,documentTypeCollocates: {en: 'Collocates of "<span class="keyword">{0}</span>" in the Document "{1}"'}
		,concept: {en: 'Concept for Flexible Word Cloud: Dave Beavan'}
	}
});

Ext.reg('voyeurWordCloud', Voyeur.Tool.WordCloud);

/**
 * @class Voyeur.Tool.Ticker
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @namespace Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.Ticker = Ext.extend(Ext.Panel, {
	index : 0,
	wordEntries : new Ext.util.MixedCollection(),
	updateData : Ext.emptyFn,
	lastUpdate : 0,
	delay : 3000,
	sortBy: 'cumulative',
	delta: true,
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config,this))

		this.slider = new Ext.Slider({
					width : 100,
					value : 0,
					increment : 1,
					minValue : 0,
					maxValue : 100,
					listeners : {
						change : {
							fn : function(slider, val) {
								this.updateDocs();
								this.tryUpdate();
								if (this.slider.value == this.slider.maxValue) {
									this.stop();
								}
							},
							scope : this
						}
					}

				})
		this.progress = new Ext.ProgressBar({
					width : 50,
					listeners : {
						update : {
							fn : function(pb, val) {
								if (val == 1) {
									if (this.isPlaying()) {
									this.slider.setValue(this.slider.getValue()
											+ 1);
									}
								} else {
									pb.updateText(parseInt(val * 50) + '%')
								}
							},
							scope : this
						}
					}
				});

		// this.delay = new Ext.form.Spinner({
		// xtype : 'spinner',
		// value: 3,
		// width : 40,
		// minValue : 1,
		// maxValue : 60,
		// allowDecimals : false,
		// incrementValue : 1,
		// accelerate : true
		// })
		Ext.applyIf(config, {
			layout : 'fit',
			bbar : [
					{
						xtype : 'button',
						iconCls : 'icon-play',
						enableToggle : true,
						toggleHandler : function(btn, state) {
							// this.tryUpdate();
							btn
									.setIconClass(state
											? 'icon-pause'
											: 'icon-play');
							if (state) {
								if (this.slider.getValue() == this.slider.maxValue) {
									this.slider.setValue(0);
								} else {
									this.tryUpdate();
								}
							}
							else {this.progress.reset();}
						},
						scope : this,
						pressed : false
					}, {
						xtype : 'tbtext',
						text : 1
					}, this.slider, {
						xtype : 'tbtext',
						text : 10
					}, ' ', '-', ' ',
					this.localize('delay') + this.localize('colon', 'app'), {
						xtype : 'spinner',
						value : 6,
						width : 40,
						minValue : 1,
						maxValue : 60,
						allowDecimals : false,
						incrementValue : 1,
						accelerate : true
					}, this.progress]
			,html: '<div style="position: absolute; top: 5px; left: 5px;"><table cellpadding="0" cellspacing="0"><tbody><tr><td style="width: 90px; text-align: center;" ext:qtip="'+this.localize('rankTip')+'">'+this.localize('rank')+'</td><td ext:qtip="'+this.localize('wordCountTip')+'">'+this.localize('wordCount')+'</td></tr></tbody></table></div><div style="margin-left: 260px;"><h3>Documents</h3><ul>&nbsp;</ul></div>'
			,autoScroll: true

		});

		Voyeur.Tool.Ticker.superclass.constructor.apply(this, arguments);

		this.addListener('resize', function() {
			this.updateWords();
		}, this);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
					this.setMaxIndex(data.corpus.documents.length - 1);
					this.slider.maxValue = data.corpus.documents.length - 1;
					if (this.slider.getValue() != 0) {
						this.slider.setValue(0);
					}
					this.corpusData(0);
					var titles = this.getCorpus().getShortTitles();
					var titleItems = "";
					for (var i=0;i<titles.length;i++) {
						titleItems+='<li class="docs-item"><a href"#" onclick="return false;">'+titles[i]+'</a></li>';
					}
					var list = this.body.last().last();
					list.update(titleItems);
					list.select("li").each(function(el, scp, index) {
						el.addListener('click', function() {
							this.slider.setValue(index);
						}, this);
					}, this)
					this.updateDocs();
				}, this);
		this.addListener('TickerBootstrap', function(src, params) {
					this.update({
								tool : 'CorpusSummary',
								params : params
							});
				}, this)
		

	}

	,updateDocs : function() {
		var val = this.slider.getValue();
		var docs = this.body.last().last().select("li");
		if (this.slider.previousVal!=undefined) {
			docs.item(this.slider.previousVal).removeClass('keyword');
		}
		var item = docs.item(val);
		item.addClass('keyword');
		item.scrollIntoView(this.body);
		this.slider.previousVal = val;
	}
	,
	tryUpdate : function() {
		if (this.wordEntries.getCount() > 0) {
			this.updateWords();
			if (this.isPlaying()) {
				var spinner = this.getBottomToolbar().findByType('spinner')[0];
				var duration = spinner.getValue() * 1000;
				this.progress.reset();
				this.progress.wait({
							duration : duration,
							interval : 500,
							animate : true,
							increment : duration / 500,
							scope : this
						});
			}
		} else {
			this.tryUpdate.defer(250, this);
		}
	},
	play : function() {
		this.getBottomToolbar().items.first().toggle(true);
	}

	,
	stop : function() {
		this.getBottomToolbar().items.first().toggle(false);
	}

	,
	isPlaying : function() {
		return this.getBottomToolbar().items.first().pressed
	}

	,
	getIndex : function() {
		return this.slider.getValue();
	},
	setMaxIndex : function(index) {
		this.slider.nextSibling().setText(index + 1);
		this.slider.maxValue = index;
	}

	,
	getMaxIndex : function() {
		return this.slider.maxValue;
	}

	,
	corpusData : function(ind) {
		var corpus = this.getCorpus();
		Ext.Ajax.request({
					url : this.getTromboneUrl(),
					params : {
						tool : 'DocumentTypeFrequencies',
						corpus : corpus.getId(),
						docId : corpus.getDocument(ind).get('id'),
						start : '0',
						limit : 200,
						stopList : 'stop.en.taporware.txt'
					},
					success : function(response, options) {
						var obj = Ext.decode(response.responseText);
						var types = obj.documentTypes.types;
						var newwords = [];
						for (var i = 0; i < types.length; i++) {
							newwords.push({
										word : types[i]['@type'],
										value : types[i]['@rawFreq']
									})
						}
						this.handleNewWords(newwords, ind);
						if (ind == 0) {
							this.play();
						}
						if (ind + 1 < corpus.documents.length) {
							this.corpusData(ind + 1);
						}
					},
					failure: this.handleAjaxError,
					scope : this
				})
	}

	,
	handleNewWords : function(newwords, ind) {
		var wordEntry;
		var word;
		var value;

		// update data
		for (var i = 0; i < newwords.length; i++) {
			word = newwords[i].word;
			value = newwords[i].value;
			wordEntry = this.wordEntries.get(word);
			if (!wordEntry) {
				wordEntry = {
					word : word,
					values : [],
					totals : [],
					rank : [],
					total : 0,
					window : 0
				}
				this.wordEntries.add(word, wordEntry);
			}
			wordEntry.values[ind] = value;
			wordEntry.total += Number(value);
			wordEntry.totals[ind] = wordEntry.total;
		}
	}

	,
	updateWords : function() {
		// sort entries
		var ind = this.getIndex();
		
		if (this.sortBy=='current') {
			this.wordEntries.sort(null, function(a, b) {
				if (a.values[ind] == b.values[ind]) {
					return a.word > b.word ? 1 : -1;
				} else {
					return a.values[ind] > b.values[ind] || !b.values[ind] ? -1 : 1;
				}
			})			
		}
		else if (this.sortBy=='cumulative') {
			this.wordEntries.sort(null, function(a, b) {
				if (a.totals[ind] == b.totals[ind]) {
					return a.word > b.word ? 1 : -1;
				} else {
					return a.totals[ind] > b.totals[ind] || !b.totals[ind] ? -1 : 1;
				}
			})
		}
		
		// set the delta
		this.wordEntries.each(function(item, index, length) {
			item.delta = item.rank[ind-1] == undefined ? (length-index) : item.rank[ind-1] - index;
			item.rank[ind] = index;
		});
		
		// re-order by delta if desired
		if (this.delta) {
			this.wordEntries.sort(null, function(a, b) {
				if (Math.abs(a.delta)==Math.abs(b.delta)) {return a.rank[ind] > b.rank[ind];}
				return Math.abs(a.delta) < Math.abs(b.delta);
			})
		}
		
		// determine position
		var box = this.body.getBox();
		var x = box.x + 10;
		var y = box.y + 25;
		var lastHeight = box.y + box.height;
		var lastItem = parseInt((box.height -25) / 18);

		// determine the max change
		var item;
		var maxDelta = 0;
		for (var i = 0; i < lastItem; i++) {
			item = this.wordEntries.get(i);
			if (item && Math.abs(item.delta) > maxDelta) {
				maxDelta = Math.abs(item.delta)
			}
		}
		
		this.wordEntries.each(function(item, index, length) {
					var el = item.el;
					if (index < lastItem) {
						if (!el) {
							el = new Ext.Layer({})
							el.update(item['word']);
							el.setVisible(true);
							item.el = el;
						}
						el.update(
								'<table cellpadding="0" cellspacing="0" class="'
										+ (item.delta < 0
												? 'negative'
												: (item.delta > 0
														? 'positive'
														: ''))
										+ '"><tr><td style="width: 18px;"><tt>'
										+ (item.delta == 0
												? '&nbsp;'
												: (item.delta > 0 ? '⬆' : '⬇'))
										+ '</tt><td><td style="width: 90px;">'
										+ (1 + item.rank[ind])
										+ (item.delta == 0 || ind == 0 ? '' : (' ('+(item.delta >0 ? '+' : '') + item.delta+ ')'))
										+ '</td>'+'<td>'
										+ '<span class="keyword">'+item.word
										+ ': '
										+ Ext.util.Format.number(item.values[ind] == undefined
												? 0
												: item.values[ind], '0,000')
										+ '</span> <span style="font-size: smaller;">(' + Ext.util.Format.number(item.totals[ind] == undefined
												? 0
												: item.totals[ind], '0,000') + ' / ' + Ext.util.Format.number(item.total, '0,000')
										+ ')</span></td></tr></table>', false)
						el.moveTo(x, y, {
												duration : 1,
												easing : 'easeOut'
											}).setOpacity((Math.abs(item.delta) * .8 / maxDelta)
								+ .2);
						y += 18;
					} else if (el) {
						el.moveTo(x, lastHeight, {
									callback : function(el) {
										el.destroy();
										delete item.el
									}
								}).fadeOut({remove: true})
					}
				})
	}
	
	,
	showOptions : function() {
		this.stop();
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [{
					xtype : 'radiogroup',
					id : 'sort',
					fieldLabel : '<span ext:qtip="'
							+ this.localize('sortByTip') + '">'
							+ this.localize('sortBy') + '</span>',
					columns: 1,
					items: [
	                    {boxLabel: this.localize('current'), name: 'sortBy', inputValue: 'current', checked: this.sortBy=='current'},
	                    {boxLabel: this.localize('cumulative'), name: 'sortBy', inputValue: 'cumulative', checked: this.sortBy=='cumulative'},
	                    {boxLabel: this.localize('delta'), name: 'delta', inputValue: 'on', checked: this.delta=='on', xtype: 'checkbox', id: 'delta'}
					]
				}],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								
								if (stopList.getValue() && !stopList.getRawValue()) stopList.setValue('');
								else if (stopList.lastQuery && stopList.lastQuery!=stopList.getValue()) {
									stopList.getStore().loadData({stopLists: {lists: [{id: stopList.lastQuery, label: stopList.lastQuery, description: ''}]}}, true);
									stopList.setValue(stopList.lastQuery);
								}

								this.stopList=stopList.getValue();
								if (global) {
									this.getApplication().applyParamsGlobally({
										stopList: this.getApiParamValue('stopList')
									}, true);
								}
								else {
									var values = form.getValues();
									this.sortBy = values.sortBy;
									this.delta = values.delta;
									this.play();
								}
								formPanel.findParentByType('window').destroy();
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								return
								var form = btn.findParentByType('form').getForm();
								form.findField('comparisonCorpus').setRawValue(this.defaultComparisonCorpus);
								form.findField('extendedSortZscoreMinimum').setValue(this.defaultExtendedSortZscoreMinimum);
								form.findField('stopList').setValue(this.stopList);
							},
							scope : this
						}
					}
	
				}]
			}]
		}, true);

	}
	
	,api: {
		toolType: ['Visualization']
		,listeners: ['CorpusSummaryResultLoaded', 'TickerBootstrap']
	}
	,i18n : {
		title : {
			en : 'Take Stock'
		},
		delay : {
			en : 'Delay'
		}
		,rank : {
			en : 'Rank'
		}
		,rankTip : {
			en: 'This shows the current rank of the word as well as its previous rank in parentheses. Entries that are bolder have shifted the most.'
		}
		,wordCount : {
			en : 'Word &amp; Count'
		}
		,wordCountTip : {
			en: 'This shows the word and its count as well as the cumulative total (occurrences until that point) and the total occurrences.'
		}
		,sortBy : {
			en: 'Sort By'
		}
		,sortByTip : {
			en: 'This allows you to modify the ordering of words.'
		}
		,current : {
			en: 'word frequency in current document'
		}
		,cumulative : {
			en: 'cumulative word frequency'
		}
		,delta : {
			en: 're-sort by change in above value'
		}
	}
});


Ext.reg('voyeurTicker', Voyeur.Tool.Ticker);

/**
 * @class Ext.ux.SliderTip
 * @extends Ext.Tip Simple plugin for using an Ext.Tip with a slider to show the
 *          slider value
 * @private
 */
Ext.ux.SliderTip = Ext.extend(Ext.Tip, {
			minWidth : 10,
			offsets : [0, -10],
			init : function(slider) {
				slider.on('dragstart', this.onSlide, this);
				slider.on('drag', this.onSlide, this);
				slider.on('dragend', this.hide, this);
				slider.on('destroy', this.destroy, this);
			},

			onSlide : function(slider) {
				this.show();
				this.body.update(this.getText(slider));
				this.doAutoWidth();
				this.el.alignTo(slider.thumb, 'b-t?', this.offsets);
			},

			getText : function(slider) {
				return String(slider.getValue());
			}
		});

Ext.apply(Ext.DomQuery.pseudos, {
	nodeValueCaseInsensitive: function(c, v){
        var r = [], ri = -1;
        v = new RegExp('^'+v+'$', 'i');
        for(var i = 0, ci; ci = c[i]; i++){
            if(ci.firstChild && ci.firstChild.nodeValue.match(v)){
                r[++ri] = ci;
            }
        }
        return r;
    }
});

/**
 * @class Voyeur.Tool.Reader A panel that provides reading and searching capabilities.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.Reader = Ext.extend(Ext.Panel, {
	MINIMUM_LIMIT: 1000,
	documents: new Ext.util.MixedCollection(),
	loading: false,
	constructor : function(config) {

		// register localization variables
		Ext.apply(this, new Voyeur.Tool(config, this));

		// set configuration options for superclass initialization, but allow overriding from skin
		Ext.applyIf(config, {
			layout: 'fit',
			html: '<div class="token_images" style="height: 100%; width: 30px;"> </div><div id="prospect_reader" style="position: absolute; left: 40px; right: 0px; top: 0px; height: 100%; overflow: auto; "> </div>'
		});
		
		this.search = new Ext.ux.TypeSearch({
			parentTool: this,
			width: 100,
			listeners: {
				typeSelected: function(combo, type, record) {
    				this.showProspectHits(type);
        		},
        		scope: this
        	}
		});
		
		Ext.applyIf(config, {
			bbar: [
	     		new Ext.Button({
	                tooltip: this.localize('startText'),
	                overflowText: this.localize('startText'),
	                iconCls: 'x-tbar-page-first',
	                handler: this.moveFirst,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('previousText'),
	                overflowText: this.localize('previousText'),
	                iconCls: 'x-tbar-page-prev',
	                handler: this.movePrevious,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('nextText'),
	                overflowText: this.localize('nextText'),
	                iconCls: 'x-tbar-page-next',
	                handler: this.moveNext,
	                scope: this
	            }), new Ext.Button({
	                tooltip: this.localize('endText'),
	                overflowText: this.localize('this.endText'),
	                iconCls: 'x-tbar-page-last',
	                handler: this.moveLast,
	                scope: this
	            }), ' ', '-', ' ',
	            this.search
	        ]
		});
		
		Voyeur.Tool.Reader.superclass.constructor.apply(this, arguments);
		
		this.addListener('afterrender', function(panel) {
			if (this.getCorpus().getSize()>0) {
				this.buildProspect();
				this.fetchText();
			}
			panel.body.addListener('click', function(e) {
				var target = e.getTarget(null,null,true);
				if (target && target.dom.tagName=='IMG' && target.dom.id.indexOf("prospect")==0) {
					var parts = target.dom.id.split(".");
					var docIndex = parseInt(parts[1]);
					var docLine = parseInt(parts[2]);
					var doc = this.documents.get(docIndex);
					var totalWords = doc.document.getTotalTokens();
					var start = parseInt(docLine * totalWords / doc.lines);
//					console.log('fetching', docIndex, docLine);
					this.setApiParams({docIndex: docIndex, start: start});
					Ext.get('prospect_reader').update('');
					this.fetchText();
				}
				else if (target && target.dom.tagName=='SPAN' && target.hasClass('word')) {
					var token = target.dom.innerHTML;
					var segment = this.getSegmentObject(target.parent());
					/**
					 * @event documentTypeSelected
					 * @param {Voyeur.Tool.Reader} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					this.getApplication().dispatchEvent('documentTypeSelected', this, {
						docIdType: this.getCorpus().getDocument(segment.docIndex).getId()+':'+token.toLowerCase()
					});
				}
			}, this);
			Ext.get('prospect_reader').addListener('scroll', function(ev) {
				var el = ev.target;
				if (this.loading) {return}
				var height = el.offsetHeight;
				var scrollHeight = el.scrollHeight;
				var scrollTop = el.scrollTop;
				el = Ext.get(el);
				if (scrollTop+height > scrollHeight-100 || scrollTop < 1) {
					var start = parseInt(this.getApiParamValue('start'));
					var limit = parseInt(this.getApiParamValue('limit'));
					if (scrollTop < 1) {
						var segmentObject = this.getSegmentObject(el.first());
						if (segmentObject) {
							var doc = segmentObject.docIndex;
							var start = segmentObject.start;
							if (doc==0 && start==0) {return;} // already at start
							if (start==0) { // move to end of previous document
								doc -= 1;
								this.setApiParams({docIndex: doc});
								start = this.getCorpus().getDocument(doc).getTotalTokens()-limit;
								if (start<0) {start=0}
							}
							else {
								if (start-limit>-1) {start -= limit;}
								else {
									limit = parseInt(start);
									start = 0;
								}
							}
							this.setApiParams({start: start});
							this.fetchText(Ext.DomHelper.insertFirst(el, {tag: 'span'}, true), {limit: limit});
						}
					} else {
						var currentSegment = el.last();
						var segmentObject = this.getSegmentObject(currentSegment);
						if (segmentObject && segmentObject.docIndex != null) {
							var doc = segmentObject.docIndex;
							var start = segmentObject.start;
							var lastLimit = segmentObject.lastLimit;
							var numberOfDocs = this.getCorpus().getSize();
							var totalWords = this.getCorpus().getDocument(doc).getTotalTokens();
							if (doc+1==numberOfDocs && start+lastLimit>=totalWords) {return;} // already at end
							if (start+lastLimit>=totalWords) { // move to end of previous document
								doc += 1;
								this.setApiParams({docIndex: doc});
								start = 0;
							}
							else { // move ahead
								start+=limit;
							}
							this.setApiParams({start: start});
							this.fetchText(null, {limit: limit});
						}
					}
				} else {
					var currentSegmentObject = this.getCurrentSegmentObject();
					this.highlightProspect(currentSegmentObject.docIndex, currentSegmentObject.start);
				}
			}, this);
		}, this);

		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, params) {
			if (this.rendered) {
				this.buildProspect();
				this.fetchText();
			}
		}, this);

		/**
		 * @event TokensResultLoaded
		 * @type listener
		 */
		this.addListener('TokensResultLoaded', function(src, data) {
			var content = "";
			var docs = data.tokens.documents;
			var toks, category;
			for (var i=0;i<docs.length;i++) {
				toks = docs[i].tokens;
				for (var j=0;j<toks.length;j++) {
					category = toks[j]['@category'];
					if (category.indexOf("TAG")>-1) {
						if (toks[j]['@newline']) {
							content+='<br />';
						}
					}
					else {content+=toks[j]['@token'];}
				}
			}
			var el = this.body.last();
			el.update(content);
		}, this);
		
		/**
		 * @event tokenSelected
		 * @type listener
		 */
		this.addListener('tokenSelected', function(src, data) {
			var tokenId = data.tokenId;
			var docInfo = data.docIdType.split(':');
			var docId = docInfo[0];
			var type = docInfo[1].toLowerCase();
			var docIndex = this.getCorpus().getDocument(docId).getIndex();
			this.setApiParams({docIndex: docIndex, start: tokenId});
			Ext.get('prospect_reader').update('');
			this.fetchText();
//			this.search.setValue(type);
//			this.showProspectHits(type);
		});
		
		/**
		 * @event corpusTypeSelected
		 * @type listener
		 */
		this.addListener('corpusTypeSelected', function(src, data) {
			var type = data.type.toLowerCase();
			this.search.setValue(type);
			this.showProspectHits(type);
		});
		
		/**
		 * @event documentTypeSelected
		 * @type listener
		 */
		this.addListener('documentTypeSelected', function(src, data) {
			var type = data.docIdType.split(':')[1].toLowerCase();
			this.search.setValue(type);
			this.showProspectHits(type);
		}, this);
	}

	,buildProspect: function() {
		var el = this.body.first();
		var availableLines = parseInt(el.getHeight() / 5); // 5px is line height
		availableLines--; // make sure there's no scrollbar for prospect
		
		var corpus = this.getCorpus();
		var totalTokens = 0;
		corpus.getDocuments().each(function(document) {
			totalTokens += document.getTotalTokens();
		});
		var tokensPerLine = Math.floor(totalTokens / availableLines);
		if (tokensPerLine < this.MINIMUM_LIMIT) {tokensPerLine = this.MINIMUM_LIMIT;}
		this.setApiParams({limit: tokensPerLine});
		
		var docTotalTokens, linesPerDocument;
		var imagesSnippet = "";
		var label;
		this.documents = new Ext.util.MixedCollection();
		
		corpus.getDocuments().each(function(document) {
			label = document.getShortLabel();
			docIndex = document.getIndex();
			imagesSnippet += "<div>";
			docTotalTokens = document.getTotalTokens();
			var percentageOfWhole = docTotalTokens / totalTokens;
			//linesPerDocument = Math.floor(docTotalTokens / tokensPerLine);
			linesPerDocument = Math.floor(availableLines * percentageOfWhole);
			//console.log('linesPerDocument',linesPerDocument,'percentageOfWhole',percentageOfWhole);
			if (linesPerDocument < 1) {linesPerDocument = 1;}
			var colourClass = 'colour_'+(docIndex % 24);
			for (var i = 0; i < linesPerDocument; i++) {
				imagesSnippet += "<img src='"+Ext.BLANK_IMAGE_URL+"' class='prospectLine "+colourClass+"' ext:qtip='"+label+"' id='prospect."+docIndex+'.'+i+"' />";
				//imagesSnippet += "<img src='"+img_src+"' class='prospectLine spacer' style='cursor: default;'/>"; // FIXME: do proper CSS override of cursor
			}
			this.documents.add(document.getIndex(), {
				document: document, lines: linesPerDocument
			});
			imagesSnippet += '</div>';
		}, this);
		el.update(imagesSnippet);
		this.addListener('afterlayout', function(p, l) {
			this.buildProspect.defer(500, this);
			var query = this.search.getValue();
			if (query != '') {
				this.showProspectHits.defer(550, this, [query]);
			}
		}, this, {single: true});
	}

	,fetchText : function(el, overrides, getNextDoc) {
		var params = this.getApiParams();
		getNextDoc = getNextDoc != null ? getNextDoc : true;
		Ext.applyIf(params, {
			start: this.getApiParamValue('start')
			,limit: this.getApiParamValue('limit')
		})
		Ext.apply(params, overrides);
		Ext.apply(params, {
			tool: 'Tokens'
			,template: 'docTokens2html'
			,outputFormat: 'html'
			,snippet: 'true'
		});
		if (params.docId) {
			params.docIndex = this.getCorpus().getDocument(params.docId).getIndex();
			delete params.docId;
		}
		if (!params.docIndex) {params.docIndex=0;}
		
		if (!el) {el=Ext.DomHelper.append(Ext.get('prospect_reader'), {tag: 'span'}, true);}
		el.addClass('segment_'+params.docIndex+'_'+params.start+'_'+params.limit);
		this.loading = true;
		el.load({
			url: this.getTromboneUrl()
			,callback: function(el, success, response, options) {
				var doc = this.getCorpus().getDocument(params.docIndex);
				if (params.start==0) {
					var title = Ext.DomHelper.insertFirst(el, {tag: 'h1'/*, cls: 'x-panel-header'*/}, true);
					title.update(doc.getLabel())
				}
				this.loading = false;
				if (getNextDoc && doc.getTotalTokens() < params.limit) {
					var nextDocIndex = params.docIndex + 1;
					if (nextDocIndex < this.getCorpus().getSize()) {
						this.setApiParams({docIndex: nextDocIndex, start: 0});
						this.fetchText(null, null, false);
					}
				}
				var reader = Ext.get('prospect_reader');
				if (reader.first()==reader.last()) {
					this.highlightProspect(params.docIndex, params.start);
				}
				else {
					if (reader.first()==el) {
						el.last().scrollIntoView(reader).frame("ff0000", 1, { duration: 2 });
						this.highlightProspect(params.docIndex, params.start+params.limit);
					}
					else {
						el.first().frame("ff0000", 1, { duration: 2 });
						this.highlightProspect(params.docIndex, params.start);
					}
				}
				
				var query = this.search.getValue();
				if (query != '') {
					this.highlightKeywords(query, false);
				}
			}
			,params: params
			,scope: this
		});
	}
	
	,moveFirst : function() {
		var el = Ext.get('prospect_reader');
		this.setApiParams({docIndex: 0, start: 0});
		el.update('');
		this.fetchText();
	}
	
	,movePrevious : function() {
		var limit = parseInt(this.getApiParamValue('limit'));
		var el = Ext.get('prospect_reader');
		var segmentObject = this.getSegmentObject(el.first());
		if (segmentObject) {
			var doc = segmentObject.docIndex;
			var start = segmentObject.start;
			if (doc==0 && start==0) {return;} // already at start
			if (start==0) { // move to end of previous document
				doc -= 1;
				this.setApiParams({docIndex: doc});
				start = this.getCorpus().getDocument(doc).getTotalTokens()-limit;
				if (start<0) {start=0}
			} else {
				if (start-limit>-1) {start -= limit;}
				else {
					limit = parseInt(start);
					start = 0;
				}
			}
			this.setApiParams({start: start});
			el.update('');
			this.fetchText();
		}
	}
	
	,moveNext : function() {
		var limit = parseInt(this.getApiParamValue('limit'));
		var el = Ext.get('prospect_reader');
		var segmentObject = this.getSegmentObject(el.last());
		if (segmentObject && segmentObject.docIndex != null) {
			var doc = segmentObject.docIndex;
			var start = segmentObject.start;
			var lastLimit = segmentObject.lastLimit;
			var numberOfDocs = this.getCorpus().getSize();
			var totalWords = this.getCorpus().getDocument(doc).getTotalTokens();
			if (doc+1==numberOfDocs && start+lastLimit>=totalWords) {return;} // already at end
			if (start+lastLimit>=totalWords) { // move to end of previous document
				doc += 1;
				this.setApiParams({docIndex: doc});
				start = 0;
			}
			else { // move ahead
				start+=limit;
			}
			this.setApiParams({start: start});
			el.update('');
			this.fetchText();
		}
	}
	
	,moveLast : function() {
		var el = Ext.get('prospect_reader');
		var limit = parseInt(this.getApiParamValue('limit'));
		var lastDocIndex = this.getCorpus().getSize()-1;
		var totalWords = this.getCorpus().getDocument(lastDocIndex).getTotalTokens();
		var start = totalWords - limit;
		this.setApiParams({docIndex: lastDocIndex, start: start});
		el.update('');
		this.fetchText();
	}
	
	,highlightKeywords: function(query, doScroll) {
		var reader = Ext.get('prospect_reader');
		var nodes = Ext.DomQuery.select('span[class*=keyword]', reader.dom);
		for (var i=0; i<nodes.length;i++) {
			Ext.get(nodes[i]).removeClass('keyword');
		}
		nodes = Ext.DomQuery.select('span.token:nodeValueCaseInsensitive('+query+')', reader.dom);
		if (nodes.length>0) {			
			for (var i=0; i<nodes.length;i++) {
				Ext.get(nodes[i]).addClass('keyword');
			}
			if (doScroll) Ext.get(nodes[0]).scrollIntoView(reader).frame("ff0000", 1, { duration: 2 });
		}
	}
	
	,showProspectHits : function(query) {
		this.highlightKeywords(query, true);

		var docIdTypes = [];
		var maxLines = 1;
		this.documents.each(function(doc, index) {
			docIdTypes.push(doc.document.getId()+':'+query);
			if (doc.lines > maxLines) maxLines = doc.lines;
		}, this);
		if (this.documents.getCount() > 1) {
			// round up to nearest 100
			maxLines /= 100;
			maxLines = Math.ceil(maxLines);
			maxLines *= 100;
		}
		
		Ext.Ajax.request({
		   url: this.getTromboneUrl(),
		   params: {
				tool: 'DocumentTypeFrequencies'
				,docIdType: docIdTypes
				,bins: maxLines
				,corpus: this.getCorpus().getId()
		   },
		   success: function(response, options) {
				var json = Ext.decode(response.responseText);
				var store = new Ext.data.JsonStore({
					fields: Voyeur.data.DocumentTypes.fields
					,data: json.documentTypes.types
				});
				
				var min,				// min value
					max,				// max value
					absMax,				// absolute max
					vals,				// raw frequency distribution values
					index,				// document index
					lines,				// number of lines per document
					valsPerLine,		// number of values per line
					extraValsPerLine,	// the remainder, per line
					extraCount,			// tracker for when to add another value to the current line
					valsCount,			// tracker to ensure all values have been used
					sum,				// sum of values for the current line
					lineSums,			// array to store the value sums for each line
					op,					// opacity
					i, j,				// counters
					valsToUse;			// how many values to sum for the current line
				
				store.each(function(item) {
					max = null;//item.get('rawDistributionMax');
					min = null;//item.get('rawDistributionMin');
					vals = item.get('rawFreqs');
					index = item.get('docIndex');
					
					lines = this.documents.get(index).lines;
					valsPerLine = Math.floor(vals.length / lines);
					extraValsPerLine = vals.length % lines / lines;
					extraCount = 0;
					valsCount = 0;
					
					lineSums = [];
					for (i = 0; i < lines; i++) {
						sum = 0;
						valsToUse = i * valsPerLine + valsPerLine;
						extraCount += extraValsPerLine;
						if (extraCount >= 1) {
							valsToUse++;
							extraCount -= 1;
						}
						for (j = i * valsPerLine; j < valsToUse; j++) {
							valsCount++;
							sum += vals[j];
						}
						lineSums[i] = sum;
						if (max == null || max < sum) max = sum;
						if (min == null || min > sum) min = sum;
					}
					if (valsCount < vals.length) {
						lineSums[lineSums.length-1] += vals[vals.length-1];
					}
					absMax = max - min;
					
					for (i = 0; i < lines; i++) {
						var el = Ext.get('prospect.'+index+'.'+i);
						if (el) {
							op = max > 0 ? (lineSums[i] - min) / absMax : 0;
							if (op < .05) op = .05;
							el.setOpacity(op);
						} else {
//							console.warn(el,op,index,i);
						}
					}
				}, this);
		   },
		   failure: function(response, options) {
			   var imgs = Ext.DomQuery.jsSelect('img', this.body.dom);
			   for (var i = 0; i < imgs.length; i++) {
				   Ext.get(imgs[i]).setOpacity(.05);
			   }
		   },
		   scope: this
		});
	}
	
	,highlightProspect: function(docIndex, offset) {
		var doc = this.documents.get(docIndex);
		var line = Math.round(doc.lines * offset / doc.document.getTotalTokens());
		//console.log('finding', docIndex, offset, line);
		var el = Ext.get('prospect.'+docIndex+'.'+line);
		if (this.currentHighlightedProspect) {
			if (this.currentHighlightedProspect==el) {return;}
			this.currentHighlightedProspect.removeClass('current');
		}
		if (el) {
			el.addClass('current').frame("ff0000", 1, { duration: 2 });
			this.currentHighlightedProspect = el;
		}
	}
	
	,getCurrentSegmentObject: function() {
		var el = Ext.get('prospect_reader');
		var scrollTop = el.dom.scrollTop;
		var segments = el.dom.childNodes;
		for (var i=0;i<segments.length;i++) {
			if (segments[i].nodeType==1) {
				if (scrollTop>=segments[i].offsetTop && scrollTop<=segments[i].offsetTop+segments[i].offsetHeight) {
					return this.getSegmentObject(segments[i]);
				}
			}
		}
		return this.getSegmentObject(el.first())
	}
	
	,getSegmentObject: function(segment) {
		var className = segment.className ? segment.className : segment.dom.className;
		var segmentRegex = /segment_(\d+)_(\d+)_(\d+)/;
		var match = segmentRegex.exec(className)
		if (match) {
			return {
				element: segment
				,docIndex: parseInt(match[1])
				,start: parseInt(match[2])
				,lastLimit: parseInt(match[3])
			}
		}
		return {};
		
	}
	 
	,api: {
		/**
		 * @property start The index in the results to start at.
		 * @type Integer
		 * @default 0
		 */
		start: {'default': 0}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 1000
		 */
		,limit: {'default': 1000}
		/**
		 * @property docIndex The document index to restrict results to.
		 * @type Integer
		 * @default 0
		 */
		,docIndex: {'default': 0}
		,toolType: ['Corpus']
		,listeners: ['CorpusSummaryResultLoaded', 'TokensResultLoaded', 'documentTypeSelected', 'tokenSelected']
		,dispatchers: ['documentTypeSelected']
	}
	
	,thumb: {
		large: 'Reader.png'
	}
	
	,i18n : {
		title : {
			en : 'Corpus Reader'
		},
		help: {en: ''}
		,startText: {en: 'Jump to the start of the text'}
		,previousText: {en: 'Fetch preceding segment of text'}
		,nextText: {en: 'Fetch next segment of text'}
		,endText: {en: 'Jump to the end of the text'}
	}
});
Ext.reg('voyeurReader', Voyeur.Tool.Reader);

/**
 * @class Voyeur.Tool.CorpusSummary A panel that provides an overview of the current corpus, intended to suggest preliminary aspects to study.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Stéfan Sinclair
 * @since 0.1.1
 */
Voyeur.Tool.CorpusSummary = Ext.extend(Ext.Panel, {
	
	loading: false,
	
	constructor : function(config) {
		
		Ext.apply(this, new Voyeur.Tool(config, this));

		Ext.applyIf(config, {
			bodyStyle: {'background-color': 'white', 'padding-top': '12px'},
			frame: true,
			border: false,
			autoScroll: true
		})
		Voyeur.Tool.CorpusSummary.superclass.constructor.call(this, config);
		
		/**
		 * @event CorpusSummaryResultLoaded
		 * @type listener
		 */
		this.addListener('CorpusSummaryResultLoaded', function(src, data) {
			if (this.rendered && !this.loading) {
				this.loadData();
			}
		}, this);
	}

	,loadData: function() {
		this.loading = true;
		this.body.update(""); // clear page
		if (this.getCorpus().getSize()==0) {return;}
		this.showCorpusDocsAndWordsCount();
		this.body.addListener('click', function(e) {
			var target = e.getTarget(null,null,true);
			if (target && target.dom.tagName=='A') {
				var app = this.getApplication();
				params = {};
				if (target.hasClass('corpus-documents')) {
					if (target.hasClass('corpus-documents-length')) {
						params.sortBy =  'totalWordTokens';
					}
					else if (target.hasClass('corpus-documents-density')) {
						params.sortBy =  'wordDensity';
					}
					params.sortDirection='DESC';
					/**
					 * @event CorpusGridBootstrap
					 * @param {Voyeur.Tool.CorpusSummary} tool
					 * @param {Object} params <ul>
					 * <li><b>sortBy</b> : String</li>
					 * <li><b>sortDirection</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					app.dispatchEvent.call(app, 'CorpusGridBootstrap', this, params);
				}
				else if (target.hasClass('document-id')) {
					if (target.hasClass('document-id-distinctive')) {
						Ext.apply(params, {sortBy: 'relativeFreqCorpusDelta', sortDirection: 'DESC'});
					}
					if (target.hasClass('document-id-density')) {
						Ext.apply(params, {sortBy: 'wordDensity', sortDirection: 'DESC'});
					}
					params.docId = target.getAttributeNS('voyeur', 'val');
					/**
					 * @event corpusDocumentSelected
					 * @param {Voyeur.Tool.CorpusSummary} tool
					 * @param {Object} params <ul>
					 * <li><b>docId</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					app.dispatchEvent.call(app, 'corpusDocumentSelected', this, params);
				}
				else if (target.hasClass('corpus-type')) {
					/**
					 * @event corpusTypeSelected
					 * @param {Voyeur.Tool.CorpusSummary} tool
					 * @param {Object} params <ul>
					 * <li><b>type</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					app.dispatchEvent.call(app, 'corpusTypeSelected', this, {type: target.dom.innerHTML});
				}
				else if (target.hasClass('corpus-types')) {
					if (target.hasClass('corpus-types-peaks')) {
						Ext.apply(params, {sortBy: 'RELATIVEDISTRIBUTIONKURTOSIS', sortDirection: 'DESC', extendedSortZscoreMinimum: 1});
					}
					else {
						Ext.apply(params, {sortBy: 'rawFreq', sortDirection: 'DESC', extendedSortZscoreMinimum: null});
					}
					/**
					 * @event CorpusTypeFrequenciesRequest
					 * @param {Voyeur.Tool.CorpusSummary} tool
					 * @param {Object} params <ul>
					 * <li><b>sortBy</b> : String</li>
					 * <li><b>sortDirection</b> : String</li>
					 * <li><b>extendedSortZscoreMinimum</b> : Number</li>
					 * </ul>
					 * @type dispatcher
					 */
					app.dispatchEvent.call(app, 'CorpusTypeFrequenciesRequest', this, params, {type: 'whitelist', tools: 'voyeurCorpusTypeFrequenciesGrid'});
				}
				else if (target.hasClass('document-type')) {
					/**
					 * @event documentTypeSelected
					 * @param {Voyeur.Tool.CorpusSummary} tool
					 * @param {Object} params <ul>
					 * <li><b>docIdType</b> : String</li>
					 * </ul>
					 * @type dispatcher
					 */
					app.dispatchEvent.call(app, 'documentTypeSelected', this, {docIdType: target.getAttributeNS('voyeur', 'val')});
				}
			}
		}, this);
	}

	,showCorpusDocsAndWordsCount: function() {
		var corpus = this.getCorpus();
		new Ext.XTemplate(this.localize('corpusDocsAndWordsCount')).append(this.body, {
			docsCount: Ext.util.Format.number(corpus.get('docsCount'),'0,000')
			,totalWordTokens: Ext.util.Format.number(corpus.get('totalWordTokens'),'0,000')
			,totalWordTypes: Ext.util.Format.number(corpus.get('totalWordTypes'),'0,000')
		});
		this.showDocLengthsAndDensity();
	}
	
	,showDocLengthsAndDensity : function() {
		var docs = this.getCorpus().getDocuments().clone();
		var count = docs.getCount();
		if (count>1) {
			
			var lengths = [];
			var densities = [];
			docs.each(function(item) {
				lengths.push(item.get('totalWordTokens'))
				densities.push(item.get('wordDensity'))
			})

			// sort items
			docs.sort(null, function(a,b) {
				return a.get('totalWordTokens') >  b.get('totalWordTokens') ? -1 : 1;
			});
			
			var data = {};
			var data = count>5 ? {shortest: docs.getRange(count-2).reverse(), longest: docs.getRange(0,1)} : {all: docs.getRange()}
			var obj;
			for (var k in data) {
				if (k) {
					var items = data[k];
					for (var i=0;i<items.length;i++) {
						data[k][i] = {
							title: items[i].getShortTitle(),
							id: items[i].get('id'),
							totalWordTokens: Ext.util.Format.number(items[i].get('totalWordTokens'),'0,000'),
							docsLen: items.length
						}
					}
				}
			}
			
			var tpl = new Ext.XTemplate(this.localize('docsLength'));
			var out = '';
			if (count>5) {
				out += new Ext.Template(this.localize('docsLengthLongest')).applyTemplate([this.getSparkLine(lengths)]) + this.localize('colon', 'app') + tpl.apply({docs: data.longest})+'. ';
				out += this.localize('docsLengthShortest') + this.localize('colon', 'app') + tpl.apply({docs: data.shortest})+'. ';
				out += "<a href='#' onclick='return false' class='corpus-documents corpus-documents-length'>"+this.localize('seeAll')+'</a>'
			}
			else {
				out += new Ext.Template(this.localize('docsLengthAll')).applyTemplate([this.getSparkLine(lengths)]) + this.localize('colon', 'app') + tpl.apply({docs: data.all});
			}
			
			Ext.DomHelper.append(this.body, {tag: 'div', cls: 'corpus-summary-item', html: out});

			// sort items
			docs.sort(null, function(a,b) {
				return a.get('wordDensity') >  b.get('wordDensity') ? -1 : 1;
			});
			
			var data = {};
			var data = count>5 ? {lowest: docs.getRange(count-2).reverse(), highest: docs.getRange(0,1)} : {all: docs.getRange()}
			var obj;
			
			for (var k in data) {
				if (k) {
					var items = data[k];
					for (var i=0;i<items.length;i++) {
						data[k][i] = {
							title: items[i].getShortTitle(),
							id: items[i].get('id'),
							wordDensity: Ext.util.Format.number(items[i].get('wordDensity'),'0,000.0'),
							docsLen: items.length
						}
					}
				}
			}
			var tpl = new Ext.XTemplate(this.localize('docsDensity'));
			var out = '';
			if (count>5) {
				out += new Ext.Template(this.localize('docsDensityHighest')).applyTemplate([this.getSparkLine(densities)]) + this.localize('colon', 'app') + tpl.apply({docs: data.highest})+'. ';
				out += this.localize('docsDensityLowest') + this.localize('colon', 'app') + tpl.apply({docs: data.lowest})+'. '
				out += "<a href='#' onclick='return false' class='corpus-documents corpus-documents-density'>"+this.localize('seeAll')+'</a>'
			}
			else {
				out += new Ext.Template(this.localize('docsDensityAll')).applyTemplate([this.getSparkLine(densities)])  + this.localize('colon', 'app') + tpl.apply({docs: data.all}) +'.';
			}

			Ext.DomHelper.append(this.body, {tag: 'div', cls: 'corpus-summary-item', html: out})
		}
		
		this.showCorpusTypesTop();
	}
	
	,showCorpusTypesTop: function() {
		var el = Ext.DomHelper.append(this.body, {tag: 'div', cls: 'corpus-summary-item', html: this.localize('corpusTypesTop')}, true);
		var mask = new Ext.LoadMask(el, {msg: this.localize('loading'), removeMask: true});
		mask.show();
		var params = this.getApiParams();
		params.tool = 'CorpusTypeFrequencies';
		Ext.Ajax.request({
			url: this.getTromboneUrl()
			,params: params
			,callback: function(options, success, response) {
				if (success) {
					var json = Ext.decode(response.responseText);
					var store = new Ext.data.JsonStore({
						fields: Voyeur.data.CorpusTypes.fields
						,data: json.corpusTypes.types
					})
					var data = [];
					var len = store.getCount();
					store.each(function(item) {
						data.push({
							type: item.get('type'),
							val: Ext.util.Format.number(item.get('rawFreq'),'000,0'),
							len: len});
					})
					mask.hide();
					new Ext.DomHelper.overwrite(el, {
						tag: 'p'
						,html: this.localize('corpusTypesTop')+this.localize('colon','app')+
							new Ext.XTemplate(this.localize('corpusType')).applyTemplate({types: data})+
							'. <a href="#" onclick="return false;" class="corpus-types corpus-types-freqs">'+this.localize('more')+'</a>'
					})
				}
				else {
					this.alertError(response.responseText);
				}
				this.showCorpusTypesPeaks();
			}
			,scope: this
		});
		
	}
	
	,showCorpusTypesPeaks: function() {
		if (this.getCorpus().getSize()>1) {
			var el = Ext.DomHelper.append(this.body, {tag: 'div', cls: 'corpus-summary-item',  html: this.localize('corpusTypesPeaks')}, true);
			var mask = new Ext.LoadMask(el, {msg: this.localize('loading'), removeMask: true});
			mask.show();
			var params = this.getApiParams();
			params.tool = 'CorpusTypeFrequencies';
			params.sortBy = 'RELATIVEDISTRIBUTIONKURTOSIS';
			params.extendedSortZscoreMinimum = 1; // try to make them more significant
			Ext.Ajax.request({
				url: this.getTromboneUrl()
				,params: params
				,callback: function(options, success, response) {
					if (success) {
						var json = Ext.decode(response.responseText);
						var store = new Ext.data.JsonStore({
							fields: Voyeur.data.CorpusTypes.fields
							,data: json.corpusTypes.types
						})
						var data = [];
						var len = store.getCount();
						store.each(function(record) {
							var val = record.get('relativeFreqs');
							data.push({
								type: record.get('type'),
								val: Ext.util.Format.number(record.get('rawFreq'),'0,000')+' '+this.getSparkLine(val, 20),
								len: len
							});
						}, this)
						mask.hide();
						new Ext.DomHelper.overwrite(el, {
							tag: 'p'
							,html: this.localize('corpusTypesPeaks')+this.localize('colon','app')+
								new Ext.XTemplate(this.localize('corpusType')).applyTemplate({types: data}) +
								'. <a href="#" onclick="return false;" class="corpus-types corpus-types-peaks">'+this.localize('more')+'</a>'
						})
					}
					this.showDocumentDistinctiveWords();
				}
				,scope: this
			})
		} else {
			this.showDocumentDistinctiveWords();
		}
	}
	
	/**
	 * Show the distinctive words in the document.
	 * @private
	 */
	,showDocumentDistinctiveWords : function() {
		if (this.getCorpus().getSize()>1) {
			var docs = this.getCorpus().getDocuments();
			if (docs.getCount()>1) {
				Ext.DomHelper.append(this.body, {tag: 'div', cls: 'corpus-summary-item',  html: this.localize('distinctiveWords')});
				var list = Ext.DomHelper.append(this.body, {tag: 'ol', cls: 'normal-list', html: ''}, true);
				var item;
				var len = this.getApiParamValue('numberOfDocumentsForDistinctiveWords');
				docs.each(function(item,index,length) {
					Ext.DomHelper.append(list, {tag: 'li', 'voyeur:index': String(index), cls: (index>len-1 ? 'x-hidden' : ''), html: '<a href="#" onclick="return false" class="document-id document-id-distinctive" voyeur:val="'+item.get('id')+'">'+item.getShortTitle()+'</a>'});
				});
				if (docs.getCount()>len) {
					var tpl = new Ext.Template(this.localize('moreDistinctiveWords'));
					var remaining = docs.getCount()-len;
					var more = Ext.DomHelper.append(this.body, {tag: 'div', style: 'display: block; margin-left: 40px;', html: tpl.apply([len>remaining ? remaining : len,remaining])}, true);
					more.addListener('click', function() {
						var hidden = list.select("li[@class=x-hidden]");
						var item;
						for (i=0;i<hidden.getCount();i++) {
							if (i==len) {break;}
							item = hidden.item(i).removeClass('x-hidden');
						}
						this.showDocumentDistinctiveWordsStep(hidden.item(0));
						var remaining = hidden.getCount()-len;
						if (remaining>0) {
							more.update(tpl.apply([len>remaining ? remaining : len,remaining]))
						}
						else {more.remove();}
					}, this);
				}
	//			docs.each(function(item,index,length) {
	//				Ext.DomHelper.append(list, {tag: 'li', html: '<a href="#" onclick="return false" class="document-id document-id-distinctive" voyeur:val="'+item.get('id')+'">'+item.getShortLabel()+'</a>'});
	//			});
				this.showDocumentDistinctiveWordsStep(list.first());
			}
		} else {
			this.showCirrus();
		}
	}
	
	,showDocumentDistinctiveWordsStep : function(el) {
		var mask = new Ext.LoadMask(el, {msg: this.localize('loading'), removeMask: true});
		mask.show();
		var index = Number(el.getAttributeNS('voyeur','index'));
		var params = this.getApiParams();
		Ext.apply(params, {
			tool: 'DocumentTypeFrequencies',
			docIndex: String(index),
			sortBy: 'relativeFreqCorpusDelta',
			sortDirection: 'DESC'
		})
		Ext.Ajax.request({
			url: this.getTromboneUrl()
			,params: params
			,callback: function(options, success, response) {
				if (success) {
					var data = Ext.decode(response.responseText);
					var store = new Ext.data.JsonStore({
						fields: Voyeur.data.DocumentTypes.fields
						,data: data.documentTypes.types
					})
					var data = [];
					var len = store.getCount();
					store.each(function(item) {
						data.push({
							type: item.get('type'),
							val: Ext.util.Format.number(item.get('rawFreq'),'000,0'),
							docId: item.get('docId'),
							len: len})
					})
					mask.hide();
					new Ext.DomHelper.append(el,
							this.localize('colon','app')+new Ext.XTemplate(this.localize('documentType')).applyTemplate({types: data})+
							'. <a href="#" onclick="return false;" class="document-id document-id-distinctive" voyeur:val="'+this.getCorpus().getDocument(index).get('id')+'">'+this.localize('more')+'</a>'
					)
					var nextel = el.next();
					if (nextel && !nextel.hasClass('x-hidden')) {
						this.showDocumentDistinctiveWordsStep(nextel, index+1)
					} else {
						this.showCirrus();
					}
				}
			}
			,scope: this
		})
	}
	
	,showCirrus : function() {
		this.loading = false;
		
		return; // disabled for now
		
		var width = this.getWidth() - 12;
		var height = width / 2.5;
		this.add({
			xtype: 'voyeurCirrus',
			width: width,
			height: height,
			style: 'padding: 0 12px 12px;'
		});
		this.doLayout();
	}
	
	,showOptions : function() {
		this.showOptionsWindow({
			items : [{
				xtype : 'form',
				labelWidth : 150,
				labelAlign : 'right',
				border : false,
				items : [],
				buttons : [{
					text : this.localize('ok', 'tool'),
					iconCls : 'icon-accept',
					listeners : {
						click : {
							fn : function(btn) {
								var formPanel = btn.findParentByType('form');
								var form = formPanel.getForm();
								var stopList = form.findField('stopList');
								var global = form.findField('globalStopWords').getValue();
								
								// make sure we don't have any queries
								if (stopList.getValue() && !stopList.getRawValue()) {stopList.setValue('');}
								this.setApiParams({stopList: stopList.getValue()});

								formPanel.findParentByType('window').destroy();
								
								if (global) {
									this.getApplication().applyParamsGlobally({
										stopList: this.getApiParamValue('stopList')
									}, true);
								}
								else {
									this.loadData();
								}
						    	
							},
							scope : this
						}
					}
				}, {
					text : this.localize('cancel', 'tool'),
					handler : function(btn) {
						btn.findParentByType('window').destroy();
					}
				}, {
					text : this.localize('restore', 'tool'),
					listeners : {
						click : {
							fn : function(btn) {
								var form = btn.findParentByType('form').getForm();
								form.findField('stopList').setValue(this.getApiParamDefaultValue('stopList'));
							},
							scope : this
						}
					}
	
				}]
			}]
		}, true);

	}
	
	,api: {
		/**
		 * @property stopList The stop list to use to filter results.
		 * Choose from a pre-defined list, or enter a comma separated list of words, or enter an URL to a list of stop words in plain text (one per line).
		 * @type String
		 * @default null
		 * @choices stop.en.taporware.txt, stop.fr.veronis.txt
		 */
		'stopList': {
			'default': null
			,'choices': ['stop.en.taporware.txt', 'stop.fr.veronis.txt']
		}
		/**
		 * @property numberOfDocumentsForDistinctiveWords The maximum number of documents to show distinctive words for.
		 * @type Integer
		 * @default 5
		 */
		,'numberOfDocumentsForDistinctiveWords': {
			'default': 5
		}
		/**
		 * @property limit The number of words to return in each call.
		 * @type Integer
		 * @default 5
		 */
		,'limit': {
			'default': 5
		}
		,toolType: ['Corpus']
		,listeners: ['CorpusSummaryResultLoaded']
		,dispatchers: ['CorpusGridBootstrap', 'corpusDocumentSelected', 'corpusTypeSelected', 'CorpusTypeFrequenciesRequest', 'documentTypeSelected']
	}
	
	,thumb: {
		large: 'CorpusSummary.png'
	}
	
	// private localization variables
	,i18n : {
		title : {en: "Summary"}
	    ,type : {en: "Summary"}	
		,corpusDocsAndWordsCount: {en: '<div class="corpus-summary-item">There <tpl if="docsCount == 1">is <a href="#" onclick="return false" class="corpus-documents" ext:qtip="Click to see a table of documents">1 document</a></tpl><tpl if="docsCount &gt; 1">are <a href="#" onclick="return false" class="corpus-documents" ext:qtip="Click to see a table of documents">{docsCount} documents</a></tpl> in this corpus with a total of <b>{totalWordTokens} words</b> and <b>{totalWordTypes} unique words</b>.</div>'}
		,docsLength: {en: '<tpl for="docs"><a href="#" onclick="return false" class="document-id" voyeur:val="{id}">{title}</a> ({totalWordTokens})<tpl if="xindex &lt; docsLen">, </tpl></tpl>'}
		,docsLengthLongest: {en: '<b>Longest documents</b> (by words {0})'}
		,docsLengthShortest: {en: 'Shortest documents'}
		,docsLengthAll: {en: 'Documents ordered by number of words ({0})'}
		,docsDensity: {en: '<tpl for="docs"><a href="#" onclick="return false" class="document-id" voyeur:val="{id}">{title}</a> ({wordDensity})<tpl if="xindex &lt; docsLen">, </tpl></tpl>'}
		,docsDensityHighest: {en: 'Highest <b>vocabulary density</b> ({0})'}
		,docsDensityLowest: {en: 'Lowest density'}
		,docsDensityAll: {en: 'Documents ordered by vocabulary density ({0})'}
		,help : {en : "<p>This tool shows an overview of the corpus and tries to suggest characteristics worthy of further exploration.</p>"}
		,corpusTypesTop: {en: 'Most <b>frequent words</b> in the corpus'}
		,corpusTypesPeaks: {en: 'Words with <b>notable peaks in frequency</b> across the corpus'}
		,corpusType: {en: '<tpl for="types"><a href="#" onclick="return false" class="corpus-type keyword">{type}</a> ({val})<tpl if="xindex &lt; len">, </tpl></tpl>'}
		,documentType: {en: '<tpl for="types"><a href="#" onclick="return false" class="document-type keyword" voyeur:val="{docId}:{type}">{type}</a> ({val})<tpl if="xindex &lt; len">, </tpl></tpl>'}
		,distinctiveWords: {en: '<b>Distinctive words</b> (compared to the rest of the corpus)'}
		,moreDistinctiveWords: {en: '<a href="#" onclick="return false">Next {0} of {1} remaining</a>'}
		,loading: {en: 'Loading&hellip;'}
		,seeAll: {en: 'All&hellip;'}
		,more: {en: 'More&hellip;'}
	}
});

Ext.reg('voyeurCorpusSummary', Voyeur.Tool.CorpusSummary);


Voyeur.Tool.Sunburst = Ext.extend(Ext.Panel, {
	
	initialized: false,
	
	getObjectId: function() {
		return this.id.replace(/-/g,'_')+'_entities';
	},
	
	graph: null,
	
	data: null,

	initApp: function() {
		var id = this.getObjectId();
		
		var initSunburst = function() {
			try {
				var box = this.body.getBox();
				var ringWidth = Math.min(box.height, box.width) / 1.5 / (this.data.getCount() + 1);
				var left = box.width / 2;
				var bottom = box.height / 2;
				
				Ext.DomHelper.overwrite(this.body, {
					tag: 'div',
					id: id,
					style: 'width: '+box.width+'px; height: '+box.height+'px;'
				});
				
				this.graph = new pv.Panel()
				.canvas(id)
				.event('mousedown', pv.Behavior.pan())
				.event('mousewheel', pv.Behavior.zoom());
				
				var pi2 = 2 * Math.PI;
				
				this.graph.add(pv.Wedge)
				.left(left)
				.bottom(bottom)
				.innerRadius(0)
			    .outerRadius(ringWidth)
				.angle(pi2)
				.fillStyle('#fff')
				.anchor('center').add(pv.Label)
				.text('FirstPersonNarrator')
				.font('bold 10px sans-serif');
				
				var freqSizing = this.getApiParamValue('freqSizing');
				
				var i, item;
				var index = 1;
				this.data.eachKey(function(key, tagParent) {
					var data = [];
					for (i = 0; i < tagParent.length; i++) {
						item = tagParent[i];
						data.push({
							freq: item.freq,
							label: item.name,
							color: item.color
						});
					}

					var sum =  pv.sum(data, function(d) {return d.freq});
					var a = pv.Scale.linear(0, sum).range(0, pi2);
					
					this.graph.add(pv.Wedge)
					.title(function(d) {
				    	return d.label;
				    })
				    .data(data)
				    .left(left)
				    .bottom(bottom)
				    .fillStyle(data[0].color)
				    .strokeStyle('#fff')
				    .lineWidth(0.5)
				    .innerRadius(index * ringWidth)
				    .outerRadius((index+1) * ringWidth)
				    .angle(function(d) {
				    	if (freqSizing) {
				    		return a(d.freq);
				    	} else {
				    		return pi2 / data.length;
				    	}
				    })
				    .cursor('pointer')
				    .events('all')
				    .event('click', function(d) {
				    	this.alertInfo({msg: d.label, width: 200});
				    }.createDelegate(this))
				    .anchor('center').add(pv.Label)
				    .text(function(d) {return d.label});
					
					index++;
				}, this);

				this.graph.render();
				
			    this.initialized = true;
	        } catch(e) {
	            setTimeout(initSunburst.createDelegate(this), 500);
	        }
		}
		
		var inserts = '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/lib/protovis/protovis-r3.2.js"></script>';
        this.body.update(inserts, true, initSunburst.createDelegate(this));
	},
	
	loadData: function() {
		if (typeof tsvInput != 'undefined') {
			tsvInput = tsvInput.replace(/\/n/g, "\n");
			this.processData(tsvInput);
			this.initApp();
		} else {
			Ext.Ajax.request({
				url: this.getToolDirectoryUrl()+'FirstPersonNarrator.csv',
				success: function(response, options) {
					var r = response.responseText;
					this.processData(r);
					this.initApp();
				},
				scope: this
			});
		}
	},
	
	processData: function(tsvData) {
		var lines = tsvData.split(/\n/);
		this.data = new Ext.util.MixedCollection();
		var line, tag, name, freq, color;
		for (var i = 0; i < lines.length; i++) {
			line = lines[i].split(/\t/);
			if (line.length == 4) {
				tag = line[0];
				name = line[1];
				freq = parseFloat(line[2]);
				color = line[3];
				if (this.data.containsKey(tag)) {
					var entry = this.data.key(tag);
					entry.push({
						name: name,
						freq: freq,
						color: color
					});
					this.data.replace(tag, entry);
				} else {
					this.data.add(tag, [{
						name: name,
						freq: freq,
						color: color
					}]);
				}
			}
		}
	},
	
    initComponent: function() {
        Voyeur.Tool.Sunburst.superclass.initComponent.call(this);
    },
    

    constructor: function(config) {
    	Ext.apply(config, {
    		bodyStyle: 'text-align: center'
    	});
        Ext.apply(this, new Voyeur.Tool(config, this));
        
        Voyeur.Tool.Sunburst.superclass.constructor.apply(this, arguments);
        
        this.addListener('afterrender', function() {
        	this.loadData();
        }, this);
    },
	
    api: {
    	freqSizing: {
    		'default': true
    	}
    },
    
	i18n: {
        title : {en: 'Sunburst'}
        ,type : {en: "Visualization"}
        ,help : {en: 'Click and drag to pan, mousewheel to zoom.'}
	}
});

Ext.reg('voyeurSunburst', Voyeur.Tool.Sunburst);
/**
 * @class Voyeur.Tool.Knots A visualization which displays type offsets using curving branches.
 * @namespace Voyeur.Tool
 * @extends_ext Ext.Panel
 * @extends Voyeur.Tool
 * @author Andrew MacDonald
 * @since 1.0
 */
Voyeur.Tool.Knots = Ext.extend(Ext.Panel, {
	MAX_LINE_LENGTH: 0,
	REFRESH_INTERVAL: Ext.isGecko ? 50 : 10,
	LINE_SIZE: 2.5,
	
	colors: ['116,116,181', '139,163,83', '189,157,60', '171,75,75', '174,61,155'],
	
	drawPos: 0,
	intervalId: null,
	offset: {x: 0, y: 0},
	
	originColor: '30, 65, 118',
	originOpacity: 1.0,
	
	maxDocLength: 0,
	store: null,
	canvas: null,
	ctx: null,
	cache: new Ext.util.MixedCollection(),
	selectedDocs: null,
	typeTpl: new Ext.XTemplate(
		'<tpl for=".">',
			'<div class="type" style="color: rgb({color});">{type}</div>',
		'</tpl>'
	),
	typeStore: new Ext.data.ArrayStore({
        id: 0,
        fields: ['type', 'color'],
        listeners: {
        	load: function(store, records, options) {
        		var typesView = Ext.getCmp('typesView');
        		for (var i = 0; i < records.length; i++) {
        			var r = records[i];
        			typesView.select(r, true);
        		}
        	},
        	scope: this
        }
    }),
    kwicsStore: null,
	
	getObjectId : function() {
		return this.id.replace(/-/g,'_')+'_knots';
	},
	
	corpusTypeReader: new Ext.data.JsonReader({
        root : 'corpusTypes.types'
        ,totalProperty : 'corpusTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.CorpusTypes.fields)),
    documentTypeReader: new Ext.data.JsonReader({
        root : 'documentTypes.types'
        ,totalProperty : 'documentTypes["@totalTypes"]'
    }, Ext.data.Record.create(Voyeur.data.DocumentTypes.fields)), 
	
	constructor : function(config) {
		Ext.apply(this, new Voyeur.Tool(config, this));
		
		Ext.applyIf(config, {
            tbar: [{
                xtype: 'typeSearch',
                width: 100,
                emptyText: this.localize('findTerm'),
                parentTool: this,
                listeners: {
                	typeSelected: function(combo, type, record) {
            	    	var types = this.getApiParamValue('type') || [];
            	    	if (typeof types == 'string') types = [types];
                		types.push(type);
                		this.setApiParams({type: types});
						this.store.load({params: {type: type}, add: true});
            	    },
            	    scope: this
                }
            },' ',{
            	xtype: 'button',
            	text: this.localize('clearTerms'),
            	handler: function() {
            		this.removeAllTypes();
            		Ext.getCmp('typesView').clearSelections(true);
            		this.typeStore.removeAll();
            		this.store.removeAll();
            		this.setApiParams({typeFilter: []});
            		this.buildGraph();
            	},
            	scope: this
            },'-',{
            	xtype: 'documentSelector',
            	singleSelect: true,
            	listeners: {
            		documentsSelected: function(docIds) {
            			this.setApiParams({docId: docIds});
            			this.filterDocuments();
            			this.selectedDocs.each(this.findLongestDocument, this);
            			this.store.load();
            		},
            		scope: this
            	}
            },'-',this.localize('buildSpeed'),' ',{
            	xtype: 'slider',
            	width: 100,
            	increment: 10,
            	minValue: 10,
            	maxValue: 300,
            	value: parseInt(this.getApiParamValue('interval')),
            	plugins: new Ext.slider.Tip({
            		getText: function(thumb) {
            			return thumb.value;
            		}
            	}),
            	listeners: {
            		changecomplete: function(slider, newvalue) {
            			this.setApiParams({interval: newvalue});
            			this.buildGraph(t
