Mount Olympus

Common features shared amongst all Olympian scripts.

  1. // ==UserScript==
  2. // @name Mount Olympus
  3. // @namespace mobiusevalon.tibbius.com
  4. // @version 2.0-7
  5. // @author Mobius Evalon
  6. // @description Common features shared amongst all Olympian scripts.
  7. // @license Creative Commons Attribution-ShareAlike 4.0; http://creativecommons.org/licenses/by-sa/4.0/
  8. // @require https://code.jquery.com/jquery-1.12.4.min.js
  9. // @require https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.26.0/js/jquery.tablesorter.min.js
  11. // @include /^https{0,1}:\/\/\w{0,}\.?mturk\.com.+/
  12. // @include /^https{0,1}:\/\/\w*\.amazon\.com\/ap\/signin.*(?:openid\.assoc_handle|pf_rd_i)=amzn_mturk/
  13. // @exclude /&hit_scraper$/
  14. // @exclude /\/HM$/
  15. // @grant none
  16. // ==/UserScript==
  17.  
  18. if(window.olympus === undefined) window.olympus = {};
  19.  
  20. // there is a reason they are initialized in this order
  21. window.olympus.__name = "olympus";
  22. window.olympus.__version = "2.0-7";
  23. window.olympus.__href = "https://greasyfork.org/en/scripts/23092-mount-olympus";
  24. window.olympus.known_olympians = ["harpocrates","hermes","artemis","athena"];
  25. window.olympus.default_settings = {
  26. query_turkopticon:true,
  27. use_to_cache:true,
  28. to_cache_timeout:10800000,
  29. to_pay_weight:6.5,
  30. to_fair_weight:4,
  31. to_fast_weight:1,
  32. to_comm_weight:0.5,
  33. bayesian_to:true,
  34. bayesian_reviews:75,
  35. bayesian_average:2.75
  36. };
  37.  
  38. window.olympus.__init = function() {
  39. console.log("olympus init");
  40.  
  41. Array.prototype.contains = function(item) {
  42. return (this.indexOf(item) > -1);
  43. };
  44.  
  45. String.prototype.collapseWhitespace = function() {
  46. return this.replace(/\s{2,}/g," ").trim();
  47. };
  48.  
  49. String.prototype.contains = function(substring) {
  50. return (this.indexOf(substring) > -1);
  51. };
  52.  
  53. String.prototype.ucFirst = function() {
  54. return (this.charAt(0).toUpperCase()+this.slice(1));
  55. };
  56.  
  57. olympus.style.add(
  58. "#javascriptDependentFunctionality {display: block !important;}"+
  59. ".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;} "+
  60. ".dialog.narrow {width: 300px; min-width: 300px;} "+
  61. ".dialog.wide {width: 550px; min-width: 550px;} "+
  62. ".dialog .scrolling-content {max-height: 350px; overflow-y: auto;} "+
  63. ".dialog .actions {margin: 10px auto; padding: 0px; text-align: center; display: block;} "+
  64. ".dialog .actions input:not(:last-of-type) {margin-right: 15px;} "+
  65. ".dialog .head {padding: 0px; margin: 10px auto; font-size: 175%; font-weight: bold; width: 100%; text-align: center; cursor: move;} "+
  66. "#olympian_help p.inset {margin-left: 25px;}"+
  67. "#olympian_help p.inset b {margin-left: -25px; display: block;}"+
  68. "#olympian_settings .sidebar {float: left; min-width: 100px; padding-left: 5px;}"+
  69. "#olympian_settings .sidebar .tab {border-radius: 8px 0 0 8px; height: 30px; line-height: 30px; text-align: center; font-weight: bold; cursor: pointer;}"+
  70. "#olympian_settings .sidebar .tab.active {background-color: #88c1de;}"+
  71. "#olympian_settings .container {background-color: #88c1de; padding: 5px; margin-right: 5px; min-height: 150px;}"+
  72. "#olympian_settings .subheader {font-size: 125%; font-weight: bold; margin-bottom: 10px; background-color: #96d5f5; padding: 5px 0px 5px 5px;}"+
  73. "#olympian_settings .container .option_container {display: block;}"+
  74. "#olympian_settings .container .option_container b.name {width: 150px; display: inline-block; margin-right: 10px;}"+
  75. "#olympian_settings .container .description {margin-top: 5px; margin-bottom: 10px; font-size: 90%; padding-left: 10px;}"+
  76. "#olympian_settings .container .description .toggle {margin-right: 5px;}"+
  77. "#olympian_settings .container .description .collapsed {display: inline-block; width: 350px; height: 16px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;}"+
  78. "#olympian_settings .container .sublabel {width: 50px; display: inline-block; margin-right: 5px; text-align: right;}"+
  79. "#olympian_settings .container .fa-toggle-off, #olympian_settings .container .fa-toggle-on {width: 47px; text-align: center;}"+
  80. "#olympian_settings .container .fa-toggle-off {color: #000;}"+
  81. "#olympian_settings .container .fa-toggle-on {color: #fff;}"+
  82. "#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;}"+
  83. "#olympian_settings .container input[type='number'] {width: 45px;}"+
  84. "#olympian_settings .container .plain {margin-bottom: 20px;}"+
  85. "#open_olympus_settings {cursor: pointer;}"+
  86. ".olympian_identifier {display: block; margin: 5px auto 10px auto; text-align: center; font-size: 150%; font-weight: bold;}"+
  87. ".anim_pulse {animation-name: anim_pulse; animation-duration: 350ms; animation-iteration-count: infinite; animation-timing-function: linear; animation-direction: alternate;}"+
  88. "@keyframes anim_pulse {from {opacity: 1;} to {opacity: 0.25;}} "
  89. );
  90.  
  91. // append the fontawesome stylesheet to the page if it does not exist
  92. if(!$("link[rel='stylesheet'][href$='font-awesome.min.css']").length) $("head").append(
  93. $("<link/>")
  94. .attr({
  95. "data-pantheon":"olympus",
  96. "rel":"stylesheet",
  97. "href":"https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"
  98. })
  99. );
  100.  
  101. // append the help window to the document
  102. $("body").append(
  103. $("<div/>")
  104. .attr({
  105. "data-pantheon":"olympus",
  106. "id":"olympian_help",
  107. "class":"dialog wide floats"
  108. })
  109. .append(
  110. $("<h1/>")
  111. .attr("class","head")
  112. .text("Olympian help"),
  113. $("<div/>")
  114. .attr("class","scrolling-content")
  115. .append(
  116. $("<div/>").attr("class","explain")
  117. ),
  118. $("<div/>")
  119. .attr("class","actions")
  120. .append(
  121. $("<button/>")
  122. .text("Close")
  123. .click(function() {
  124. olympus.help.hide();
  125. })
  126. )
  127. )
  128. .hide(),
  129. $("<div/>")
  130. .attr({
  131. "data-pantheon":"olympus",
  132. "id":"olympian_settings",
  133. "class":"dialog wide floats"
  134. })
  135. .append(
  136. $("<h1/>")
  137. .attr("class","head")
  138. .text("Olympian settings"),
  139. $("<div/>").attr("class","sidebar"),
  140. $("<div/>")
  141. .attr("class","scrolling-content")
  142. .append(
  143. $("<div/>").attr("class","container")
  144. ),
  145. $("<div/>")
  146. .attr("class","actions")
  147. .append(
  148. $("<button/>")
  149. .text("Close")
  150. .click(function() {
  151. olympus.settings.hide();
  152. })
  153. )
  154. )
  155. .hide()
  156. );
  157.  
  158. this.settings.init(this);
  159.  
  160. // use jqueryui.draggable() to make the help window movable
  161. $(".floats").draggable({handle:"h1.head"});
  162.  
  163. // put the settings icon on the page
  164. $("span.header_links").first().before(
  165. olympus.settings.button("olympus")
  166. .addClass("fa-3x")
  167. .css({
  168. "float":"right",
  169. "margin-left":"5px"
  170. })
  171. );
  172.  
  173. $.each(this.known_olympians,function(index,olympian) {
  174. if($.type(olympus[olympian]) === "object") { // olympian is installed
  175. olympus.settings.init(olympus[olympian]); //if(olympus[olympian].hasOwnProperty("__configurable")) olympus.settings.init(olympus[olympian]);
  176. olympus[olympian].__init();
  177. }
  178. });
  179. };
  180.  
  181. window.olympus.__configurable = function() {
  182. function _gen_to_weight_element(type) {
  183. return $("<span/>")
  184. .attr("class","sublabel")
  185. .text(type.ucFirst()+":")
  186. .add(
  187. olympus.settings._gen_option({
  188. option:("to_"+type+"_weight"),
  189. type:"number",
  190. value:olympus.settings.get(olympus,("to_"+type+"_weight"))
  191. })
  192. );
  193. }
  194.  
  195. return [
  196. olympus.settings.explain({
  197. type:"plain",
  198. desc:
  199. "Olympus settings affect all Olympian scripts in all tabs. For instance, if you disable Turkopticon queries "+
  200. "then every Olympian script will stop querying Turkopticon immediately."
  201. }),
  202. olympus.settings.generate({
  203. option:"query_turkopticon",
  204. type:"checkbox",
  205. value:olympus.settings.get(olympus,"query_turkopticon"),
  206. name:"Query Turkopticon",
  207. desc:
  208. "Sometimes, the Turkopticon server goes AWOL and any scripts that request data from it (such as this one) "+
  209. "will hang for several minutes in the absence of a response. When this happens, it's best to turn off "+
  210. "Turkopticon queries for a while. When Turkopticon queries are disabled, Olympus will continue to use "+
  211. "cached information as allowed by the following options."
  212. }),
  213. olympus.settings.generate({
  214. option:"use_to_cache",
  215. type:"checkbox",
  216. value:olympus.settings.get(olympus,"use_to_cache"),
  217. name:"Cache TO data",
  218. desc:
  219. "After completing a Turkopticon query, Olympus can save that data to your computer to allow for rapid retrieval "+
  220. "later to speed up the Olympian scripts and prevent a lot of unnecessary queries. Be aware that this option does "+
  221. "not control whether or not Olympus uses existing cached data, but instead whether or not Olympus stores new data. "+
  222. "Disabling this option will not delete the Turkopticon data that Olympus has already cached."
  223. }),
  224. olympus.settings.generate({
  225. option:"to_cache_timeout",
  226. type:"number",
  227. value:(olympus.settings.get(olympus,"to_cache_timeout")/3600000),
  228. name:"TO cache life",
  229. desc:
  230. "The number of hours that Olympus will consider cached data recent enough to use that instead of querying "+
  231. "Turkopticon for it. Decimals are valid (e.g. 1.5 for 90 minutes) and has a minimum value of 0.5 "+
  232. "(30 minutes). If you want to disable caching, use the option above."
  233. }),
  234. olympus.settings
  235. ._gen_option_wrapper({
  236. name:"Turkopticon weights",
  237. elements:4
  238. })
  239. .append(
  240. _gen_to_weight_element("pay"),
  241. _gen_to_weight_element("fast"),
  242. $("<br/>"),
  243. _gen_to_weight_element("fair").first().css("margin-left","160px").end(),
  244. _gen_to_weight_element("comm")
  245. )
  246. .add(
  247. olympus.settings._gen_desc({
  248. desc:
  249. "The weight of a Turkopticon attribute has a big effect on the final average by making values more important "+
  250. "(with values greater than 1) or less important (with values between 0 and 1.) Most turkers choose to stress "+
  251. "pay and fairness over speed and communication with values like 6, 4, 1, 1 respectively, which makes the "+
  252. "computed average lean very heavily on the former two attributes and very little on the latter two. If you do "+
  253. "not want to weight the Turkopticon attributes, then each of these should be set to 1."
  254. })
  255. ),
  256. olympus.settings
  257. ._gen_option_wrapper({
  258. name:"Bayesian settings",
  259. elements:3
  260. })
  261. .append(
  262. $("<span/>")
  263. .attr("class","sublabel")
  264. .text("Enable:"),
  265. olympus.settings._gen_option({
  266. option:"bayesian_to",
  267. type:"checkbox",
  268. value:olympus.settings.get(olympus,"bayesian_to")
  269. }),
  270. $("<span/>")
  271. .attr("class","sublabel")
  272. .text("Average:"),
  273. olympus.settings._gen_option({
  274. option:"bayesian_average",
  275. "type":"number",
  276. value:olympus.settings.get(olympus,"bayesian_average")
  277. }),
  278. $("<br/>"),
  279. $("<span/>")
  280. .attr("class","sublabel")
  281. .text("Reviews:")
  282. .css("margin-left","262px"),
  283. olympus.settings._gen_option({
  284. option:"bayesian_reviews",
  285. "type":"number",
  286. value:olympus.settings.get(olympus,"bayesian_reviews")
  287. })
  288. )
  289. .add(
  290. olympus.settings._gen_desc({
  291. desc:
  292. "The simple explanation of what a Bayesian function does is lowering the given average based on a lack of faith "+
  293. "in the accuracy of the result, which occurs when the average is higher than a specified amount and/or when "+
  294. "there are very few reviews that factor into that average. Bayesian functions are mostly guesswork/personal "+
  295. "preference by design which is the reason you are allowed to tinker with these if you so choose.<br><br>"+
  296. "The Bayesian average is the point where you doubt the accuracy above that amount. In a perfect system, "+
  297. "this would be the average of every input available (in this case, averaging every requester's averages), so in the "+
  298. "absence of that information we will simply have to guess. In the case of Turk specifically, we all know that there "+
  299. "are very few requesters that are not crap, so we doubt glowing reviews simply by knowing this fact.<br><br>"+
  300. "The Bayesian reviews is the point where we think we have enough data to make reasonable assumptions. This prevents any "+
  301. "requester with a low number of reviews from getting a high score. A requester with a single all fives review will "+
  302. "get a Bayesian average around 2.50 instead of the 5.00 that Turkopticon assigns them."
  303. })
  304. ),
  305. olympus.settings.generate({
  306. option:"clear_to_cache",
  307. type:"button",
  308. action:olympus.utilities.clear_to_cache,
  309. name:"Clear TO cache",
  310. desc:
  311. "The Turkopticon cache is using about <span data-function='to_cache_size'></span>, or approximately "+
  312. "<span data-function='to_cache_usage'></span> of the available storage for the mturk.com domain."
  313. })
  314. ];
  315. };
  316.  
  317. window.olympus.__parse_settings = function(settings) {
  318. settings.to_cache_timeout = Math.max(settings.to_cache_timeout*3600000,1800000);
  319. $.each(["pay","fair","fast","comm"],function(index,value) {
  320. if(settings["to_"+value+"_weight"] < 0) settings["to_"+value+"_weight"] = 0;
  321. });
  322. olympus.settings.update(olympus,settings);
  323. };
  324.  
  325. window.olympus.help = {
  326. __topics:{},
  327. add:function(obj) {
  328. if($.type(obj) === "object" && Object.keys(obj).length) {
  329. $.each(obj,function(key,val) {
  330. olympus.help.__topics[key] = val;
  331. });
  332. }
  333. },
  334. display:function(topic) {
  335. if(this.has_topic(topic)) {
  336. $("#olympian_help .explain").html(this.__topics[topic]);
  337. // parse elements with special functions
  338. olympus.utilities.parse_deferred_functions($("#olympian_help .explain"));
  339. // show help window
  340. $("#olympian_help").show();
  341. }
  342. },
  343. has_topic:function(topic) {
  344. return this.__topics.hasOwnProperty(topic);
  345. },
  346. hide:function() {
  347. $("#olympian_help").hide();
  348. }
  349. };
  350.  
  351. window.olympus.settings = {
  352. _gen_desc:function(config) {
  353. return $("<div/>")
  354. .attr("class","description")
  355. .append(
  356. $("<span/>")
  357. .attr("class","collapsed")
  358. .html(config.desc)
  359. .prepend(
  360. $("<span/>")
  361. .attr("class","toggle fa fa-lg fa-plus-square-o")
  362. .click(function() {
  363. $(this).toggleClass("fa-plus-square-o fa-minus-square-o");
  364. if($(this).hasClass("fa-plus-square-o")) $(this).parent().addClass("collapsed");
  365. else $(this).parent().removeClass("collapsed");
  366. })
  367. )
  368. );
  369. },
  370. _gen_option:function(config) {
  371. switch(config.type) {
  372. case "checkbox": return $("<span/>")
  373. .attr({
  374. "class":("fa fa-lg fa-toggle-"+((config.value === true) ? "on" : "off")),
  375. "id":config.option
  376. })
  377. .click(function() {
  378. $(this).toggleClass("fa-toggle-off fa-toggle-on");
  379. });
  380. case "number": return $("<input/>")
  381. .attr({
  382. "type":"number",
  383. "id":config.option
  384. })
  385. .val(config.value ? config.value : "");
  386. case "button": return $("<button/>")
  387. .attr("id",config.option)
  388. .text(config.name)
  389. .click(function() {
  390. config.action();
  391. });
  392. case "dropdown": {
  393. var options = [];
  394. $.each(config.selections,function(index,value) {
  395. options.push(
  396. $("<option/>")
  397. .attr("value",olympus.utilities.html_friendly(value))
  398. .text(value.ucFirst())
  399. );
  400. });
  401. return $("<select/>")
  402. .attr("id",config.option)
  403. .append(options)
  404. .val(config.value);
  405. }
  406. }
  407. },
  408. _gen_option_wrapper:function(config) {
  409. return $((config.hasOwnProperty("elements") && config.elements > 1) ? "<div/>" : "<label/>")
  410. .attr("class","option_container")
  411. .append(
  412. $("<b/>")
  413. .attr("class","name")
  414. .text(config.name)
  415. );
  416. },
  417. button:function(source) {
  418. return $("<span/>")
  419. .attr({
  420. "class":"fa fa-cogs",
  421. "id":"open_olympus_settings",
  422. "title":"Open Olympus settings"
  423. })
  424. .click(function() {
  425. olympus.settings.open(source);
  426. });
  427. },
  428. change_tab:function(tab) {
  429. var olympian = (tab === "olympus" ? window.olympus : olympus[tab]);
  430.  
  431. if($("#olympian_settings .sidebar .tab.active").length) this.commit_page();
  432. $("#olympian_settings .sidebar .tab").removeClass("active");
  433. $("#"+tab+"_tab").addClass("active");
  434. $("#olympian_settings .container").empty().append(
  435. $("<a/>")
  436. .attr({
  437. "class":"olympian_identifier",
  438. "href":olympian.__href,
  439. "target":"_blank"
  440. })
  441. .text(olympian.__name.ucFirst()+" "+olympian.__version),
  442. (olympian.hasOwnProperty("__configurable") ? olympian.__configurable() : olympus.settings.explain({
  443. type:"plain",
  444. desc:
  445. "This Olympian is installed, but has no configurable options to appear here."
  446. }))
  447. );
  448. olympus.utilities.parse_deferred_functions($("#olympian_settings .container"));
  449. },
  450. commit_page:function() {
  451. var tab = $("#olympian_settings .sidebar .tab.active").attr("id").slice(0,-4),
  452. olympian = (tab === "olympus" ? window.olympus : olympus[tab]);
  453. if($.type(olympian) === "object" && olympian.hasOwnProperty("__parse_settings")) {
  454. var settings = {};
  455. $.each($("#olympian_settings .container *[id]"),function(index,$element) {
  456. $element = $($element);
  457. switch($element.prop("tagName")) {
  458. case "INPUT": case "SELECT": {
  459. settings[$element.attr("id")] = $element.val();
  460. break;
  461. }
  462. case "SPAN": {
  463. if($element.hasClass("fa-toggle-on") || $element.hasClass("fa-toggle-off")) settings[$element.attr("id")] = $element.hasClass("fa-toggle-on");
  464. break;
  465. }
  466. }
  467. });
  468. olympian.__parse_settings(settings);
  469. }
  470. },
  471. explain:function(config) {
  472. return $("<div/>")
  473. .attr("class",config.type)
  474. .html(config.desc);
  475. },
  476. generate:function(config) {
  477. return this._gen_option_wrapper(config)
  478. .append(this._gen_option(config))
  479. .add(this._gen_desc(config));
  480. },
  481. get:function() {
  482. if($.type(arguments[0]) === "object" && arguments[0].hasOwnProperty("__name")) {
  483. var olympian = arguments[0],
  484. settings = (olympus.utilities.localstorage_obj(olympian.__name+"_settings") || olympian.default_settings);
  485. if(arguments.length < 2) return settings;
  486. else if($.type(arguments[1]) === "string") return settings[arguments[1]];
  487. }
  488. },
  489. hide:function() {
  490. this.commit_page();
  491. $("#olympian_settings")
  492. .find(".sidebar .tab").removeClass("active")
  493. .end().hide();
  494. },
  495. init:function(olympian) {
  496. if($.type(olympian) === "object" && olympian.hasOwnProperty("__name")) {
  497. // this makes sure that any new options that are added from later updates are automatically
  498. // loaded into the existing saved settings as their default values
  499. if(olympian.hasOwnProperty("default_settings")) {
  500. var settings = olympus.utilities.localstorage_obj(olympian.__name+"_settings"),
  501. defaults = olympian.default_settings;
  502. if($.type(settings) === "object") {
  503. var original_len = Object.keys(settings).length;
  504. $.each(defaults,function(k,v) {
  505. if(!settings.hasOwnProperty(k)) settings[k] = v;
  506. });
  507. if(Object.keys(settings).length !== original_len) localStorage[olympian.__name+"_settings"] = JSON.stringify(settings); // new options exist
  508. }
  509. }
  510. // add a tab to the settings window for this olympian
  511. $("#olympian_settings .sidebar").append(
  512. $("<div/>")
  513. .attr({
  514. "class":"tab",
  515. "id":(olympian.__name+"_tab")
  516. })
  517. .text(olympian.__name.ucFirst())
  518. .click(function() {
  519. olympus.settings.change_tab($(this).attr("id").slice(0,-4));
  520. })
  521. );
  522. }
  523. },
  524. open:function(source) {
  525. this.change_tab(source);
  526. $("#olympian_settings").show();
  527. },
  528. update:function() {
  529. if(arguments.length > 1 && $.type(arguments[0]) === "object" && arguments[0].hasOwnProperty("__name")) {
  530. var olympian = arguments[0],
  531. settings = (olympus.utilities.localstorage_obj(olympian.__name+"_settings") || olympian.default_settings);
  532. if($.type(arguments[1]) === "object") {
  533. $.each(arguments[1],function(key,val) {
  534. if(settings.hasOwnProperty(key)) settings[key] = val;
  535. });
  536. }
  537. else if($.type(arguments[1]) === "string" && arguments.length > 2) if(settings.hasOwnProperty(arguments[1])) settings[arguments[1]] = arguments[2];
  538.  
  539. localStorage[olympian.__name+"_settings"] = JSON.stringify(settings);
  540. }
  541. }
  542. };
  543.  
  544. window.olympus.style = {
  545. __css:"",
  546. __commit:function() {
  547. // retrieve the olympian style node, or create it if it does not yet exist
  548. var $style_node = $("#olympian_css");
  549. if(!$style_node.length) {
  550. $style_node = $("<style/>")
  551. .attr({
  552. "data-pantheon":"olympus",
  553. "id":"olympian_css",
  554. "type":"text/css"
  555. });
  556. $("head").append($style_node);
  557. }
  558.  
  559. // update the olympian style node with the new css
  560. $style_node.text(this.__css);
  561. },
  562. add:function(new_css,tokens) {
  563. if($.type(tokens) === "object" && Object.keys(tokens).length) new_css = this.expand(new_css,tokens);
  564. this.__css += new_css;
  565. this.__commit();
  566. },
  567. expand:function(css,tokens) {
  568. // olympians sometimes use bracketed tokens in their css to allow for centralized
  569. // style definitions from functions or for swapping values easily
  570. $.each(tokens,function(key,val) {css = css.replace(new RegExp(("\\["+key+"\\]"),"gi"),val);});
  571. return css;
  572. }
  573. };
  574.  
  575. window.olympus.utilities = {
  576. datetime:{
  577. __day_string:function(int) {
  578. switch(int) {
  579. case 0: return "Sunday";
  580. case 1: return "Monday";
  581. case 2: return "Tuesday";
  582. case 3: return "Wednesday";
  583. case 4: return "Thursday";
  584. case 5: return "Friday";
  585. case 6: return "Saturday";
  586. }
  587. },
  588. __meridiem:function(int) {
  589. if(int > 12) return "pm";
  590. else return "am";
  591. },
  592. __meridiem_hour:function(int) {
  593. if(int > 12) int -= 12;
  594. return int;
  595. },
  596. __month_string:function(int) {
  597. switch(int) {
  598. case 0: return "January";
  599. case 1: return "February";
  600. case 2: return "March";
  601. case 3: return "April";
  602. case 4: return "May";
  603. case 5: return "June";
  604. case 6: return "July";
  605. case 7: return "August";
  606. case 8: return "September";
  607. case 9: return "October";
  608. case 10: return "November";
  609. case 11: return "December";
  610. }
  611. },
  612. __ordinal:function(int) {
  613. switch(int) {
  614. case 1: case 21: case 31: return "st";
  615. case 2: case 22: return "nd";
  616. case 3: case 23: return "rd";
  617. }
  618. return "th";
  619. },
  620. __short_year:function(int) {
  621. return (""+int).slice(-2);
  622. },
  623. getDayString:function() {
  624. return this.__day_string(this.__date.getDay());
  625. },
  626. getMeridiem:function() {
  627. return this.__meridiem(this.__date.getHours());
  628. },
  629. getMeridiemHours:function() {
  630. return this.__meridiem_hour(this.__date.getHours());
  631. },
  632. getMonthString:function() {
  633. return this.__month_string(this.__date.getMonth());
  634. },
  635. getOrdinal:function() {
  636. return this.__ordinal(this.__date.getDate());
  637. },
  638. getShortYear:function() {
  639. return this.__short_year(this.__date.getFullYear());
  640. },
  641. getUTCDayString:function() {
  642. return this.__day_string(this.__date.getUTCDay());
  643. },
  644. getUTCMeridiem:function() {
  645. return this.__meridiem(this.__date.getUTCHours());
  646. },
  647. getUTCMeridiemHours:function() {
  648. return this.__meridiem_hour(this.__date.getUTCHours());
  649. },
  650. getUTCMonthString:function() {
  651. return this.__month_string(this.__date.getUTCMonth());
  652. },
  653. getUTCOrdinal:function() {
  654. return this.__ordinal(this.__date.getUTCDate());
  655. },
  656. getUTCShortYear:function() {
  657. return this.__short_year(this.__date.getUTCFullYear());
  658. },
  659. getTokenizedOutput:function(t) {
  660. var r = "",
  661. i = -1;
  662. while(i++ < t.length) {
  663. switch(t.charAt(i)) {
  664. // escape sequence, ignore following character by advancing index beyond it
  665. case '\\': {r += t.charAt(++i); break;}
  666.  
  667. // local year
  668. case 'y': {r += this.getShortYear(); break;}
  669. case 'Y': {r += this.__date.getFullYear(); break;}
  670. // local month
  671. case 'n': {r += (this.__date.getMonth()+1); break;}
  672. case 'm': {r += olympus.utilities.pad_string(this.__date.getMonth()+1,2); break;}
  673. case 'F': {r += this.getMonthString(); break;}
  674. case 'M': {r += this.getMonthString().slice(0,3); break;}
  675. // local day
  676. case 'j': {r += this.__date.getDate(); break;}
  677. case 'd': {r += olympus.utilities.pad_string(this.__date.getDate(),2); break;}
  678. case 'l': {r += this.getDayString(); break;}
  679. case 'D': {r += this.getDayString().slice(0,3); break;}
  680. case 'S': {r += this.getOrdinal(); break;}
  681. // local hour
  682. case 'g': {r += this.getMeridiemHours(); break;}
  683. case 'h': {r += olympus.utilities.pad_string(this.getMeridiemHours(),2); break;}
  684. case 'G': {r += this.__date.getHours(); break;}
  685. case 'H': {r += olympus.utilities.pad_string(this.__date.getHours(),2); break;}
  686. case 'a': {r += this.getMeridiem(); break;}
  687. case 'A': {r += this.getMeridiem().toUpperCase(); break;}
  688. // local minute, second
  689. case 'i': {r += olympus.utilities.pad_string(this.__date.getMinutes(),2); break;}
  690. case 's': {r += olympus.utilities.pad_string(this.__date.getSeconds(),2); break;}
  691.  
  692. // utc year
  693. case 'z': {r += this.getUTCShortYear(); break;}
  694. case 'Z': {r += this.__date.getUTCFullYear(); break;}
  695. // utc month
  696. case 'p': {r += (this.__date.getUTCMonth()+1); break;}
  697. case 'q': {r += olympus.utilities.pad_string(this.__date.getUTCMonth()+1,2); break;}
  698. case 'T': {r += this.getUTCMonthString(); break;}
  699. case 'U': {r += this.getUTCMonthString().slice(0,3); break;}
  700. // utc day
  701. case 'f': {r += this.__date.getUTCDate(); break;}
  702. case 'e': {r += olympus.utilities.pad_string(this.__date.getUTCDate(),2); break;}
  703. case 'k': {r += this.getUTCDayString(); break;}
  704. case 'E': {r += this.getUTCDayString().slice(0,3); break;}
  705. case 'R': {r += this.getUTCOrdinal(); break;}
  706. // utc hour
  707. case 'b': {r += this.getUTCMeridiemHours(); break;}
  708. case 'c': {r += olympus.utilities.pad_string(this.getUTCMeridiemHours(),2); break;}
  709. case 'B': {r += this.__date.getUTCHours(); break;}
  710. case 'C': {r += olympus.utilities.pad_string(this.__date.getUTCHours()); break;}
  711. case 'o': {r += this.getUTCMeridiem(); break;}
  712. case 'O': {r += this.getUTCMeridiem().toUpperCase() ;break;}
  713. // utc minute, second
  714. case 'w': {r += olympus.utilities.pad_string(this.__date.getUTCMinutes(),2); break;}
  715. case 'x': {r += olympus.utilities.pad_string(this.__date.getUTCSeconds(),2); break;}
  716.  
  717. default: {r += t.charAt(i); break;}
  718. }
  719. }
  720. return r;
  721. },
  722. output:function() {
  723. if(arguments.length) {
  724. if(arguments.length > 1) this.__date = new Date(arguments[0]);
  725. if($.type(this.__date) !== "undefined") return this.getTokenizedOutput(arguments[arguments.length-1]);
  726. }
  727. }
  728. },
  729. ajax_get:function(mirrors,params,callback,scope) {
  730. var result = "";
  731.  
  732. function exit() {
  733. if($.type(callback) === "function") callback.call(scope,result);
  734. }
  735.  
  736. function domain_name(s) {
  737. return s.match(/^https?:\/\/([^/$]+)/i)[1];
  738. }
  739.  
  740. function request(url) {
  741. $.ajax({
  742. async:true,
  743. method:"GET",
  744. url:(url+params)
  745. })
  746. .fail(function() {
  747. console.log("Mount Olympus get request: attempt to gather data from '"+domain_name(url)+"' mirror failed");
  748. var idx = (mirrors.indexOf(url)+1);
  749. if(idx < mirrors.length) {
  750. console.log("Mount Olympus get request: attempting data request from mirror '"+domain_name(mirrors[idx])+"'...");
  751. request(mirrors[idx]);
  752. }
  753. else {
  754. console.log("Mount Olympus get request: attempts to gather data from all available mirrors has failed");
  755. exit();
  756. }
  757. })
  758. .done(function(response) {
  759. if(response.length) {
  760. console.log("Mount Olympus get request: query to '"+domain_name(url)+"' was successful");
  761. result = response;
  762. }
  763. exit();
  764. });
  765. }
  766.  
  767. request(mirrors[0]);
  768. },
  769. bkmg:function(bytes) {
  770. var multiple = 0;
  771. while(bytes > 1024) {
  772. multiple++;
  773. bytes /= 1024;
  774. }
  775. return (""+bytes.toFixed(2)+" "+["","kilo","mega","giga"][multiple]+"byte"+olympus.utilities.plural(bytes));
  776. },
  777. capsule_info:function($element) {
  778. function scrape_from_tooltip() {
  779. var value = "";
  780. $.each(arguments,function(index,text) {
  781. var $anchor = $("a[id^='"+text+".tooltip']",$element);
  782. if($anchor.length) {
  783. value = $anchor.parent().next().text().collapseWhitespace();
  784. return false; // the only way to break $.each
  785. }
  786. });
  787. return value;
  788. }
  789.  
  790. var tokens = {
  791. // basic HIT info that can be scraped off the page
  792. hit_name:$("a.capsulelink[href='#']",$element).first().text().collapseWhitespace(),
  793. hit_id:olympus.utilities.href_group_id($("a[href*='roupId=']",$element).first().attr("href") || window.location.href), // groupid does not appear in preview
  794. hit_desc:(scrape_from_tooltip("description") || "None"), // description does not appear in preview
  795. hit_time:scrape_from_tooltip("duration_to_complete","time_left"),
  796. hits_available:scrape_from_tooltip("number_of_hits"),
  797. hit_reward:$("span.reward",$element).text().collapseWhitespace(),
  798. requester_name:$("a[href*='selectedSearchType=hitgroups']",$element).first().text().collapseWhitespace(),
  799. requester_id:olympus.utilities.href_requester_id($("a[href*='requesterId']",$element).first().attr("href"))
  800. };
  801.  
  802. // link properties for convenience, since these are long URLs that only use one bit of previously collected info
  803. tokens.preview_link = ("https://www.mturk.com/mturk/preview?groupId="+tokens.hit_id);
  804. tokens.panda_link = ("https://www.mturk.com/mturk/previewandaccept?groupId="+tokens.hit_id);
  805. tokens.requester_hits = ("https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="+tokens.requester_id);
  806. tokens.contact_requester = ("https://www.mturk.com/mturk/contact?requesterId="+tokens.requester_id+"&requesterName="+tokens.requester_name);
  807. tokens.to_reviews = ("https://turkopticon.ucsd.edu/"+tokens.requester_id);
  808.  
  809. // parse qualifications
  810. var $qual_anchor = $("a[id^='qualificationsRequired.tooltip'], a[id^='qualifications.tooltip']",$element).first();
  811. if($qual_anchor.parent().next().text().collapseWhitespace() === "None") tokens.quals = "None";
  812. else {
  813. if($qual_anchor.attr("id").contains("Required")) { // viewing list of HITs
  814. var quals = [];
  815. $("tr:not(:first-of-type) td:first-of-type",$qual_anchor.closest("table")).each(function() {quals.push($(this).text().collapseWhitespace());});
  816. tokens.quals = quals.join("; ");
  817. }
  818. else tokens.quals = $qual_anchor.parent().next().text().collapseWhitespace(); // previewing HIT where quals are already semicolon-delimited
  819. }
  820.  
  821. return tokens;
  822. },
  823. clear_page:function(title) {
  824. // when an olympian wants an independent full-page display. every element added to
  825. // the top level of the document has a data-pantheon attribute for exactly this purpose:
  826. // if an element does not have that attribute, it is removed
  827. $("head")
  828. .children().not("[data-pantheon]").remove()
  829. .end().end().append(
  830. $("<title/>").text(title)
  831. );
  832. $("body")
  833. .removeAttr("onload onLoad")
  834. .children().not("[data-pantheon]").remove();
  835. },
  836. clear_to_cache:function() {
  837. if(confirm("Are you sure you want to delete the Turkopticon cache?")) localStorage.removeItem("olympian_to_cache");
  838. },
  839. dhms:function(secs) {
  840. // takes a number of seconds (chiefly, hitAutoAppDelayInSeconds) and returns a
  841. // "friendly" value in seconds, minutes, hours, or days. has a precision of
  842. // tenths, e.g. "1.5 days" or "6.7 hours"
  843. function output(multiple,name) {
  844. function zeroes(num) {
  845. // removes ugly trailing zeroes (e.g. "1.0 days" or "2.40 hours")
  846. return +num.toFixed(1);
  847. }
  848. var units = zeroes((secs/multiple));
  849. return (""+units+" "+name+olympus.utilities.plural(units));
  850. }
  851.  
  852. if($.type(secs) !== "number") secs = Math.round(secs*1);
  853.  
  854. if(secs < 60) return output(1,"second");
  855. else if(secs < 3600) return output(60,"minute");
  856. else if(secs < 86400) return output(3600,"hour");
  857. else return output(86400,"day");
  858. },
  859. href_group_id:function(href) {
  860. if($.type(href) === "string") {
  861. href = href.match(/groupId=([^&\s]+)/i);
  862. if($.type(href) === "array") return href[1];
  863. }
  864. },
  865. href_id:function(href) {
  866. // when i don't know which one i have
  867. return (this.href_requester_id(href) || this.href_group_id(href));
  868. },
  869. href_requester_id:function(href) {
  870. if($.type(href) === "string") {
  871. href = href.match(/requesterId=([^&\s]+)/i);
  872. if($.type(href) === "array") return href[1];
  873. }
  874. },
  875. html_friendly:function(string) {
  876. return string.collapseWhitespace().replace(/\s/g,"_");
  877. },
  878. json_obj:function(json) {
  879. var obj;
  880. if(typeof json === "string" && json.trim().length) {
  881. try {obj = JSON.parse(json);}
  882. catch(e) {console.log("Malformed JSON object. Error message from JSON library: ["+e.message+"]");}
  883. }
  884. return obj;
  885. },
  886. localstorage_obj:function(key) {
  887. var obj = this.json_obj(localStorage.getItem(key));
  888. if(typeof obj !== "object") localStorage.removeItem(key);
  889. return obj;
  890. },
  891. pad_string:function(string,width,padding,side) {
  892. string = (""+string);
  893. var pad_item = (padding || "0"),
  894. half = ((width-string.length)/2);
  895. padding = "";
  896. while((string.length+padding.length) < width) padding = (padding+pad_item);
  897. if(side === "both") return (padding.slice(0,Math.floor(half))+string+padding.slice(Math.ceil(half)*-1));
  898. else if(side === "right") return (string+padding).slice(0,width);
  899. else return (padding+string).slice(width*-1);
  900. },
  901. parse_deferred_functions:function($context) {
  902. $context.find("*[data-function]").each(function() {
  903. switch($(this).attr("data-function")) {
  904. case "desc2fa": {
  905. $(this)
  906. .addClass("fa fa-fw fa-2x "+olympus.athena.desc2fa($(this).attr("data-args")))
  907. .removeAttr("data-function data-args");
  908. break;
  909. }
  910. case "feasibility2desc": {
  911. $(this).replaceWith(olympus.athena.feasibility2desc($(this).attr("data-args")));
  912. break;
  913. }
  914. case "to_cache_size": {
  915. $(this).replaceWith(
  916. document.createTextNode(""+olympus.utilities.bkmg(localStorage.olympian_to_cache.length))
  917. );
  918. break;
  919. }
  920. case "to_cache_usage": {
  921. $(this).replaceWith(
  922. document.createTextNode(""+(localStorage.olympian_to_cache.length/10485760).toFixed(2)+"%")
  923. );
  924. break;
  925. }
  926. }
  927. });
  928. },
  929. plural:function(num) {
  930. // returns the letter s if the number is not 1. just for pretty display
  931. // to say something like "2 widgets" instead of "2 widget"
  932. if($.type(num) !== "number") num = +num;
  933. if(num != 1) return "s";
  934. return "";
  935. },
  936. to_average:function(info) {
  937. function confidence(avg,ttl) {
  938. var rr = (olympus.settings.get(olympus,"bayesian_reviews")*1);
  939. return ((ttl/(ttl+rr))*avg+(rr/(ttl+rr))*olympus.settings.get(olympus,"bayesian_average"));
  940. }
  941.  
  942. var sum = 0,
  943. divisor = 0,
  944. average = 0;
  945. $.each(info.attrs,function(key,val) {
  946. var weight = (olympus.settings.get(olympus,"to_"+key+"_weight")*1),
  947. total = (val*weight);
  948. if(total > 0) {
  949. sum += total;
  950. divisor += weight;
  951. }
  952. });
  953.  
  954. average = (sum/divisor);
  955. return (olympus.settings.get(olympus,"bayesian_to") ? confidence(average,info.reviews) : average);
  956. },
  957. turkopticon:function(rids,callback,scope) {
  958. var to_mirrors = [
  959. "https://mturk-api.istrack.in/multi-attrs.php?ids=",
  960. "https://turkopticon.ucsd.edu/api/multi-attrs.php?ids="
  961. ],
  962. query_rids = [],
  963. query_result = {},
  964. deferred_cache = {};
  965.  
  966. function exit() {
  967. cache_commit();
  968. if($.type(callback) === "function") callback.call(scope,query_result);
  969. }
  970.  
  971. // turkopticon caching functions of this script reduce overhead ajax calls to the api
  972. // and instead stash data on the local system, if the user has allowed it. i personally
  973. // think a couple hundred kilobytes on your computer is well worth the performance boost
  974. function cache_commit() {
  975. // commit the deferred information from cache_set
  976. if(Object.keys(deferred_cache).length) {
  977. var to_cache = (olympus.utilities.localstorage_obj("olympian_to_cache") || {});
  978. $.each(deferred_cache,function(rid,attrs) {
  979. if($.type(attrs) !== "object") attrs = {};
  980. attrs.cache_time = new Date().getTime();
  981. to_cache[rid] = attrs;
  982. });
  983. localStorage.olympian_to_cache = JSON.stringify(to_cache);
  984. }
  985. }
  986.  
  987. function cache_get(rid) {
  988. var to_cache = olympus.utilities.localstorage_obj("olympian_to_cache");
  989. if($.type(to_cache) === "object" && to_cache.hasOwnProperty(rid)) {
  990. var attrs = to_cache[rid];
  991. // when turkopticon is disabled, any data is better than no data so cached information is used
  992. // regardless of age. otherwise, if turkopticon is enabled, there is a maximum age imposed
  993. // on the cached results as defined in the options that the user has set
  994. if(!olympus.settings.get(olympus,"query_turkopticon") || new Date().getTime() - (attrs.cache_time*1) < olympus.settings.get(olympus,"to_cache_timeout")) return attrs;
  995. }
  996. }
  997.  
  998. function cache_set(rid,attrs) {
  999. // so that all new cached data is stored once instead of firing a storage event infinity times
  1000. if(olympus.settings.get(olympus,"use_to_cache")) deferred_cache[rid] = attrs;
  1001. }
  1002.  
  1003. // check the cache for relevant data we can use and query for the rest
  1004. $.each(rids,function(k,v) {
  1005. var cached = cache_get(v);
  1006. if($.type(cached) === "object") query_result[v] = cached;
  1007. else query_rids.push(v);
  1008. });
  1009. var num_cached = Object.keys(query_result).length,
  1010. num_queried = query_rids.length;
  1011. 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"));
  1012. if(olympus.settings.get(olympus,"query_turkopticon") && query_rids.length) {
  1013. this.ajax_get(to_mirrors,query_rids.join(","),function(response) {
  1014. var jsobj = olympus.utilities.json_obj(response);
  1015. if($.type(jsobj) === "object") {
  1016. $.each(jsobj,function(rid,attrs) {
  1017. cache_set(rid,attrs);
  1018. query_result[rid] = attrs;
  1019. });
  1020. }
  1021. else console.log("Mount Olympus Turkopticon: query was successful but the response was malformed");
  1022. exit();
  1023. });
  1024. }
  1025. else exit();
  1026. }
  1027. };
  1028.  
  1029. $(document).ready(function() {
  1030. olympus.__init();
  1031. });