Mount Olympus

Common features shared amongst all Olympian scripts.

// ==UserScript==
// @name         Mount Olympus
// @namespace    mobiusevalon.tibbius.com
// @version      2.0-7
// @author       Mobius Evalon
// @description  Common features shared amongst all Olympian scripts.
// @license      Creative Commons Attribution-ShareAlike 4.0; http://creativecommons.org/licenses/by-sa/4.0/
// @require      https://code.jquery.com/jquery-1.12.4.min.js
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.26.0/js/jquery.tablesorter.min.js
// @include      /^https{0,1}:\/\/\w{0,}\.?mturk\.com.+/
// @include      /^https{0,1}:\/\/\w*\.amazon\.com\/ap\/signin.*(?:openid\.assoc_handle|pf_rd_i)=amzn_mturk/
// @exclude      /&hit_scraper$/
// @exclude      /\/HM$/
// @grant        none
// ==/UserScript==

if(window.olympus === undefined) window.olympus = {};

// there is a reason they are initialized in this order
window.olympus.__name = "olympus";
window.olympus.__version = "2.0-7";
window.olympus.__href = "https://greasyfork.org/en/scripts/23092-mount-olympus";
window.olympus.known_olympians = ["harpocrates","hermes","artemis","athena"];
window.olympus.default_settings = {
	query_turkopticon:true,
	use_to_cache:true,
	to_cache_timeout:10800000,
	to_pay_weight:6.5,
	to_fair_weight:4,
	to_fast_weight:1,
	to_comm_weight:0.5,
	bayesian_to:true,
	bayesian_reviews:75,
	bayesian_average:2.75
};

window.olympus.__init = function() {
	console.log("olympus init");

	Array.prototype.contains = function(item) {
		return (this.indexOf(item) > -1);
	};

	String.prototype.collapseWhitespace = function() {
		return this.replace(/\s{2,}/g," ").trim();
	};

	String.prototype.contains = function(substring) {
		return (this.indexOf(substring) > -1);
	};

	String.prototype.ucFirst = function() {
		return (this.charAt(0).toUpperCase()+this.slice(1));
	};

	olympus.style.add(
		"#javascriptDependentFunctionality {display: block !important;}"+
		".dialog.floats {border-radius: 8px; border: 2px solid #000000; max-height: 550px; position: absolute !important; z-index: 500; background-color: #7fb4cf; top: 25px; left: 200px; font-size: 12px;} "+
		".dialog.narrow {width: 300px; min-width: 300px;} "+
		".dialog.wide {width: 550px; min-width: 550px;} "+
		".dialog .scrolling-content {max-height: 350px; overflow-y: auto;} "+
		".dialog .actions {margin: 10px auto; padding: 0px; text-align: center; display: block;} "+
		".dialog .actions input:not(:last-of-type) {margin-right: 15px;} "+
		".dialog .head {padding: 0px; margin: 10px auto; font-size: 175%; font-weight: bold; width: 100%; text-align: center; cursor: move;} "+
		"#olympian_help p.inset {margin-left: 25px;}"+
		"#olympian_help p.inset b {margin-left: -25px; display: block;}"+
		"#olympian_settings .sidebar {float: left; min-width: 100px; padding-left: 5px;}"+
		"#olympian_settings .sidebar .tab {border-radius: 8px 0 0 8px; height: 30px; line-height: 30px; text-align: center; font-weight: bold; cursor: pointer;}"+
		"#olympian_settings .sidebar .tab.active {background-color: #88c1de;}"+
		"#olympian_settings .container {background-color: #88c1de; padding: 5px; margin-right: 5px; min-height: 150px;}"+
		"#olympian_settings .subheader {font-size: 125%; font-weight: bold; margin-bottom: 10px; background-color: #96d5f5; padding: 5px 0px 5px 5px;}"+
		"#olympian_settings .container .option_container {display: block;}"+
		"#olympian_settings .container .option_container b.name {width: 150px; display: inline-block; margin-right: 10px;}"+
		"#olympian_settings .container .description {margin-top: 5px; margin-bottom: 10px; font-size: 90%; padding-left: 10px;}"+
		"#olympian_settings .container .description .toggle {margin-right: 5px;}"+
		"#olympian_settings .container .description .collapsed {display: inline-block; width: 350px; height: 16px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;}"+
		"#olympian_settings .container .sublabel {width: 50px; display: inline-block; margin-right: 5px; text-align: right;}"+
		"#olympian_settings .container .fa-toggle-off, #olympian_settings .container .fa-toggle-on {width: 47px; text-align: center;}"+
		"#olympian_settings .container .fa-toggle-off {color: #000;}"+
		"#olympian_settings .container .fa-toggle-on {color: #fff;}"+
		"#olympian_settings .container .fa-toggle-off, #olympian_settings .container .fa-toggle-on, #olympian_settings .container .fa-plus-square-o, #olympian_settings .container .fa-minus-square-o {cursor: pointer;}"+
		"#olympian_settings .container input[type='number'] {width: 45px;}"+
		"#olympian_settings .container .plain {margin-bottom: 20px;}"+
		"#open_olympus_settings {cursor: pointer;}"+
		".olympian_identifier {display: block; margin: 5px auto 10px auto; text-align: center; font-size: 150%; font-weight: bold;}"+
		".anim_pulse {animation-name: anim_pulse; animation-duration: 350ms; animation-iteration-count: infinite; animation-timing-function: linear; animation-direction: alternate;}"+
		"@keyframes anim_pulse {from {opacity: 1;} to {opacity: 0.25;}} "
	);

	// append the fontawesome stylesheet to the page if it does not exist
	if(!$("link[rel='stylesheet'][href$='font-awesome.min.css']").length) $("head").append(
		$("<link/>")
			.attr({
				"data-pantheon":"olympus",
				"rel":"stylesheet",
				"href":"https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"
			})
	);

	// append the help window to the document
	$("body").append(
		$("<div/>")
			.attr({
				"data-pantheon":"olympus",
				"id":"olympian_help",
				"class":"dialog wide floats"
			})
			.append(
				$("<h1/>")
					.attr("class","head")
					.text("Olympian help"),
				$("<div/>")
					.attr("class","scrolling-content")
					.append(
						$("<div/>").attr("class","explain")
					),
				$("<div/>")
					.attr("class","actions")
					.append(
						$("<button/>")
							.text("Close")
							.click(function() {
								olympus.help.hide();
							})
					)
			)
			.hide(),
		$("<div/>")
			.attr({
				"data-pantheon":"olympus",
				"id":"olympian_settings",
				"class":"dialog wide floats"
			})
			.append(
				$("<h1/>")
					.attr("class","head")
					.text("Olympian settings"),
				$("<div/>").attr("class","sidebar"),
				$("<div/>")
					.attr("class","scrolling-content")
					.append(
						$("<div/>").attr("class","container")
					),
				$("<div/>")
					.attr("class","actions")
					.append(
						$("<button/>")
							.text("Close")
							.click(function() {
								olympus.settings.hide();
							})
					)
			)
			.hide()
	);

	this.settings.init(this);

	// use jqueryui.draggable() to make the help window movable
	$(".floats").draggable({handle:"h1.head"});

	// put the settings icon on the page
	$("span.header_links").first().before(
		olympus.settings.button("olympus")
			.addClass("fa-3x")
			.css({
				"float":"right",
				"margin-left":"5px"
			})
	);

	$.each(this.known_olympians,function(index,olympian) {
		if($.type(olympus[olympian]) === "object") { // olympian is installed
			olympus.settings.init(olympus[olympian]); //if(olympus[olympian].hasOwnProperty("__configurable")) olympus.settings.init(olympus[olympian]);
			olympus[olympian].__init();
		}
	});
};

