Home Reference Source

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);
    }
}