Skip to content

Commit

Permalink
UI and support for saving shape names, tasks, timestamps to a file, see
Browse files Browse the repository at this point in the history
  • Loading branch information
jessegreenberg committed May 16, 2024
1 parent d72d469 commit 52f0d14
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 4 deletions.
47 changes: 47 additions & 0 deletions js/quadrilateral/model/prototype/TangibleConnectionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import BooleanProperty from '../../../../../axon/js/BooleanProperty.js';
import Property from '../../../../../axon/js/Property.js';
import TProperty from '../../../../../axon/js/TProperty.js';
import Bounds2 from '../../../../../dot/js/Bounds2.js';
import Range from '../../../../../dot/js/Range.js';
import Vector2 from '../../../../../dot/js/Vector2.js';
import ModelViewTransform2 from '../../../../../phetcommon/js/view/ModelViewTransform2.js';
import Tandem from '../../../../../tandem/js/Tandem.js';
Expand All @@ -30,6 +31,7 @@ import QuadrilateralShapeModel, { VertexLabelToProposedPositionMap } from '../Qu
import QuadrilateralTangibleOptionsModel from './QuadrilateralTangibleOptionsModel.js';
import MarkerDetectionModel from './MarkerDetectionModel.js';
import QuadrilateralVertexLabel from '../QuadrilateralVertexLabel.js';
import NumberProperty from '../../../../../axon/js/NumberProperty.js';