window.olympus.__configurable = function() {
	function _gen_to_weight_element(type) {
		return $("<span/>")
			.attr("class","sublabel")
			.text(type.ucFirst()+":")
			.add(
				olympus.settings._gen_option({
					option:("to_"+type+"_weight"),
					type:"number",
					value:olympus.settings.get(olympus,("to_"+type+"_weight"))
				})
			);
	}

	return [
		olympus.settings.explain({
			type:"plain",
			desc:
				"Olympus settings affect all Olympian scripts in all tabs.  For instance, if you disable Turkopticon queries "+
				"then every Olympian script will stop querying Turkopticon immediately."
		}),
		olympus.settings.generate({
			option:"query_turkopticon",
			type:"checkbox",
			value:olympus.settings.get(olympus,"query_turkopticon"),
			name:"Query Turkopticon",
			desc:
				"Sometimes, the Turkopticon server goes AWOL and any scripts that request data from it (such as this one) "+
				"will hang for several minutes in the absence of a response.  When this happens, it's best to turn off "+
				"Turkopticon queries for a while.  When Turkopticon queries are disabled, Olympus will continue to use "+
				"cached information as allowed by the following options."
		}),
		olympus.settings.generate({
			option:"use_to_cache",
			type:"checkbox",
			value:olympus.settings.get(olympus,"use_to_cache"),
			name:"Cache TO data",
			desc:
				"After completing a Turkopticon query, Olympus can save that data to your computer to allow for rapid retrieval "+
				"later to speed up the Olympian scripts and prevent a lot of unnecessary queries.  Be aware that this option does "+
				"not control whether or not Olympus uses existing cached data, but instead whether or not Olympus stores new data. "+
				"Disabling this option will not delete the Turkopticon data that Olympus has already cached."
		}),
		olympus.settings.generate({
			option:"to_cache_timeout",
			type:"number",
			value:(olympus.settings.get(olympus,"to_cache_timeout")/3600000),
			name:"TO cache life",
			desc:
				"The number of hours that Olympus will consider cached data recent enough to use that instead of querying "+
				"Turkopticon for it.  Decimals are valid (e.g. 1.5 for 90 minutes) and has a minimum value of 0.5 "+
				"(30 minutes).  If you want to disable caching, use the option above."
		}),
		olympus.settings
			._gen_option_wrapper({
				name:"Turkopticon weights",
				elements:4
			})
			.append(
				_gen_to_weight_element("pay"),
				_gen_to_weight_element("fast"),
				$("<br/>"),
				_gen_to_weight_element("fair").first().css("margin-left","160px").end(),
				_gen_to_weight_element("comm")
			)
			.add(
				olympus.settings._gen_desc({
					desc:
						"The weight of a Turkopticon attribute has a big effect on the final average by making values more important "+
						"(with values greater than 1) or less important (with values between 0 and 1.)  Most turkers choose to stress "+
						"pay and fairness over speed and communication with values like 6, 4, 1, 1 respectively, which makes the "+
						"computed average lean very heavily on the former two attributes and very little on the latter two.  If you do "+
						"not want to weight the Turkopticon attributes, then each of these should be set to 1."
				})
			),
		olympus.settings
			._gen_option_wrapper({
				name:"Bayesian settings",
				elements:3
			})
			.append(
				$("<span/>")
					.attr("class","sublabel")
					.text("Enable:"),
				olympus.settings._gen_option({
					option:"bayesian_to",
					type:"checkbox",
					value:olympus.settings.get(olympus,"bayesian_to")
				}),
				$("<span/>")
					.attr("class","sublabel")
					.text("Average:"),
				olympus.settings._gen_option({
					option:"bayesian_average",
					"type":"number",
					value:olympus.settings.get(olympus,"bayesian_average")
				}),
				$("<br/>"),
				$("<span/>")
					.attr("class","sublabel")
					.text("Reviews:")
					.css("margin-left","262px"),
				olympus.settings._gen_option({
					option:"bayesian_reviews",
					"type":"number",
					value:olympus.settings.get(olympus,"bayesian_reviews")
				})
			)
			.add(
				olympus.settings._gen_desc({
					desc:
						"The simple explanation of what a Bayesian function does is lowering the given average based on a lack of faith "+
						"in the accuracy of the result, which occurs when the average is higher than a specified amount and/or when "+
						"there are very few reviews that factor into that average.  Bayesian functions are mostly guesswork/personal "+
						"preference by design which is the reason you are allowed to tinker with these if you so choose.<br><br>"+
						"The Bayesian average is the point where you doubt the accuracy above that amount.  In a perfect system, "+
						"this would be the average of every input available (in this case, averaging every requester's averages), so in the "+
						"absence of that information we will simply have to guess.  In the case of Turk specifically, we all know that there "+
						"are very few requesters that are not crap, so we doubt glowing reviews simply by knowing this fact.<br><br>"+
						"The Bayesian reviews is the point where we think we have enough data to make reasonable assumptions.  This prevents any "+
						"requester with a low number of reviews from getting a high score.  A requester with a single all fives review will "+
						"get a Bayesian average around 2.50 instead of the 5.00 that Turkopticon assigns them."
				})
			),
		olympus.settings.generate({
			option:"clear_to_cache",
			type:"button",
			action:olympus.utilities.clear_to_cache,
			name:"Clear TO cache",
			desc:
				"The Turkopticon cache is using about <span data-function='to_cache_size'></span>, or approximately "+
				"<span data-function='to_cache_usage'></span> of the available storage for the mturk.com domain."
		})
	];
};

