scripts/experiments/estimation/estimation.js
import {balance_subconditions} from "/scripts/experiment-properties/balancing/balancing_controller.js";
import {get_data} from "/scripts/experiment-properties/data/data_controller.js";
import {randomize_position,
randomize_radius_position,
force_greater_right_position} from "/scripts/helpers/experiment_helpers.js";
export default class Estimation {
/**
* Initializes a Estimation experiment object.
*
* @param params {object} Parameters passed in from routing
*/
constructor(params) {
let trial_structure = params["trial_structure"];
let condition_name = params["condition"];
let graph_type = params["graph_type"];
let balancing_type = params["balancing"];
// **NOTE: EXPERIMENTS variable comes from /public/config/experiments-config.js
if (!EXPERIMENTS["estimation"]["trial_structure"].includes(trial_structure)) {
throw Error(trial_structure + " is not supported.");}
else {
this.trial_structure = trial_structure;
}
if (!EXPERIMENTS["estimation"]["graph_type"].includes(graph_type)){
throw Error(graph_type + " is not supported.")}
else {
this.graph_type = graph_type;
};
if (!EXPERIMENTS["estimation"]["balancing_type"].includes(balancing_type)) {
throw Error(balancing_type + " is not supported.") }
else {
this.balancing_type = balancing_type;
}
this.condition_name = condition_name;
this.subject_id = params["subject_id"];
this.subject_initials = params["subject_initials"];
// ========================================
// EXPERIMENT CONSTANTS
this.X_DISTANCE_BETWEEN_SHAPES = 12;
this.Y_DIVIATION_FROM_X_AXIS = 3;
this.MAX_STEP_INTERVAL = 10;
this.ROUNDS_PER_COND = 4;
this.MAX_Y_POS_JITTER = 0.1; // y axis can be shifted away from default (window / 2) by at most 0.1 * ImageHeight;
this.MAX_STEP_SIZE = 0.05; // how much can the size of shapes can be changed at one keypress
// PIXELS_PER_CM is defined in estimation_experiment.html
if (PIXELS_PER_CM) {
this.PIXEL_TO_CM = PIXELS_PER_CM;
} else {
// 1cm is 37.7952755906 pixels
this.PIXEL_TO_CM = 37.7952755906;
throw Error("PIXELS_PER_CM is not defined");
}
// Margin from top and bottom of screen is set to at least 5cm
this.MARGIN = 5;
// ========================================
// EXPERIMENT VARIABLES
this.input_count_array= [0, 0, 0, 0];
this.curr_round_num = 0;
this.curr_condition_index = 0; // pointing to positions in this.curr_conditions_constants
this.is_practice = true;
// input_count_array has length equals to trials_per_round, each index representing num inputs per round
// for a given sub condition
this.curr_conditions_constants; // array of sub-conditions currently running
this.raw_sub_conds; // subconditions in estimation_data.js
this.curr_condition_index; // pointing to positions in this.curr_conditions_constants
this.round_end = true;
this.interf_shape_variables = {};
// ========================================
// PRACTICE EXPERIMENT VARIABLES
this.adjusted_midpoint_matrix = {};
this.practice_trial_data = [];
this.practice_end = false;
// ========================================
// TEST EXPERIMENT VARIABLES
this.sub_condition_order;
// ========================================
// CURRENT TRIAL DATA
this.curr_trial_data = {};
this.results = []; // trials are pushed to results at the end of trial;
// ========================================
// PREPARE EXPERIMENT
// Extract raw constants
// this.raw_sub_conds = generate_estimation_experiment_data(params.condition);
this.raw_sub_conds = get_data(this);
// console.log("raw sub conds");
// Prepare experiment + practice data
this.practice_conditions_constants = [];
this.curr_conditions_constants = []; // array of sub-conditions currently running
this.experiment_conditions_constants = [];
this.prepare_experiment();
this.prepare_practice();
}
/**
* Orders the input data according to balancing type and
* initializes the Estimation object's variables.
*
*/
prepare_experiment() {
let dataset = this.raw_sub_conds;
this.sub_condition_order = balance_subconditions(this.balancing_type, this.constructor.name.toLowerCase(), dataset.length);
let ordered_dataset = [];
// Order the data set according to the randomly ordered array
for (let i = 0; i < this.sub_condition_order.length; i++) {
ordered_dataset[i] = dataset[this.sub_condition_order[i]];
}
// Set experiment trials
this.experiment_conditions_constants = ordered_dataset;
}
/**
* Creates the practice dataset by taking the first FOUR subconditions.
*
*/
prepare_practice() {
let dataset = this.raw_sub_conds;
let practice_dataset = [];
for (let i = 0; i < 1; i++){
practice_dataset[i] = dataset[i];
this.practice_trial_data[i] = [];
}
// set variables to practice
this.practice_conditions_constants = practice_dataset;
this.curr_conditions_constants = practice_dataset;
this.curr_condition_index = 0;
this.current_practice_condition_index = 0;
this.input_count_array = new Array(this.curr_conditions_constants[0].trials_per_round).fill(0);
this.is_practice = true;
}
/**
* Resets all relevant variables to use that of the experiment.
* (input_count_array, curr_conditions_constants, and curr_condition_index
* are shared variables between the practice and test trials).
*
* This function is called once all the practice trials have run.
*/
set_variables_to_experiment() {
console.log("set_variables_to_experiment");
this.curr_conditions_constants = this.experiment_conditions_constants;
this.curr_condition_index = 0;
this.curr_round_num = 0;
this.input_count_array = new Array(this.curr_conditions_constants[0].trials_per_round).fill(0);
this.is_practice = false;
}
/**
* Generates a Estimation object for use in the JsPsych timeline.
*
* @param block_type {string} "test" or "practice"
* @return trial {object}
*/
generate_trial(block_type) {
if ((block_type !== "test") && (block_type !== "practice")) {
throw Error(block_type + " is not supported.")
}
// Initialize a variable for this so it is usable inside on_start
var estimation_exp = this;
var address = location.protocol + "//" + location.hostname + ":" + location.port + "/estimation_trial";
let group = {};
let is_ref_left = false;
let ready = {
type: 'html-keyboard-response',
choices: [32, 'q'],
stimulus: "",
on_start: function(trial) {
is_ref_left = Math.random() > 0.5;
trial.stimulus = "";
trial.stimulus += is_ref_left? "<div align = 'center'><font size = 20>" +
"<p>The modifiable shape will be on the <b>right.</b><p>" +
"<br> <br> <p><b>Press space to continue.</b></p></font></div>" :
"<div align = 'center'><font size = 20>" +
"<p>The modifiable shape will be on the <b>left.</b><p>" +
"<br> <br> <p><b>Press space to continue.</b></p></font></div>" ;
},
data: {type: 'instruction'}
};
let trial = {
type:'external-html-keyboard-response',
url: address,
choices: [32, 'q'], // 32 = spacebar, 81 = q (exit button for debugging)
execute_script: true,
response_ends_trial: true,
data: {
round_num: 0,
estimated_size: -1,
adjustments: [], // array of numbers representing the adjustments made to the shape
sub_condition_index: 0,
block_type: block_type
},
on_start: function(trial) {
console.log("====================on_start=======================");
// Set the constants to be used:
let current_constants = estimation_exp.curr_conditions_constants[estimation_exp.curr_condition_index];
console.log(current_constants);
trial.data.sub_condition_index = estimation_exp.curr_condition_index;
trial.data.round_num = estimation_exp.curr_round_num;
trial.data = Object.assign({}, trial.data);
trial.data = Object.assign(trial.data, current_constants);
trial.data.is_ref_left = is_ref_left; // is the reference shape on the left
// Handing saving for data that is an assoc array
if (current_constants.mod_side_shapes && current_constants.ref_side_shapes) {
trial.data.mod_side_shape_mod = current_constants.mod_side_shapes.mod;
trial.data.mod_side_shape_ref = current_constants.mod_side_shapes.ref;
trial.data.ref_side_shape_main = current_constants.ref_side_shapes.main;
trial.data.ref_side_shape_sub = current_constants.ref_side_shapes.sub;
delete trial.data.mod_side_shapes;
delete trial.data.ref_side_shapes;
}
if (current_constants.flicker_ref_durations) {
trial.data.flicker_ref_duration_on = current_constants.flicker_ref_durations.on;
trial.data.flicker_ref_duration_off = current_constants.flicker_ref_durations.off;
delete trial.data.flicker_ref_durations;
}
estimation_exp.curr_trial_data = trial.data;
// Save trial data for practice so can calculate exclusion criteria
if (trial.data.run_type === "practice") {
estimation_exp.practice_trial_data[estimation_exp.curr_condition_index].push(trial.data);
}
// console.log(JSON.stringify(trial));
},
on_finish: function(data) { // NOTE: on_finish takes in data var
// save data here
console.log("====================on_finish=======================");
// Save estimated ratio at end of round
console.log("REF SHAPE AREA: " + data.ref_shape_area);
console.log("ESTIMATED AREA: " + data.estimated_area);
// Assumption is that the ref ratio does small val/big val --> so need
// to do same when computing estimated ratio
if (data.ref_shape_area >= data.estimated_area) {
data.estimated_ratio = data.estimated_area / data.ref_shape_area;
} else {
data.estimated_ratio = data.ref_shape_area / data.estimated_area;
}
let curr_trial_data = JSON.parse(JSON.stringify((data)));
estimation_exp.results.push(curr_trial_data);
estimation_exp.update_curr_round_number(data);
estimation_exp.update_curr_cond_idx(data);
estimation_exp.update_input_array(data);
}
};
if (this.condition_name === "absolute_area_ratio_bisection_variant_A" ||
this.condition_name === "absolute_area_ratio_bisection_variant_B"){
group.timeline = [trial];
} else {
group.timeline = [ready, trial];
}
return group;
}
/**
* Set the current trial's number of inputs in the input_count_array
* @param data {object} the trial.data object from jsPsych
* */
update_input_array(data) {
if (data.round_num < 0 || data.round_num > 3) {
throw Error("trail number : " + data.round_num + " is out of range");
}
this.input_count_array[data.round_num] = data.adjustments.length;
}
/**
* Update the current round number
* @param trial_data {object} the trial.data object from jsPsych
* */
update_curr_round_number(trial_data) {
if (trial_data.round_num === this.ROUNDS_PER_COND - 1) {
this.curr_round_num = 0;
} else {
this.curr_round_num++;
}
}
/**
* Update the index of the condition that is being referred to
* @param trial_data {object} the trail.data object from jsPsych
* */
update_curr_cond_idx(trial_data) {
if (trial_data.round_num === this.ROUNDS_PER_COND - 1) {
this.curr_condition_index++;
}
}
/**
* Appends the adjustment to the curr_trial_data.adjustments.
*
* @param {double} adjustment
*/
save_adjustment(adjustment) {
this.curr_trial_data.adjustments.push(adjustment);
}
/**
* Saves the estimated size to the curr_trial_data.
*
* @param {double} estimated_size
* {string} unit of the estimated_size
*/
save_estimated_size(estimated_size, unit) {
this.curr_trial_data.estimated_size = estimated_size;
this.curr_trial_data.estimated_size_unit = unit;
// console.log("ESTIMATED SIZE: " + estimated_size);
}
/**
* Saves the estimated area to the curr_trial_data.
*
* @param {double} area
* {string} unit of the estimated_area
*/
save_estimated_area(area) {
this.curr_trial_data.estimated_area = area;
// console.log("ESTIMATED AREA: " + area);
}
/**
* Saves and computes the ref shape area to the curr_trial_data.
*
* - For normal estimation conditions (AKA one shape on mod and one shape on ref side),
* will use the single shape on the ref side
* - For multi interference conditions, is using the main shape on the mod side
* - For interference conditions, is using the single shape on ref side
* - For bisection conditions, it is taking the midpoint between the left and right reference shape areas
*
* @param {object} attributes
*/
save_reference_shape_area(attributes) {
let name_array = this.condition_name.split("_");
let ref_shape_attributes, area;
if (name_array.includes("interference")) {
if (name_array.includes("multi")){
// Taking main shape on mod side
ref_shape_attributes = this.curr_trial_data.is_ref_left ? attributes.right_shape : attributes.left_shape;
area = this.compute_shape_area(ref_shape_attributes.shape, ref_shape_attributes.size);
} else {
ref_shape_attributes = this.curr_trial_data.is_ref_left ? attributes.left_shape : attributes.right_shape;
area = this.compute_shape_area(ref_shape_attributes.shape, ref_shape_attributes.size);
}
}
else if (name_array.includes("bisection")) {
let left_area = this.compute_shape_area(attributes.left_shape.shape, attributes.left_shape.size);
let right_area = this.compute_shape_area(attributes.right_shape.shape, attributes.right_shape.size);
area = (left_area + right_area) / 2;
} else {
ref_shape_attributes = this.curr_trial_data.is_ref_left ? attributes.left_shape : attributes.right_shape;
area = this.compute_shape_area(ref_shape_attributes.shape, ref_shape_attributes.size);
}
this.curr_trial_data.ref_shape_area = area;
}
/**
* Computes the area for a given shape and the size.
*
* @param {string} shape
* {string} size (in pixels)
*
* @return {double} area (in CM^2)
*/
compute_shape_area(shape, size) {
let area;
switch (shape) {
case "square": {
let length = size / this.PIXEL_TO_CM;
area = length * length;
} break;
case "triangle" : { //Assuming equilateral for now
let length = size / this.PIXEL_TO_CM;
area = (Math.sqrt(3)/4)*(length*length);
} break;
case "circle" : {
let radius = (size / 2) / this.PIXEL_TO_CM; //Size = diameter
area = Math.PI*(radius*radius);
} break;
case "line" : {
let length = size / this.PIXEL_TO_CM;
let width = 1 / this.PIXEL_TO_CM; //Assuming stroke-width is 1px
area = length * width;
} break;
case "rectangle" : {
let short_side = size / this.PIXEL_TO_CM;
let long_side = short_side * this.curr_trial_data.width_height_ratio;
area = short_side * long_side;
} break;
default:
throw Error("Handling for computing area for shape " + shape + " has not been implemented.");
break;
}
return area;
}
/**
* Computes the area for a fan.
*
* @param {double} angle (in degrees)
* {double} radius (in pixels)
*
* @return {double} area (in CM^2)
*/
compute_fan_area(angle, radius) {
let radius_in_cm = radius / this.PIXEL_TO_CM;
let area = Math.PI*(radius_in_cm*radius_in_cm)*(angle/360); // A = pi*r^2*(C/360)
return area;
}
/**
* Computes attributes for all estimation plots.
*
* @param {object} experiment
*/
compute_plot_attributes() {
let sub_cond = this.curr_conditions_constants[this.curr_condition_index];
let round_num = this.curr_round_num;
// ----------------------------------------------------------------------------
// COMPUTATION FOR ATTRIBUTES
// Disable jitter for multi interference conditions
let is_jitter = this.condition_name.split("_").includes("multi") ? false : true;
let width = window.innerWidth;
let height = window.innerHeight;
let mid_width = width / 2;
let mid_height = height / 2;
let left_x = mid_width - this.X_DISTANCE_BETWEEN_SHAPES * this.PIXEL_TO_CM / 2;
let right_x = mid_width + this.X_DISTANCE_BETWEEN_SHAPES * this.PIXEL_TO_CM / 2;
let ref_size = sub_cond.ref_size * this.PIXEL_TO_CM ;
let ref_y = this.calculate_y_position(ref_size, is_jitter);
// The size of the modifiable shape start from mod_min_size for trial 0 and 2, mod_max_size for 1 and 3;
let mod_size = (round_num % 2 === 1)?
sub_cond.mod_max_size * this.PIXEL_TO_CM : sub_cond.mod_min_size * this.PIXEL_TO_CM;
let mod_y = this.calculate_y_position(mod_size, is_jitter);
let is_ref_left = this.curr_trial_data.is_ref_left;
let flicker_options;
if (sub_cond.flicker_ref_durations) {
flicker_options = {"flicker": {on: sub_cond.flicker_ref_durations.on, off: sub_cond.flicker_ref_durations.off}};
}
this.curr_trial_data.is_ref_smaller = (round_num % 2 === 1);
// ----------------------------------------------------------------------------
// ATTRIBUTE SET-UP
let attributes = {
chart: {
width: width,
height: height,
target_area_ratio: sub_cond.target_area_ratio ? sub_cond.target_area_ratio : null
},
core: {
ref_size: ref_size,
ref_y: ref_y,
mod_size: mod_size,
mod_y: mod_y,
left_size: is_ref_left ? ref_size : mod_size,
right_size: is_ref_left ? mod_size : ref_size,
},
left_shape: {
shape: is_ref_left ? sub_cond.ref_shape : sub_cond.mod_shape,
size: is_ref_left ? ref_size : mod_size,
x: left_x,
y: is_ref_left ? ref_y : mod_y,
outline: is_ref_left ? sub_cond.ref_outline : sub_cond.mod_outline,
fill: is_ref_left ? sub_cond.ref_fill : sub_cond.mod_fill,
is_ref: is_ref_left ? true : false,
options: is_ref_left ? flicker_options : null
},
right_shape: {
shape: is_ref_left ? sub_cond.mod_shape : sub_cond.ref_shape,
size: is_ref_left ? mod_size : ref_size,
x: right_x,
y: is_ref_left ? mod_y : ref_y,
outline: is_ref_left ? sub_cond.mod_outline : sub_cond.ref_outline,
fill: is_ref_left ? sub_cond.mod_fill : sub_cond.ref_fill,
is_ref: is_ref_left ? false : true,
options: is_ref_left ? null : flicker_options
}
}
// ----------------------------------------------------------------------------
// ADD'L ATTRIBUTE PREP FOR INTERFERENCE CONDITIONS
let name_array = this.condition_name.split("_");
if (name_array.includes("interference")) {
if (name_array.includes("multi")) {
attributes = this.compute_estimation_multi_interference_attributes(sub_cond, attributes);
}
else {
attributes = this.compute_estimation_interference_attributes(sub_cond, attributes);
}
}
else if (name_array.includes("bisection")) {
attributes = this.compute_bisection_attributes(sub_cond, attributes);
}
this.save_reference_shape_area(attributes);
return attributes;
}
/**
* Computes attributes for bisection conditions (AKA has "bisection" in condition name).
*
* @param {object} sub_cond
* {object} attributes
*/
compute_bisection_attributes(sub_cond, attributes) {
let x_adjustment = this.X_DISTANCE_BETWEEN_SHAPES * this.PIXEL_TO_CM / 4;
let mid_width = window.innerWidth / 2;
let mid_height = window.innerHeight / 2;
// Alternate left/right of the sizes depending on trial number
let left_size, right_size;
if (this.curr_round_num % 2 === 1) {
left_size = sub_cond.ref_size[1];
right_size = sub_cond.ref_size[0];
} else {
left_size = sub_cond.ref_size[0];
right_size = sub_cond.ref_size[1];
}
let flicker_options;
if (sub_cond.flicker_ref_durations) {
flicker_options = {"flicker": {on: sub_cond.flicker_ref_durations.on, off: sub_cond.flicker_ref_durations.off}};
}
attributes.left_shape = {
shape: sub_cond.ref_shape[0],
size: left_size * this.PIXEL_TO_CM,
x: attributes.left_shape.x -= x_adjustment,
y: this.calculate_y_position(sub_cond.ref_size[0] * this.PIXEL_TO_CM, false),
outline: sub_cond.ref_outline,
fill: sub_cond.ref_fill,
is_ref: true,
options: flicker_options
};
attributes.right_shape = {
shape: sub_cond.ref_shape[1],
size: right_size * this.PIXEL_TO_CM,
x: attributes.right_shape.x += x_adjustment,
y: this.calculate_y_position(sub_cond.ref_size[1] * this.PIXEL_TO_CM, false),
outline: sub_cond.ref_outline,
fill: sub_cond.ref_fill,
is_ref: true,
options: flicker_options
};
attributes.middle_shape = {
shape: sub_cond.mod_shape,
size: sub_cond.mod_max_size * this.PIXEL_TO_CM, //doesn't matter if use max or min, they are set to be same
x: mid_width,
y: this.calculate_y_position(sub_cond.mod_max_size * this.PIXEL_TO_CM, false),
outline: sub_cond.mod_outline,
fill: sub_cond.mod_fill,
is_ref: false,
options: null
}
return attributes;
}
/**
* Computes attributes for single-interference conditions (AKA no "multi" in condition name).
*
* @param {object} sub_cond
* {object} attributes
*/
compute_estimation_interference_attributes(sub_cond, attributes) {
// Only prep attributes on sub_cond with interf variables
if (sub_cond.interf_shape && sub_cond.interf_fill && sub_cond.interf_outline && sub_cond.interf_ratio) {
let is_ref_left = this.curr_trial_data.is_ref_left;
let interf_radius = attributes.core.mod_size * sub_cond.interf_ratio;
let interf_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
let interf_y_pos = attributes.core.mod_y + attributes.core.mod_size*0.1;
if (sub_cond.mod_shape === "triangle" && sub_cond.interf_shape === "circle") {
interf_radius = interf_radius * 2;
}
attributes.interf_shape = {
shape: sub_cond.interf_shape,
size: interf_radius,
x: interf_x_pos,
y: interf_y_pos,
outline: sub_cond.interf_outline,
fill: sub_cond.interf_fill,
is_ref: true,
options: {"scaling": "scales_with_mod"}
}
return attributes;
} else {
return attributes;
}
}
/**
* Computes attributes for multi-interference conditions (AKA has "multi" in condition name).
*
* @param {object} sub_cond
* {object} attributes
*/
compute_estimation_multi_interference_attributes(sub_cond, attributes) {
if (!sub_cond.mod_side_shapes || !sub_cond.mod_ratio || !sub_cond.interf_fill || !sub_cond.interf_outline ||
!sub_cond.mod_side_alignment || !sub_cond.ref_side_shapes || !sub_cond.ref_ratio || !sub_cond.ref_side_alignment) {
throw Error("Missing attributes to run a multi-interference condition.");
}
let is_ref_left = this.curr_trial_data.is_ref_left;
// ----------------------------------------------------------------------------
// FORCE LEFT/RIGHT SHAPES TO BE BASED ON REF-SIDE-MAIN AND MOD-SIDE-REF
let left_shape = is_ref_left ? sub_cond.ref_side_shapes.main : sub_cond.mod_side_shapes.ref;
let right_shape = is_ref_left ? sub_cond.mod_side_shapes.ref : sub_cond.ref_side_shapes.main;
attributes.left_shape.shape = left_shape; // Overwrite attributes
attributes.right_shape.shape = right_shape;
// Main shapes are automatically "ref"
attributes.left_shape.is_ref = true;
attributes.right_shape.is_ref = true;
// ----------------------------------------------------------------------------
// COMPUTE ATTRIBUTES FOR NON-MAIN SHAPES + PLOT
// Buffer to ensure shapes on ref and mod sides don't overlap with each other
let x_buffer = 3 * this.PIXEL_TO_CM; // 3 CM buffer
attributes.right_shape.x += x_buffer;
attributes.left_shape.x -= x_buffer;
this.append_ref_sub_attributes(sub_cond, attributes, is_ref_left);
this.append_mod_attributes(sub_cond, attributes, is_ref_left);
console.log(attributes);
return attributes;
}
/**
* For 2 shapes S1 and S2, with an area ratio of S1:S2, computes the length
* for S2 to maintain this area ratio.
*
* @param {string} S1_shape_type
* {double} S1_length (in pixels)
* {double} S1_S2_area_ratio (S1 area / S2 area)
* {string} S2_shape_type
*
* @return {double} S2_length (in pixels)
*/
calculate_length_from_area_ratio(S1_shape_type, S1_length, S1_S2_area_ratio, S2_shape_type) {
// Compute area (in CM^2) of the initial_length
let S1_area = this.compute_shape_area(S1_shape_type, S1_length);
let S2_area = S1_area * (1/S1_S2_area_ratio);
let S2_length_cm = this.compute_shape_length(S2_shape_type, S2_area);
console.log("RATIO: " + S1_S2_area_ratio);
console.log("S1 LENGTH: " + S1_length / this.PIXEL_TO_CM);
console.log("S1 AREA: " + S1_area);
console.log("S2 LENGTH: " + S2_length_cm);
console.log("S2 AREA: " + S2_area);
return S2_length_cm * this.PIXEL_TO_CM;
}
/**
* Computes the shape length with the specified area.
*
* @param {string} shape_type
* {double} target area
*
* @return {double} length
*/
compute_shape_length(shape_type, area) {
let length;
switch (shape_type) {
case "square" : {
length = Math.sqrt(area);
} break;
case "triangle" : { //Assume equilateral for now
let inner = (4*area)/Math.sqrt(3);
length = Math.sqrt(inner);
} break;
case "circle" : {
let radius = Math.sqrt(area/Math.PI);
length = radius*2; //Return diameter b/c plotting code assumes it is taking diameter
} break;
default:
throw Error ("Computations for determining length from area for shape " + shape_type + " has not been implemented.");
break;
}
return length;
}
/**
* Computes attributes for the sub shape on the ref side for multi interference conditions.
* Additionally adjusts the main shape on the ref side depending on the alignment.
*
* @param {object} sub_cond
* {object} attributes
* {boolean} is_ref_left
*/
append_ref_sub_attributes(sub_cond, attributes, is_ref_left) {
let ref_radius, ref_x_pos, ref_y_pos;
let options = {};
switch (sub_cond.ref_side_alignment) {
case "overlapping-bottom": {
let main_ref_shape_type = is_ref_left ? attributes.left_shape.shape : attributes.right_shape.shape;
ref_radius = this.calculate_length_from_area_ratio(main_ref_shape_type, attributes.core.ref_size, sub_cond.ref_ratio, sub_cond.ref_side_shapes.sub);
// Same x as main shape
ref_x_pos = is_ref_left ? attributes.left_shape.x : attributes.right_shape.x;
// Shift down to bottom of main shape
ref_y_pos = attributes.core.ref_y - (ref_radius - attributes.core.ref_size)/2;
} break;
case "vertical-left": {
let main_ref_shape_type = is_ref_left ? attributes.left_shape.shape : attributes.right_shape.shape;
ref_radius = this.calculate_length_from_area_ratio(main_ref_shape_type, attributes.core.ref_size, sub_cond.ref_ratio, sub_cond.ref_side_shapes.sub);
let x_pos_main_ref = is_ref_left ? attributes.left_shape.x : attributes.right_shape.x;
ref_x_pos = x_pos_main_ref + ref_radius/2 - attributes.core.ref_size/2;
ref_y_pos = attributes.core.ref_y + ref_radius/2 + attributes.core.ref_size * 0.5 + this.PIXEL_TO_CM * 2;
// Shift up so ref shapes are centered horizontally
let y_buffer = ((ref_y_pos + 0.5 * ref_radius) - (attributes.core.ref_y + 0.5 * attributes.core.left_size)) / 2;
ref_y_pos = ref_y_pos - y_buffer;
// Main ref shape needs to move up as well
if (is_ref_left) {
attributes.left_shape.y -= y_buffer;
} else {
attributes.right_shape.y -= y_buffer;
}
} break;
case "vertical-left-cutout": {
if (sub_cond.ref_side_shapes.sub === "square") {
let main_ref_shape_type = is_ref_left ? attributes.left_shape.shape : attributes.right_shape.shape;
ref_radius = this.calculate_length_from_area_ratio(main_ref_shape_type, attributes.core.ref_size, sub_cond.ref_ratio, sub_cond.ref_side_shapes.sub);
let x_pos_main_ref = is_ref_left ? attributes.left_shape.x : attributes.right_shape.x;
ref_x_pos = x_pos_main_ref + ref_radius/2 - attributes.core.ref_size/2;
ref_y_pos = attributes.core.ref_y + ref_radius/2 + attributes.core.ref_size * 0.5 + this.PIXEL_TO_CM * 2;
// Shift up so ref shapes are centered horizontally
let y_buffer = ((ref_y_pos + 0.5 * ref_radius) - (attributes.core.ref_y + 0.5 * attributes.core.left_size)) / 2;
ref_y_pos = ref_y_pos - y_buffer;
// Main ref shape needs to move up as well
if (is_ref_left) {
attributes.left_shape.y -= y_buffer;
} else {
attributes.right_shape.y -= y_buffer;
}
let main_ref_size = is_ref_left ? attributes.left_shape.size : attributes.right_shape.size;
options = {"cutout_radius" : main_ref_size};
} else {
throw Error(sub_cond.ref_side_shapes.sub + " shape has not been handled for vertical-left-cutout side alignment.");
}
} break;
case "vertical-centered": {
if (sub_cond.ref_side_shapes.sub === "fan") {
// Same radius and x pos
ref_radius = is_ref_left ? attributes.left_shape.size : attributes.right_shape.size;
ref_x_pos = is_ref_left ? attributes.left_shape.x : attributes.right_shape.x;
// Shift up
ref_y_pos = attributes.core.ref_y - ref_radius - this.PIXEL_TO_CM * 2;
let angle_size = 360/(1/sub_cond.ref_ratio);
options = {"fan-attributes": {"slice-alignment": "top", "angle_size": angle_size}};
}
else {
throw Error(sub_cond.ref_side_shapes.sub + " shape has not been handled for vertical-centered ref side alignment.");
}
} break;
default:
throw Error(sub_cond.ref_side_alignment + " ref side alignment is not supported.");
}
if (sub_cond.flicker_ref_durations){
options.flicker = {on: sub_cond.flicker_ref_durations.on, off: sub_cond.flicker_ref_durations.off};
}
attributes.ref_sub_shape = {
shape: sub_cond.ref_side_shapes.sub,
size: ref_radius,
x: ref_x_pos,
y: ref_y_pos,
outline: sub_cond.interf_outline,
fill: sub_cond.interf_fill,
is_ref: true,
options: options
};
}
/**
* Computes attributes for the modifiable shape for multi interference conditions.
* Additionally adjusts the main shape on the mod side depending on the alignment.
*
* @param {object} sub_cond
* {object} attributes
* {boolean} is_ref_left
*/
append_mod_attributes(sub_cond, attributes, is_ref_left) {
let mod_radius, mod_x_pos, mod_y_pos, options;
switch (sub_cond.mod_side_alignment) {
case "overlapping-center": {
if (sub_cond.mod_side_shapes.mod === "fan") {
mod_radius = attributes.core.mod_size;
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y - 0.25*(mod_radius);
let angle_size = 360/(1/sub_cond.mod_ratio);
options = {"fan-attributes": {"slice-alignment": "bottom", "angle_size": angle_size}};
} else {
let sub_shape_type = is_ref_left ? attributes.right_shape.shape : attributes.left_shape.shape;
mod_radius = this.calculate_length_from_area_ratio(sub_shape_type, attributes.core.mod_size, sub_cond.mod_ratio, sub_cond.mod_side_shapes.mod);
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y;
options = {"scaling": "scales_indep"};
}
} break;
case "diagonal": {
let sub_shape_type = is_ref_left ? attributes.right_shape.shape : attributes.left_shape.shape;
mod_radius = this.calculate_length_from_area_ratio(sub_shape_type, attributes.core.mod_size, sub_cond.mod_ratio, sub_cond.mod_side_shapes.mod);
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y;
options = null;
// Shift down
let y_shift = sub_cond.mod_ratio < 1 ? mod_radius : (mod_radius*1.5 + this.PIXEL_TO_CM * 2);
// Manipulate alignment for main shapes on mod side
if (is_ref_left) {
attributes.right_shape.x += mod_radius;
attributes.right_shape.y -= y_shift;
} else {
attributes.left_shape.x += mod_radius;
attributes.left_shape.y -= y_shift;
}
} break;
case "overlapping-bottom": {
if (sub_cond.mod_side_shapes.mod === "fan") {
mod_radius = attributes.core.mod_size;
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y;
let angle_size = 360/(1/sub_cond.mod_ratio);
options = {"fan-attributes": {"slice-alignment": "bottom", "angle_size": angle_size}};
}
else {
throw Error(sub_cond.mod_side_shapes.mod + " shape has not been handled for overlapping-bottom mod side alignment.");
}
} break;
case "overlapping-bottom-edge": {
if (sub_cond.mod_side_shapes.mod === "fan") {
mod_radius = attributes.core.mod_size;
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y + mod_radius/4; // Shift down 1/4 of circle
let angle_size = 360/(1/sub_cond.mod_ratio);
options = {"fan-attributes": {"slice-alignment": "bottom", "angle_size": angle_size}};
}
else {
throw Error(sub_cond.mod_side_shapes.mod + " shape has not been handled for overlapping-bottom-edge mod side alignment.");
}
} break;
case "slice-bottom": {
if (sub_cond.mod_side_shapes.mod === "fan") {
mod_radius = attributes.core.mod_size;
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y + mod_radius/4; // Shift down 1/4 of circle
let angle_size = 360/(1/sub_cond.mod_ratio);
options = {"fan-attributes": {"slice-alignment": "bottom", "angle_size": angle_size, "ref_shape_adjusts": true}};
}
else {
throw Error(sub_cond.mod_side_shapes.mod + " shape has not been handled for slice-bottom mod side alignment.");
}
} break;
case "overlapping-top-left-corner": {
if (sub_cond.mod_side_shapes.mod === "square") {
let sub_shape_type = is_ref_left ? attributes.right_shape.shape : attributes.left_shape.shape;
mod_radius = this.calculate_length_from_area_ratio(sub_shape_type, attributes.core.mod_size, sub_cond.mod_ratio, sub_cond.mod_side_shapes.mod);
// This centers it
mod_x_pos = is_ref_left ? attributes.right_shape.x : attributes.left_shape.x;
mod_y_pos = attributes.core.mod_y;
// Moves it to top left
let diff = attributes.core.mod_size - mod_radius*2;
mod_x_pos -= diff;
mod_y_pos -= diff;
} else {
throw Error(sub_cond.mod_side_shapes.mod + " shape has not been handled for overlapping-top-left-corner mod side alignment.");
}
} break;
default:
throw Error(sub_cond.mod_side_alignment + " is not supported.");
}
attributes.mod_shape = {
shape: sub_cond.mod_side_shapes.mod,
size: mod_radius,
x: mod_x_pos,
y: mod_y_pos,
outline: sub_cond.interf_outline,
fill: sub_cond.interf_fill,
is_ref: false,
options: options
}
}
/**
* Calculates the y value of the position where the shape should be plotted
*
* @param {number} radius the radius of the shape
* @param {boolean} whether to jitter the y pos or not
*
* @returns {number}
*/
calculate_y_position(radius, with_jitter) {
// y_margin is the distance from
let y_margin = this.MARGIN * this.PIXEL_TO_CM;
// pick a random position inside the screen such the the shapes will not be displayed outside of the border
let range = [y_margin + radius / 2, window.innerHeight - y_margin - radius / 2];
let y_pos;
if (with_jitter) {
y_pos = Math.random() * (range[1] - range[0]) + range[0];
} else {
y_pos = (range[1] + range[0]) / 2;
}
return y_pos;
}
/*
* Saves experiment data as csv
* */
export_trial_data() {
let trial_data = jsPsych.data.get().filterCustom(function (row) {
return row.block_type === "practice" || row.block_type === "test";
})
// These are variables forced on by jsPsych
.ignore('stimulus')
.ignore('key_press')
.ignore('choices')
.ignore('trial_type')
.ignore('trial_index')
.ignore('time_elapsed')
.ignore('internal_node_id')
.ignore('rt');
let fileName = "S" + this.subject_id + "_" + this.condition_name + "_shape_estimation_trial_results.csv";
trial_data.localSave('csv', fileName);
}
}