import { JsPsych } from "./JsPsych" ; import { repeat , sampleWithReplacement , sampleWithoutReplacement , shuffle , shuffleAlternateGroups , } from "./modules/randomization" ; import { deepCopy } from "./modules/utils" ; export class TimelineNode { relative_id ; parent_node ; trial_parameters ; timeline_parameters ; node_trial_data ; progress = < any > { current_location : - 1 , current_variable_set : 0 , current_repetition : 0 , current_iteration : 0 , done : false , } ; end_message ? : string ; constructor ( private jsPsych , parameters , parent ? , relativeID ? ) { this . parent_node = parent ; this . relative_id = typeof parent === "undefined" ? 0 : relativeID ; if ( typeof parameters . timeline !== "undefined" ) { this . timeline_parameters = { timeline : [ ] , loop_function : parameters . loop_function , conditional_function : parameters . conditional_function , sample : parameters . sample , randomize_order : typeof parameters . randomize_order == "undefined" ? false : parameters . randomize_order , repetitions : typeof parameters . repetitions == "undefined" ? 1 : parameters . repetitions , timeline_variables : typeof parameters . timeline_variables == "undefined" ? [ { } ] : parameters . timeline_variables , on_timeline_finish : parameters . on_timeline_finish , on_timeline_start : parameters . on_timeline_start , } ; this . setTimelineVariablesOrder ( ) ; var node_data = Object . assign ( { } , parameters ) ; delete node_data . timeline ; delete node_data . conditional_function ; delete node_data . loop_function ; delete node_data . randomize_order ; delete node_data . repetitions ; delete node_data . timeline_variables ; delete node_data . sample ; delete node_data . on_timeline_start ; delete node_data . on_timeline_finish ; this . node_trial_data = node_data ; for ( var i = 0 ; i < parameters . timeline . length ; i ++ ) { var merged_parameters = Object . assign ( { } , node_data , parameters . timeline [ i ] ) ; if ( typeof node_data . data == "object" && typeof parameters . timeline [ i ] . data == "object" ) { var merged_data = Object . assign ( { } , node_data . data , parameters . timeline [ i ] . data ) ; merged_parameters . data = merged_data ; } this . timeline_parameters . timeline . push ( new TimelineNode ( this . jsPsych , merged_parameters , this , i ) ) ; } } else { if ( typeof parameters . type === "undefined" ) { console . error ( 'Trial level node is missing the "type" parameter. The parameters for the node are: ' + JSON . stringify ( parameters ) ) ; } this . trial_parameters = { ... parameters } ; } } trial ( ) { if ( typeof this . timeline_parameters == "undefined" ) { return deepCopy ( this . trial_parameters ) ; } else { if ( this . progress . current_location >= this . timeline_parameters . timeline . length ) { return null ; } else { return this . timeline_parameters . timeline [ this . progress . current_location ] . trial ( ) ; } } } markCurrentTrialComplete ( ) { if ( typeof this . timeline_parameters === "undefined" ) { this . progress . done = true ; } else { this . timeline_parameters . timeline [ this . progress . current_location ] . markCurrentTrialComplete ( ) ; } } nextRepetiton ( ) { this . setTimelineVariablesOrder ( ) ; this . progress . current_location = - 1 ; this . progress . current_variable_set = 0 ; this . progress . current_repetition ++ ; for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { this . timeline_parameters . timeline [ i ] . reset ( ) ; } } setTimelineVariablesOrder ( ) { const timeline_parameters = this . timeline_parameters ; if ( typeof timeline_parameters === "undefined" || typeof timeline_parameters . timeline_variables === "undefined" ) { return ; } var order = [ ] ; for ( var i = 0 ; i < timeline_parameters . timeline_variables . length ; i ++ ) { order . push ( i ) ; } if ( typeof timeline_parameters . sample !== "undefined" ) { if ( timeline_parameters . sample . type == "custom" ) { order = timeline_parameters . sample . fn ( order ) ; } else if ( timeline_parameters . sample . type == "with-replacement" ) { order = sampleWithReplacement ( order , timeline_parameters . sample . size , timeline_parameters . sample . weights ) ; } else if ( timeline_parameters . sample . type == "without-replacement" ) { order = sampleWithoutReplacement ( order , timeline_parameters . sample . size ) ; } else if ( timeline_parameters . sample . type == "fixed-repetitions" ) { order = repeat ( order , timeline_parameters . sample . size , false ) ; } else if ( timeline_parameters . sample . type == "alternate-groups" ) { order = shuffleAlternateGroups ( timeline_parameters . sample . groups , timeline_parameters . sample . randomize_group_order ) ; } else { console . error ( 'Invalid type in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"' ) ; } } if ( timeline_parameters . randomize_order ) { order = shuffle ( order ) ; } this . progress . order = order ; } nextSet ( ) { this . progress . current_location = - 1 ; this . progress . current_variable_set ++ ; for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { this . timeline_parameters . timeline [ i ] . reset ( ) ; } } advance ( ) { const progress = this . progress ; const timeline_parameters = this . timeline_parameters ; const internal = this . jsPsych . internal ; if ( progress . done ) { return true ; } if ( progress . current_location == - 1 ) { if ( typeof timeline_parameters !== "undefined" ) { if ( typeof timeline_parameters . conditional_function !== "undefined" && progress . current_repetition == 0 && progress . current_variable_set == 0 ) { internal . call_immediate = true ; var conditional_result = timeline_parameters . conditional_function ( ) ; internal . call_immediate = false ; if ( conditional_result == false ) { progress . done = true ; return true ; } } if ( typeof timeline_parameters . on_timeline_start !== "undefined" && progress . current_variable_set == 0 ) { timeline_parameters . on_timeline_start ( ) ; } } progress . current_location = 0 ; return this . advance ( ) ; } if ( typeof timeline_parameters !== "undefined" ) { var have_node_to_run = false ; while ( progress . current_location < timeline_parameters . timeline . length && have_node_to_run == false ) { var target_complete = timeline_parameters . timeline [ progress . current_location ] . advance ( ) ; if ( ! target_complete ) { have_node_to_run = true ; return false ; } else { progress . current_location ++ ; } } if ( progress . current_variable_set < progress . order . length - 1 ) { this . nextSet ( ) ; return this . advance ( ) ; } else if ( progress . current_repetition < timeline_parameters . repetitions - 1 ) { this . nextRepetiton ( ) ; if ( typeof timeline_parameters . on_timeline_finish !== "undefined" ) { timeline_parameters . on_timeline_finish ( ) ; } return this . advance ( ) ; } else { if ( typeof timeline_parameters . on_timeline_finish !== "undefined" ) { timeline_parameters . on_timeline_finish ( ) ; } if ( typeof timeline_parameters . loop_function !== "undefined" ) { internal . call_immediate = true ; if ( timeline_parameters . loop_function ( this . generatedData ( ) ) ) { this . reset ( ) ; internal . call_immediate = false ; return this . parent_node . advance ( ) ; } else { progress . done = true ; internal . call_immediate = false ; return true ; } } } progress . done = true ; return true ; } } isComplete ( ) { return this . progress . done ; } getTimelineVariableValue ( variable_name ) { if ( typeof this . timeline_parameters == "undefined" ) { return undefined ; } var v = this . timeline_parameters . timeline_variables [ this . progress . order [ this . progress . current_variable_set ] ] [ variable_name ] ; return v ; } findTimelineVariable ( variable_name ) { var v = this . getTimelineVariableValue ( variable_name ) ; if ( typeof v == "undefined" ) { if ( typeof this . parent_node !== "undefined" ) { return this . parent_node . findTimelineVariable ( variable_name ) ; } else { return undefined ; } } else { return v ; } } timelineVariable ( variable_name ) { if ( typeof this . timeline_parameters == "undefined" ) { return this . findTimelineVariable ( variable_name ) ; } else { var loc = Math . max ( 0 , this . progress . current_location ) ; if ( loc == this . timeline_parameters . timeline . length ) { loc = loc - 1 ; } return this . timeline_parameters . timeline [ loc ] . timelineVariable ( variable_name ) ; } } allTimelineVariables ( ) { var all_tvs = this . allTimelineVariablesNames ( ) ; var all_tvs_vals = < any > { } ; for ( var i = 0 ; i < all_tvs . length ; i ++ ) { all_tvs_vals [ all_tvs [ i ] ] = this . timelineVariable ( all_tvs [ i ] ) ; } return all_tvs_vals ; } allTimelineVariablesNames ( so_far = [ ] ) { if ( typeof this . timeline_parameters !== "undefined" ) { so_far = so_far . concat ( Object . keys ( this . timeline_parameters . timeline_variables [ this . progress . order [ this . progress . current_variable_set ] ] ) ) ; var loc = Math . max ( 0 , this . progress . current_location ) ; if ( loc == this . timeline_parameters . timeline . length ) { loc = loc - 1 ; } return this . timeline_parameters . timeline [ loc ] . allTimelineVariablesNames ( so_far ) ; } if ( typeof this . timeline_parameters == "undefined" ) { return so_far ; } } length ( ) { var length = 0 ; if ( typeof this . timeline_parameters !== "undefined" ) { for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { length += this . timeline_parameters . timeline [ i ] . length ( ) ; } } else { return 1 ; } return length ; } percentComplete ( ) { var total_trials = this . length ( ) ; var completed_trials = 0 ; for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { if ( this . timeline_parameters . timeline [ i ] . isComplete ( ) ) { completed_trials += this . timeline_parameters . timeline [ i ] . length ( ) ; } } return ( completed_trials / total_trials ) * 100 ; } reset ( ) { this . progress . current_location = - 1 ; this . progress . current_repetition = 0 ; this . progress . current_variable_set = 0 ; this . progress . current_iteration ++ ; this . progress . done = false ; this . setTimelineVariablesOrder ( ) ; if ( typeof this . timeline_parameters != "undefined" ) { for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { this . timeline_parameters . timeline [ i ] . reset ( ) ; } } } end ( ) { this . progress . done = true ; } endActiveNode ( ) { if ( typeof this . timeline_parameters == "undefined" ) { this . end ( ) ; this . parent_node . end ( ) ; } else { this . timeline_parameters . timeline [ this . progress . current_location ] . endActiveNode ( ) ; } } ID ( ) { var id = "" ; if ( typeof this . parent_node == "undefined" ) { return "0." + this . progress . current_iteration ; } else { id += this . parent_node . ID ( ) + "-" ; id += this . relative_id + "." + this . progress . current_iteration ; return id ; } } activeID ( ) { if ( typeof this . timeline_parameters == "undefined" ) { return this . ID ( ) ; } else { return this . timeline_parameters . timeline [ this . progress . current_location ] . activeID ( ) ; } } generatedData ( ) { return this . jsPsych . data . getDataByTimelineNode ( this . ID ( ) ) ; } trialsOfType ( type ) { if ( typeof this . timeline_parameters == "undefined" ) { if ( this . trial_parameters . type == type ) { return this . trial_parameters ; } else { return [ ] ; } } else { var trials = [ ] ; for ( var i = 0 ; i < this . timeline_parameters . timeline . length ; i ++ ) { var t = this . timeline_parameters . timeline [ i ] . trialsOfType ( type ) ; trials = trials . concat ( t ) ; } return trials ; } } insert ( parameters ) { if ( typeof this . timeline_parameters === "undefined" ) { console . error ( "Cannot add new trials to a trial-level node." ) ; } else { this . timeline_parameters . timeline . push ( new TimelineNode ( this . jsPsych , { ... this . node_trial_data , ... parameters } , this , this . timeline_parameters . timeline . length ) ) ; } } }