window.olympus.__parse_settings = function(settings) {
	settings.to_cache_timeout = Math.max(settings.to_cache_timeout*3600000,1800000);
	$.each(["pay","fair","fast","comm"],function(index,value) {
		if(settings["to_"+value+"_weight"] < 0) settings["to_"+value+"_weight"] = 0;
	});
	olympus.settings.update(olympus,settings);
};

window.olympus.help = {
	__topics:{},
	add:function(obj) {
		if($.type(obj) === "object" && Object.keys(obj).length) {
			$.each(obj,function(key,val) {
				olympus.help.__topics[key] = val;
			});
		}
	},
	display:function(topic) {
		if(this.has_topic(topic)) {
			$("#olympian_help .explain").html(this.__topics[topic]);
			// parse elements with special functions
			olympus.utilities.parse_deferred_functions($("#olympian_help .explain"));
			// show help window
			$("#olympian_help").show();
		}
	},
	has_topic:function(topic) {
		return this.__topics.hasOwnProperty(topic);
	},
	hide:function() {
		$("#olympian_help").hide();
	}
};

window.olympus.settings = {
	_gen_desc:function(config) {
		return $("<div/>")
			.attr("class","description")
			.append(
				$("<span/>")
					.attr("class","collapsed")
					.html(config.desc)
					.prepend(
						$("<span/>")
							.attr("class","toggle fa fa-lg fa-plus-square-o")
							.click(function() {
								$(this).toggleClass("fa-plus-square-o fa-minus-square-o");
								if($(this).hasClass("fa-plus-square-o")) $(this).parent().addClass("collapsed");
								else $(this).parent().removeClass("collapsed");
							})
					)
			);
	},
	_gen_option:function(config) {
		switch(config.type) {
			case "checkbox": return $("<span/>")
				.attr({
					"class":("fa fa-lg fa-toggle-"+((config.value === true) ? "on" : "off")),
					"id":config.option
				})
				.click(function() {
					$(this).toggleClass("fa-toggle-off fa-toggle-on");
				});
			case "number": return $("<input/>")
				.attr({
					"type":"number",
					"id":config.option
				})
				.val(config.value ? config.value : "");
			case "button": return $("<button/>")
				.attr("id",config.option)
				.text(config.name)
				.click(function() {
					config.action();
				});
			case "dropdown": {
				var options = [];
				$.each(config.selections,function(index,value) {
					options.push(
						$("<option/>")
							.attr("value",olympus.utilities.html_friendly(value))
							.text(value.ucFirst())
					);
				});
				return $("<select/>")
					.attr("id",config.option)
					.append(options)
					.val(config.value);
			}
		}
	},
	_gen_option_wrapper:function(config) {
		return $((config.hasOwnProperty("elements") && config.elements > 1) ? "<div/>" : "<label/>")
			.attr("class","option_container")
			.append(
				$("<b/>")
					.attr("class","name")
					.text(config.name)
			);
	},
	button:function(source) {
		return $("<span/>")
			.attr({
				"class":"fa fa-cogs",
				"id":"open_olympus_settings",
				"title":"Open Olympus settings"
			})
			.click(function() {
				olympus.settings.open(source);
			});
	},
	change_tab:function(tab) {
		var olympian = (tab === "olympus" ? window.olympus : olympus[tab]);

		if($("#olympian_settings .sidebar .tab.active").length) this.commit_page();
		$("#olympian_settings .sidebar .tab").removeClass("active");
		$("#"+tab+"_tab").addClass("active");
		$("#olympian_settings .container").empty().append(
			$("<a/>")
				.attr({
					"class":"olympian_identifier",
					"href":olympian.__href,
					"target":"_blank"
				})
				.text(olympian.__name.ucFirst()+" "+olympian.__version),
			(olympian.hasOwnProperty("__configurable") ? olympian.__configurable() : olympus.settings.explain({
				type:"plain",
				desc:
					"This Olympian is installed, but has no configurable options to appear here."
			}))
		);
		olympus.utilities.parse_deferred_functions($("#olympian_settings .container"));
	},
	commit_page:function() {
		var tab = $("#olympian_settings .sidebar .tab.active").attr("id").slice(0,-4),
			olympian = (tab === "olympus" ? window.olympus : olympus[tab]);
		if($.type(olympian) === "object" && olympian.hasOwnProperty("__parse_settings")) {
			var settings = {};
			$.each($("#olympian_settings .container *[id]"),function(index,$element) {
				$element = $($element);
				switch($element.prop("tagName")) {
					case "INPUT": case "SELECT": {
						settings[$element.attr("id")] = $element.val();
						break;
					}
					case "SPAN": {
						if($element.hasClass("fa-toggle-on") || $element.hasClass("fa-toggle-off")) settings[$element.attr("id")] = $element.hasClass("fa-toggle-on");
						break;
					}
				}
			});
			olympian.__parse_settings(settings);
		}
	},
	explain:function(config) {
		return $("<div/>")
			.attr("class",config.type)
			.html(config.desc);
	},
	generate:function(config) {
		return this._gen_option_wrapper(config)
			.append(this._gen_option(config))
			.add(this._gen_desc(config));
	},
	get:function() {
		if($.type(arguments[0]) === "object" && arguments[0].hasOwnProperty("__name")) {
			var olympian = arguments[0],
				settings = (olympus.utilities.localstorage_obj(olympian.__name+"_settings") || olympian.default_settings);
			if(arguments.length < 2) return settings;
			else if($.type(arguments[1]) === "string") return settings[arguments[1]];
		}
	},
	hide:function() {
		this.commit_page();
		$("#olympian_settings")
			.find(".sidebar .tab").removeClass("active")
			.end().hide();
	},
	init:function(olympian) {
		if($.type(olympian) === "object" && olympian.hasOwnProperty("__name")) {
			// this makes sure that any new options that are added from later updates are automatically
			// loaded into the existing saved settings as their default values
			if(olympian.hasOwnProperty("default_settings")) {
				var settings = olympus.utilities.localstorage_obj(olympian.__name+"_settings"),
					defaults = olympian.default_settings;
				if($.type(settings) === "object") {
					var original_len = Object.keys(settings).length;
					$.each(defaults,function(k,v) {
						if(!settings.hasOwnProperty(k)) settings[k] = v;
					});
					if(Object.keys(settings).length !== original_len) localStorage[olympian.__name+"_settings"] = JSON.stringify(settings); // new options exist
				}
			}
			// add a tab to the settings window for this olympian
			$("#olympian_settings .sidebar").append(
				$("<div/>")
					.attr({
						"class":"tab",
						"id":(olympian.__name+"_tab")
					})
					.text(olympian.__name.ucFirst())
					.click(function() {
						olympus.settings.change_tab($(this).attr("id").slice(0,-4));
					})
			);
		}
	},
	open:function(source) {
		this.change_tab(source);
		$("#olympian_settings").show();
	},
	update:function() {
		if(arguments.length > 1 && $.type(arguments[0]) === "object" && arguments[0].hasOwnProperty("__name")) {
			var olympian = arguments[0],
				settings = (olympus.utilities.localstorage_obj(olympian.__name+"_settings") || olympian.default_settings);
			if($.type(arguments[1]) === "object") {
				$.each(arguments[1],function(key,val) {
					if(settings.hasOwnProperty(key)) settings[key] = val;
				});
			}
			else if($.type(arguments[1]) === "string" && arguments.length > 2) if(settings.hasOwnProperty(arguments[1])) settings[arguments[1]] = arguments[2];

			localStorage[olympian.__name+"_settings"] = JSON.stringify(settings);
		}
	}
};