export default class TangibleConnectionModel {

Expand Down Expand Up @@ -63,6 +65,14 @@ export default class TangibleConnectionModel {
// So that we can test proposed QuadrilateralVertex positions before we change the "real" shapeModel.
public readonly testShapeModel: QuadrilateralShapeModel;

// A task pointing to a particular question that will be asked in a user study.
// Mostly to annotate an entry in the data collection.
public readonly taskProperty: NumberProperty;

// Collection of data for the user study. Contains the task number, shape name, and time stamp for the data
// collection. Every time the user presses "Submit Answer", an entry will be added to this array.
private dataCollection: { task: number; shape: string; timestamp: string }[] = [];

public constructor( shapeModel: QuadrilateralShapeModel, testShapeModel: QuadrilateralShapeModel, tangibleOptionsModel: QuadrilateralTangibleOptionsModel, tandem: Tandem ) {
this.connectedToDeviceProperty = new BooleanProperty( false, {
tandem: tandem.createTandem( 'connectedToDeviceProperty' )
Expand All @@ -78,6 +88,10 @@ export default class TangibleConnectionModel {
tandem: tandem.createTandem( 'isCalibratingProperty' )
} );

this.taskProperty = new NumberProperty( 1, {
range: new Range( 0, 999 )
} );

this.markerDetectionModel = new MarkerDetectionModel( tandem.createTandem( 'markerDetectionModel' ) );
this.tangibleOptionsModel = tangibleOptionsModel;
this.shapeModel = shapeModel;
Expand Down Expand Up @@ -144,6 +158,39 @@ export default class TangibleConnectionModel {

return allowed;
}

/**
* For the 2024 user study, we want to collect the task number, shape name, and time stamp every time the
* user presses the "Submit Answer" button.
*/
public submitAnswer(): void {
this.dataCollection.push( {
task: this.taskProperty.value,
shape: this.shapeModel.shapeNameProperty.value.toString(),
timestamp: new Date().toISOString()
} );
}

/**
* Save the data collection to a file on the user's machine. Convert to a blob and then download.
*/
public saveData(): void {

// Convert the data collection to a JSON string.
const dataCollectionString = JSON.stringify( this.dataCollection );

// Create a blob from the JSON string.
const blob = new Blob( [ dataCollectionString ], { type: 'text/plain' } );

// Create a URL for the blob.
const url = URL.createObjectURL( blob );

// Create a link element and click it to download the file.
const a = document.createElement( 'a' );
a.href = url;
a.download = 'studyData.json';
a.click();
}
}

quadrilateral.register( 'TangibleConnectionModel', TangibleConnectionModel );
13 changes: 10 additions & 3 deletions js/quadrilateral/view/QuadrilateralScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import QuadrilateralTangibleControls from './prototype/QuadrilateralTangibleCont
import QuadrilateralModelViewTransform from './QuadrilateralModelViewTransform.js';
import QuadrilateralTangibleController from './prototype/QuadrilateralTangibleController.js';
import { SpeakableResolvedResponse } from '../../../../utterance-queue/js/ResponsePacket.js';
import QuadrilateralDataControls from './prototype/QuadrilateralDataControls.js';

export default class QuadrilateralScreenView extends ScreenView {
private readonly model: QuadrilateralModel;
Expand Down Expand Up @@ -167,6 +168,9 @@ export default class QuadrilateralScreenView extends ScreenView {
deviceConnectionParentNode.left = resetAllButton.left;
}

// Controls specific to a 2024 user study.
const dataControls = new QuadrilateralDataControls( model.tangibleConnectionModel );

// voicing components
this.quadrilateralAlerter = new QuadrilateralAlerter( model, this, this.modelViewTransform, this.quadrilateralDescriber );

Expand All @@ -189,7 +193,8 @@ export default class QuadrilateralScreenView extends ScreenView {
shapeSoundsCheckbox,
shapeNameDisplay,
resetShapeButton,
deviceConnectionParentNode
deviceConnectionParentNode,
dataControls
];

//---------------------------------------------------------------------------------------------------------------
Expand All @@ -201,7 +206,7 @@ export default class QuadrilateralScreenView extends ScreenView {
this.layoutBounds.maxY - QuadrilateralConstants.SCREEN_VIEW_Y_MARGIN
);
smallStepsLockToggleButton.leftBottom = resetAllButton.leftTop.minusXY( 0, QuadrilateralConstants.VIEW_GROUP_SPACING );
visibilityControls.leftCenter = gridNode.rightCenter.plusXY( QuadrilateralConstants.VIEW_SPACING, 0 );
visibilityControls.leftCenter = gridNode.rightCenter.plusXY( QuadrilateralConstants.VIEW_SPACING, 20 );

shapeNameDisplay.centerBottom = gridNode.centerTop.minusXY( 0, QuadrilateralConstants.VIEW_SPACING );
shapeSoundsCheckbox.rightCenter = new Vector2( gridNode.right, shapeNameDisplay.centerY );
Expand All @@ -214,10 +219,12 @@ export default class QuadrilateralScreenView extends ScreenView {
);
} );

deviceConnectionParentNode.leftBottom = visibilityControls.leftTop.minusXY( 0, QuadrilateralConstants.VIEW_GROUP_SPACING );
deviceConnectionParentNode.leftBottom = visibilityControls.leftTop.minusXY( 0, QuadrilateralConstants.VIEW_GROUP_SPACING / 2 );

debugValuesPanel.leftTop = gridNode.leftTop.plusXY( 5, 5 );

dataControls.leftTop = new Vector2( deviceConnectionParentNode.left, this.layoutBounds.top + 5 );

//---------------------------------------------------------------------------------------------------------------
// Traversal order
//---------------------------------------------------------------------------------------------------------------
Expand Down
58 changes: 58 additions & 0 deletions js/quadrilateral/view/prototype/QuadrilateralDataControls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2024, University of Colorado Boulder

/**
* A set of controls specific to the 2024 user study that has students set a task number, submit an answer, and then
* save data to a local file on their machine.
*
* @author Jesse Greenberg (PhET Interactive Simulations)
*/

import { HBox, Text, VBox } from '../../../../../scenery/js/imports.js';
import QuadrilateralConstants from '../../../QuadrilateralConstants.js';
import TextPushButton from '../../../../../sun/js/buttons/TextPushButton.js';
import QuadrilateralColors from '../../../QuadrilateralColors.js';
import TangibleConnectionModel from '../../model/prototype/TangibleConnectionModel.js';
import quadrilateral from '../../../quadrilateral.js';
import NumberPicker from '../../../../../sun/js/NumberPicker.js';

export default class QuadrilateralDataControls extends VBox {
public constructor( tangibleConnectionModel: TangibleConnectionModel ) {

// A 'task' spinner that will be used to set the task value for data collection.
const taskText = new Text( 'Task', QuadrilateralConstants.PANEL_TITLE_TEXT_OPTIONS );
const numberSpinner = new NumberPicker( tangibleConnectionModel.taskProperty, tangibleConnectionModel.taskProperty.rangeProperty, {
color: QuadrilateralColors.screenViewButtonColorProperty
} );
const spinnerComponent = new HBox( {
children: [ taskText, numberSpinner ],
spacing: 10
} );

// A 'submit' answer button that will save an answer for this task.
const submitButton = new TextPushButton( 'Submit Answer', {
listener: () => {
tangibleConnectionModel.submitAnswer();
},

textNodeOptions: QuadrilateralConstants.SCREEN_TEXT_OPTIONS,
baseColor: QuadrilateralColors.screenViewButtonColorProperty
} );

const saveButton = new TextPushButton( 'Save Data', {
listener: () => {
tangibleConnectionModel.saveData();
},

textNodeOptions: QuadrilateralConstants.SCREEN_TEXT_OPTIONS,
baseColor: QuadrilateralColors.screenViewButtonColorProperty
} );

super( {
children: [ spinnerComponent, submitButton, saveButton ],
spacing: QuadrilateralConstants.CONTROLS_SPACING / 2,
align: 'right'
} );
}
}

quadrilateral.register( 'QuadrilateralDataControls', QuadrilateralDataControls );
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quadrilateral",
"version": "1.1.3",
"version": "1.1.0-study2024.0",
"license": "GPL-3.0",
"repository": {
"type": "git",
Expand Down

0 comments on commit 52f0d14

Please sign in to comment.