window.olympus.style = {
	__css:"",
	__commit:function() {
		// retrieve the olympian style node, or create it if it does not yet exist
		var $style_node = $("#olympian_css");
		if(!$style_node.length) {
			$style_node = $("<style/>")
				.attr({
					"data-pantheon":"olympus",
					"id":"olympian_css",
					"type":"text/css"
				});
			$("head").append($style_node);
		}

		// update the olympian style node with the new css
		$style_node.text(this.__css);
	},
	add:function(new_css,tokens) {
		if($.type(tokens) === "object" && Object.keys(tokens).length) new_css = this.expand(new_css,tokens);
		this.__css += new_css;
		this.__commit();
	},
	expand:function(css,tokens) {
		// olympians sometimes use bracketed tokens in their css to allow for centralized
		// style definitions from functions or for swapping values easily
		$.each(tokens,function(key,val) {css = css.replace(new RegExp(("\\["+key+"\\]"),"gi"),val);});
		return css;
	}
};

window.olympus.utilities = {
	datetime:{
		__day_string:function(int) {
			switch(int) {
				case 0: return "Sunday";
				case 1: return "Monday";
				case 2: return "Tuesday";
				case 3: return "Wednesday";
				case 4: return "Thursday";
				case 5: return "Friday";
				case 6: return "Saturday";
			}
		},
		__meridiem:function(int) {
			if(int > 12) return "pm";
			else return "am";
		},
		__meridiem_hour:function(int) {
			if(int > 12) int -= 12;
			return int;
		},
		__month_string:function(int) {
			switch(int) {
				case 0: return "January";
				case 1: return "February";
				case 2: return "March";
				case 3: return "April";
				case 4: return "May";
				case 5: return "June";
				case 6: return "July";
				case 7: return "August";
				case 8: return "September";
				case 9: return "October";
				case 10: return "November";
				case 11: return "December";
			}
		},
		__ordinal:function(int) {
			switch(int) {
				case 1: case 21: case 31: return "st";
				case 2: case 22: return "nd";
				case 3: case 23: return "rd";
			}
			return "th";
		},
		__short_year:function(int) {
			return (""+int).slice(-2);
		},
		getDayString:function() {
			return this.__day_string(this.__date.getDay());
		},
		getMeridiem:function() {
			return this.__meridiem(this.__date.getHours());
		},
		getMeridiemHours:function() {
			return this.__meridiem_hour(this.__date.getHours());
		},
		getMonthString:function() {
			return this.__month_string(this.__date.getMonth());
		},
		getOrdinal:function() {
			return this.__ordinal(this.__date.getDate());
		},
		getShortYear:function() {
			return this.__short_year(this.__date.getFullYear());
		},
		getUTCDayString:function() {
			return this.__day_string(this.__date.getUTCDay());
		},
		getUTCMeridiem:function() {
			return this.__meridiem(this.__date.getUTCHours());
		},
		getUTCMeridiemHours:function() {
			return this.__meridiem_hour(this.__date.getUTCHours());
		},
		getUTCMonthString:function() {
			return this.__month_string(this.__date.getUTCMonth());
		},
		getUTCOrdinal:function() {
			return this.__ordinal(this.__date.getUTCDate());
		},
		getUTCShortYear:function() {
			return this.__short_year(this.__date.getUTCFullYear());
		},
		getTokenizedOutput:function(t) {
			var r = "",
				i = -1;
			while(i++ < t.length) {
				switch(t.charAt(i)) {
					// escape sequence, ignore following character by advancing index beyond it
					case '\\': {r += t.charAt(++i); break;}

					// local year
					case 'y': {r += this.getShortYear(); break;}
					case 'Y': {r += this.__date.getFullYear(); break;}
					// local month
					case 'n': {r += (this.__date.getMonth()+1); break;}
					case 'm': {r += olympus.utilities.pad_string(this.__date.getMonth()+1,2); break;}
					case 'F': {r += this.getMonthString(); break;}
					case 'M': {r += this.getMonthString().slice(0,3); break;}
					// local day
					case 'j': {r += this.__date.getDate(); break;}
					case 'd': {r += olympus.utilities.pad_string(this.__date.getDate(),2); break;}
					case 'l': {r += this.getDayString(); break;}
					case 'D': {r += this.getDayString().slice(0,3); break;}
					case 'S': {r += this.getOrdinal(); break;}
					// local hour
					case 'g': {r += this.getMeridiemHours(); break;}
					case 'h': {r += olympus.utilities.pad_string(this.getMeridiemHours(),2); break;}
					case 'G': {r += this.__date.getHours(); break;}
					case 'H': {r += olympus.utilities.pad_string(this.__date.getHours(),2); break;}
					case 'a': {r += this.getMeridiem(); break;}
					case 'A': {r += this.getMeridiem().toUpperCase(); break;}
					// local minute, second
					case 'i': {r += olympus.utilities.pad_string(this.__date.getMinutes(),2); break;}
					case 's': {r += olympus.utilities.pad_string(this.__date.getSeconds(),2); break;}

					// utc year
					case 'z': {r += this.getUTCShortYear(); break;}
					case 'Z': {r += this.__date.getUTCFullYear(); break;}
					// utc month
					case 'p': {r += (this.__date.getUTCMonth()+1); break;}
					case 'q': {r += olympus.utilities.pad_string(this.__date.getUTCMonth()+1,2); break;}
					case 'T': {r += this.getUTCMonthString(); break;}
					case 'U': {r += this.getUTCMonthString().slice(0,3); break;}
					// utc day
					case 'f': {r += this.__date.getUTCDate(); break;}
					case 'e': {r += olympus.utilities.pad_string(this.__date.getUTCDate(),2); break;}
					case 'k': {r += this.getUTCDayString(); break;}
					case 'E': {r += this.getUTCDayString().slice(0,3); break;}
					case 'R': {r += this.getUTCOrdinal(); break;}
					// utc hour
					case 'b': {r += this.getUTCMeridiemHours(); break;}
					case 'c': {r += olympus.utilities.pad_string(this.getUTCMeridiemHours(),2); break;}
					case 'B': {r += this.__date.getUTCHours(); break;}
					case 'C': {r += olympus.utilities.pad_string(this.__date.getUTCHours()); break;}
					case 'o': {r += this.getUTCMeridiem(); break;}
					case 'O': {r += this.getUTCMeridiem().toUpperCase() ;break;}
					// utc minute, second
					case 'w': {r += olympus.utilities.pad_string(this.__date.getUTCMinutes(),2); break;}
					case 'x': {r += olympus.utilities.pad_string(this.__date.getUTCSeconds(),2); break;}

					default: {r += t.charAt(i); break;}
				}
			}
			return r;
		},
		output:function() {
			if(arguments.length) {
				if(arguments.length > 1) this.__date = new Date(arguments[0]);
				if($.type(this.__date) !== "undefined") return this.getTokenizedOutput(arguments[arguments.length-1]);
			}
		}
	},
	ajax_get:function(mirrors,params,callback,scope) {
		var result = "";

		function exit() {
			if($.type(callback) === "function") callback.call(scope,result);
		}

		function domain_name(s) {
			return s.match(/^https?:\/\/([^/$]+)/i)[1];
		}

		function request(url) {
			$.ajax({
				async:true,
				method:"GET",
				url:(url+params)
			})
			.fail(function() {
				console.log("Mount Olympus get request: attempt to gather data from '"+domain_name(url)+"' mirror failed");
				var idx = (mirrors.indexOf(url)+1);
				if(idx < mirrors.length) {
					console.log("Mount Olympus get request: attempting data request from mirror '"+domain_name(mirrors[idx])+"'...");
					request(mirrors[idx]);
				}
				else {
					console.log("Mount Olympus get request: attempts to gather data from all available mirrors has failed");
					exit();
				}
			})
			.done(function(response) {
				if(response.length) {
					console.log("Mount Olympus get request: query to '"+domain_name(url)+"' was successful");
					result = response;
				}
				exit();
			});
		}

		request(mirrors[0]);
	},
	bkmg:function(bytes) {
		var multiple = 0;
		while(bytes > 1024) {
			multiple++;
			bytes /= 1024;
		}
		return (""+bytes.toFixed(2)+" "+["","kilo","mega","giga"][multiple]+"byte"+olympus.utilities.plural(bytes));
	},
	capsule_info:function($element) {
		function scrape_from_tooltip() {
			var value = "";
			$.each(arguments,function(index,text) {
				var $anchor = $("a[id^='"+text+".tooltip']",$element);
				if($anchor.length) {
					value = $anchor.parent().next().text().collapseWhitespace();
					return false; // the only way to break $.each
				}
			});
			return value;
		}

		var tokens = {
			// basic HIT info that can be scraped off the page
			hit_name:$("a.capsulelink[href='#']",$element).first().text().collapseWhitespace(),
			hit_id:olympus.utilities.href_group_id($("a[href*='roupId=']",$element).first().attr("href") || window.location.href), // groupid does not appear in preview
			hit_desc:(scrape_from_tooltip("description") || "None"), // description does not appear in preview
			hit_time:scrape_from_tooltip("duration_to_complete","time_left"),
			hits_available:scrape_from_tooltip("number_of_hits"),
			hit_reward:$("span.reward",$element).text().collapseWhitespace(),
			requester_name:$("a[href*='selectedSearchType=hitgroups']",$element).first().text().collapseWhitespace(),
			requester_id:olympus.utilities.href_requester_id($("a[href*='requesterId']",$element).first().attr("href"))
		};

		// link properties for convenience, since these are long URLs that only use one bit of previously collected info
		tokens.preview_link = ("https://www.mturk.com/mturk/preview?groupId="+tokens.hit_id);
		tokens.panda_link = ("https://www.mturk.com/mturk/previewandaccept?groupId="+tokens.hit_id);
		tokens.requester_hits = ("https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="+tokens.requester_id);
		tokens.contact_requester = ("https://www.mturk.com/mturk/contact?requesterId="+tokens.requester_id+"&requesterName="+tokens.requester_name);
		tokens.to_reviews = ("https://turkopticon.ucsd.edu/"+tokens.requester_id);

		// parse qualifications
		var $qual_anchor = $("a[id^='qualificationsRequired.tooltip'], a[id^='qualifications.tooltip']",$element).first();
		if($qual_anchor.parent().next().text().collapseWhitespace() === "None") tokens.quals = "None";
		else {
			if($qual_anchor.attr("id").contains("Required")) { // viewing list of HITs
				var quals = [];
				$("tr:not(:first-of-type) td:first-of-type",$qual_anchor.closest("table")).each(function() {quals.push($(this).text().collapseWhitespace());});
				tokens.quals = quals.join("; ");
			}
			else tokens.quals = $qual_anchor.parent().next().text().collapseWhitespace(); // previewing HIT where quals are already semicolon-delimited
		}

		return tokens;
	},
	clear_page:function(title) {
		// when an olympian wants an independent full-page display.  every element added to
		// the top level of the document has a data-pantheon attribute for exactly this purpose:
		// if an element does not have that attribute, it is removed
		$("head")
			.children().not("[data-pantheon]").remove()
			.end().end().append(
				$("<title/>").text(title)
			);
		$("body")
			.removeAttr("onload onLoad")
			.children().not("[data-pantheon]").remove();
	},
	clear_to_cache:function() {
		if(confirm("Are you sure you want to delete the Turkopticon cache?")) localStorage.removeItem("olympian_to_cache");
	},
	dhms:function(secs) {
		// takes a number of seconds (chiefly, hitAutoAppDelayInSeconds) and returns a
		// "friendly" value in seconds, minutes, hours, or days.  has a precision of
		// tenths, e.g. "1.5 days" or "6.7 hours"
		function output(multiple,name) {
			function zeroes(num) {
				// removes ugly trailing zeroes (e.g. "1.0 days" or "2.40 hours")
				return +num.toFixed(1);
			}
			var units = zeroes((secs/multiple));
			return (""+units+" "+name+olympus.utilities.plural(units));
		}

		if($.type(secs) !== "number") secs = Math.round(secs*1);

		if(secs < 60) return output(1,"second");
		else if(secs < 3600) return output(60,"minute");
		else if(secs < 86400) return output(3600,"hour");
		else return output(86400,"day");
	},
	href_group_id:function(href) {
		if($.type(href) === "string") {
			href = href.match(/groupId=([^&\s]+)/i);
			if($.type(href) === "array") return href[1];
		}
	},
	href_id:function(href) {
		// when i don't know which one i have
		return (this.href_requester_id(href) || this.href_group_id(href));
	},
	href_requester_id:function(href) {
		if($.type(href) === "string") {
			href = href.match(/requesterId=([^&\s]+)/i);
			if($.type(href) === "array") return href[1];
		}
	},
	html_friendly:function(string) {
		return string.collapseWhitespace().replace(/\s/g,"_");
	},
	json_obj:function(json) {
		var obj;
		if(typeof json === "string" && json.trim().length) {
			try {obj = JSON.parse(json);}
			catch(e) {console.log("Malformed JSON object.  Error message from JSON library: ["+e.message+"]");}
		}
		return obj;
	},
	localstorage_obj:function(key) {
		var obj = this.json_obj(localStorage.getItem(key));
		if(typeof obj !== "object") localStorage.removeItem(key);
		return obj;
	},
	pad_string:function(string,width,padding,side) {
		string = (""+string);
		var pad_item = (padding || "0"),
			half = ((width-string.length)/2);
		padding = "";
		while((string.length+padding.length) < width) padding = (padding+pad_item);
		if(side === "both") return (padding.slice(0,Math.floor(half))+string+padding.slice(Math.ceil(half)*-1));
		else if(side === "right") return (string+padding).slice(0,width);
		else return (padding+string).slice(width*-1);
	},
	parse_deferred_functions:function($context) {
		$context.find("*[data-function]").each(function() {
			switch($(this).attr("data-function")) {
				case "desc2fa": {
					$(this)
						.addClass("fa fa-fw fa-2x "+olympus.athena.desc2fa($(this).attr("data-args")))
						.removeAttr("data-function data-args");
					break;
				}
				case "feasibility2desc": {
					$(this).replaceWith(olympus.athena.feasibility2desc($(this).attr("data-args")));
					break;
				}
				case "to_cache_size": {
					$(this).replaceWith(
						document.createTextNode(""+olympus.utilities.bkmg(localStorage.olympian_to_cache.length))
					);
					break;
				}
				case "to_cache_usage": {
					$(this).replaceWith(
						document.createTextNode(""+(localStorage.olympian_to_cache.length/10485760).toFixed(2)+"%")
					);
					break;
				}
			}
		});
	},
	plural:function(num) {
		// returns the letter s if the number is not 1.  just for pretty display
		// to say something like "2 widgets" instead of "2 widget"
		if($.type(num) !== "number") num = +num;
		if(num != 1) return "s";
		return "";
	},
	to_average:function(info) {
		function confidence(avg,ttl) {
			var rr = (olympus.settings.get(olympus,"bayesian_reviews")*1);
			return ((ttl/(ttl+rr))*avg+(rr/(ttl+rr))*olympus.settings.get(olympus,"bayesian_average"));
		}

		var sum = 0,
			divisor = 0,
			average = 0;
		$.each(info.attrs,function(key,val) {
			var weight = (olympus.settings.get(olympus,"to_"+key+"_weight")*1),
				total = (val*weight);
			if(total > 0) {
				sum += total;
				divisor += weight;
			}
		});

		average = (sum/divisor);
		return (olympus.settings.get(olympus,"bayesian_to") ? confidence(average,info.reviews) : average);
	},
	turkopticon:function(rids,callback,scope) {
		var to_mirrors = [
				"https://mturk-api.istrack.in/multi-attrs.php?ids=",
				"https://turkopticon.ucsd.edu/api/multi-attrs.php?ids="
			],
			query_rids = [],
			query_result = {},
			deferred_cache = {};

		function exit() {
			cache_commit();
			if($.type(callback) === "function") callback.call(scope,query_result);
		}

		// turkopticon caching functions of this script reduce overhead ajax calls to the api
		// and instead stash data on the local system, if the user has allowed it.  i personally
		// think a couple hundred kilobytes on your computer is well worth the performance boost
		function cache_commit() {
			// commit the deferred information from cache_set
			if(Object.keys(deferred_cache).length) {
				var to_cache = (olympus.utilities.localstorage_obj("olympian_to_cache") || {});
				$.each(deferred_cache,function(rid,attrs) {
					if($.type(attrs) !== "object") attrs = {};
					attrs.cache_time = new Date().getTime();
					to_cache[rid] = attrs;
				});
				localStorage.olympian_to_cache = JSON.stringify(to_cache);
			}
		}

		function cache_get(rid) {
			var to_cache = olympus.utilities.localstorage_obj("olympian_to_cache");
			if($.type(to_cache) === "object" && to_cache.hasOwnProperty(rid)) {
				var attrs = to_cache[rid];
				// when turkopticon is disabled, any data is better than no data so cached information is used
				// regardless of age.  otherwise, if turkopticon is enabled, there is a maximum age imposed
				// on the cached results as defined in the options that the user has set
				if(!olympus.settings.get(olympus,"query_turkopticon") || new Date().getTime() - (attrs.cache_time*1) < olympus.settings.get(olympus,"to_cache_timeout")) return attrs;
			}
		}

		function cache_set(rid,attrs) {
			// so that all new cached data is stored once instead of firing a storage event infinity times
			if(olympus.settings.get(olympus,"use_to_cache")) deferred_cache[rid] = attrs;
		}

		// check the cache for relevant data we can use and query for the rest
		$.each(rids,function(k,v) {
			var cached = cache_get(v);
			if($.type(cached) === "object") query_result[v] = cached;
			else query_rids.push(v);
		});
		var num_cached = Object.keys(query_result).length,
			num_queried = query_rids.length;
		console.log("Mount Olympus Turkopticon: "+(num_cached > 0 ? ("using cached data for "+num_cached+" requesters") : "no available or timely cached data")+"; "+(num_queried > 0 ? ("query required for "+num_queried+" requesters") : "no queries necessary"));
		if(olympus.settings.get(olympus,"query_turkopticon") && query_rids.length) {
			this.ajax_get(to_mirrors,query_rids.join(","),function(response) {
				var jsobj = olympus.utilities.json_obj(response);
				if($.type(jsobj) === "object") {
					$.each(jsobj,function(rid,attrs) {
						cache_set(rid,attrs);
						query_result[rid] = attrs;
					});
				}
				else console.log("Mount Olympus Turkopticon: query was successful but the response was malformed");
				exit();
			});
		}
		else exit();
	}
};

$(document).ready(function() {
	olympus.__init();
});