package edu.cmu.cs.eyetrack.state; import java.awt.Color; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import org.jdesktop.core.animation.timing.Interpolator; import org.jdesktop.core.animation.timing.KeyFrames; import org.jdesktop.core.animation.timing.KeyFramesBuilder; import org.jdesktop.core.animation.timing.interpolators.LinearInterpolator; import org.jdesktop.core.animation.timing.interpolators.SplineInterpolator; import edu.cmu.cs.eyetrack.gui.shapes.Stimulus.StimulusClass; import edu.cmu.cs.eyetrack.helper.Coordinate; import edu.cmu.cs.eyetrack.helper.Util; import edu.cmu.cs.eyetrack.state.Settings.Experiment; import edu.cmu.cs.eyetrack.state.Settings.MotionConstraintType; import edu.cmu.cs.eyetrack.state.Settings.MotionInterpolationType; public class RandomGen { @SuppressWarnings("serial") private static final List<Color> colorList = new ArrayList<Color>() {{ add(Color.RED); add(Color.BLUE); add(Color.GREEN); add(Color.YELLOW); add(new Color(255, 119, 0)); //colorList.add(Color.ORANGE); add(Color.PINK); add(Color.MAGENTA); add(Color.CYAN); add(Color.GRAY); //add(Color.LIGHT_GRAY); //add(Color.DARK_GRAY); }}; private Random random; private Experiment exp; private List<Integer> gridList; private static final Interpolator SPLINE_0_0_0_0 = new SplineInterpolator(0.00, 0.00, 0.00, 0.00); private static final Interpolator SPLINE_0_0_0_1 = new SplineInterpolator(0.00, 0.00, 0.00, 1.00); private static final Interpolator SPLINE_0_0_1_1 = new SplineInterpolator(0.00, 0.00, 1.00, 1.00); private static final Interpolator SPLINE_0_1_1_1 = new SplineInterpolator(0.00, 1.00, 1.00, 1.00); private static final Interpolator SPLINE_1_1_1_1 = new SplineInterpolator(1.00, 1.00, 1.00, 1.00); private static final Interpolator SPLINE_0_0_1_0 = new SplineInterpolator(0.00, 0.00, 1.00, 0.00); private static final Interpolator SPLINE_1_0_1_1 = new SplineInterpolator(1.00, 0.00, 1.00, 1.00); private static final Interpolator[] SPLINES = { SPLINE_0_0_0_0, SPLINE_0_0_0_1, SPLINE_0_0_1_1, SPLINE_0_1_1_1, SPLINE_1_1_1_1, SPLINE_0_0_1_0, SPLINE_1_0_1_1 }; private double blockWidth; private double blockHeight; public RandomGen(Random random, Experiment exp) { this.random = random; this.exp = exp; gridList = new ArrayList<Integer>(); for(int gridIdx=0; gridIdx < (exp.getGridXSize()*exp.getGridYSize()); gridIdx++) { gridList.add(gridIdx); } blockWidth = (exp.getPixelWidth() / (double) exp.getGridXSize()); blockHeight = (exp.getPixelHeight() / (double) exp.getGridYSize()); } public List<Color> getRandomColors(int numColors) { List<Color> genList = new ArrayList<Color>(); // Colorado only ever wants gray, CMU wants rainbow colors if(exp.getStimulusClass().equals(StimulusClass.UCOLORADO)) { // Only return grays for(int idx=0; idx<numColors; idx++) { genList.add(Color.GRAY); } } else { // Return random colors from our color list (default for CMU experiments) if(numColors > colorList.size()) { Util.dPrintln("WARNING: More unique distractors than there are unique colors; will have repeats."); } Collections.shuffle(colorList, random); for(int idx=0; idx<numColors; idx++) { genList.add(colorList.get(idx % colorList.size())); } } return genList; } public List<Color> getRandomColors() { return getRandomColors( colorList.size() ); } public List<Coordinate<Integer>> genRandomGridPositions(int numPositions) { if(numPositions > gridList.size()) { Util.dPrintln("WARNING: Requesting placement for more objects than there are grid positions."); return null; } // Shuffle positions Collections.shuffle(gridList, random); // Grab numPositions List<Coordinate<Integer>> gridPos = new ArrayList<Coordinate<Integer>>(); for(int idx=0; idx<numPositions; idx++) { int rawGridIdx = gridList.get(idx); gridPos.add(rawToGrid(rawGridIdx)); } return gridPos; } // Target needs to end in each grid the same (+/- 1) number of times public List<Coordinate<Integer>> getRandomEndPositions(int numTrials) { if(numTrials > gridList.size()) { Util.dPrintln("Requesting more final positions than there are grid squares; some will repeat."); } List<Coordinate<Integer>> endingList = new ArrayList<Coordinate<Integer>>(); for(int trialIdx=0; trialIdx<numTrials; trialIdx++) { // If we've gone through an entire set of grid blocks, wrap around if(trialIdx % gridList.size() == 0) { Collections.shuffle(gridList, random); } endingList.add( rawToGrid( gridList.get(trialIdx % gridList.size() ))); } return endingList; } // Picks a new, random grid coordinate that is different from the current // grid coordinate. public Coordinate<Integer> getNextGridPos(Coordinate<Integer> currentGrid) { int newX, newY; do { newX = random.nextInt( exp.getGridXSize() ); newY = random.nextInt( exp.getGridYSize() ); } while( newX == currentGrid.getX() && newY == currentGrid.getY() ); return new Coordinate<Integer>(newX, newY); } // Picks a new pixel coordinate on the screen, different than the current pixel position public double getNextPixelPos(Coordinate<Integer> currPos, Coordinate<Integer> nextPos, int xBuffer, int yBuffer, double speed, double maxDist, boolean useCentersOnly) { int newX, newY; Rectangle2D.Double screen = new Rectangle2D.Double(exp.getInsetX() + xBuffer, exp.getInsetY() + yBuffer, exp.getPixelWidth() - 2*xBuffer, exp.getPixelHeight() - 2*yBuffer); Rectangle2D.Double neighborhood = new Rectangle2D.Double(currPos.getX() - maxDist, currPos.getY() - maxDist, 2*maxDist, 2*maxDist); Rectangle2D.intersect(screen, neighborhood, neighborhood); do { int rx, ry; boolean nearTheCenter = false; do { rx = random.nextInt( (int) neighborhood.getWidth() ); ry = random.nextInt( (int) neighborhood.getHeight() ); // If we choose a point "near" the center, roll a die to see if we want to re-calc a point nearTheCenter = (rx > 0.33*neighborhood.getWidth() && rx < 0.67*neighborhood.getWidth() && ry > 0.33*neighborhood.getHeight() && ry < 0.67*neighborhood.getHeight()); } while(nearTheCenter && random.nextInt(3) > 0); newX = (int) neighborhood.getX() + rx; newY = (int) neighborhood.getY() + ry; if(useCentersOnly) { Coordinate<Integer> gridCenter = getGridCenter(xToGrid(newX), yToGrid(newY)); newX = gridCenter.getX(); newY = gridCenter.getY(); } } while(newX == currPos.getX() && newY == currPos.getY() ); // Tell the object to move to this new random point nextPos.setX(newX); nextPos.setY(newY); // Calculate how long that will take double distanceTraveled = nextPos.distance(currPos); double timeUsed = 1000.0 * ( distanceTraveled / speed ); return timeUsed; } protected int xToGrid(double x) { return (int) ( (x - exp.getInsetX() ) / blockWidth ); } protected int yToGrid(double y) { return (int) ( (y - exp.getInsetY() ) / blockHeight); } protected Coordinate<Integer> getGridCenter(int gridX, int gridY) { return new Coordinate<Integer>( (int) (exp.getInsetX() + (gridX * blockWidth) + (0.5*blockWidth)), (int) (exp.getInsetY() + (gridY * blockHeight) + (0.5*blockHeight))); } public double calcKeyFrames(List<KeyFrames<Integer>> framesList, double minJumpMS, double maxJumpMS, double trialLength, int xBuffer, int yBuffer, Coordinate<?> startPos, Coordinate<?> endPos ) { KeyFramesBuilder<Integer> builderX = new KeyFramesBuilder<Integer>( startPos.getX().intValue() ); KeyFramesBuilder<Integer> builderY = new KeyFramesBuilder<Integer>( startPos.getY().intValue() ); // Objects have constant speed for now, measured in pixels/s // Convert maxJumpMs to the maximum distance in pixels we can travel without changing double speed = exp.getObjectSpeed(); double maxDistWithoutJump = (maxJumpMS / 1000.0) * speed; // From the starting position, add random keyframes for each of the jump times double totalTime = 0; Coordinate<Integer> currentPos = new Coordinate<Integer>(startPos.getX().intValue(), startPos.getY().intValue() ); FramePath fp = new FramePath(); do { // Pick a new random location to head toward Coordinate<Integer> nextPos = new Coordinate<Integer>(0,0); double oldTime = totalTime; // Logic for the target only---if we're planned beyond the minimum time and // the random nextBoolean fires, work toward the ending position and break out if( endPos != null && totalTime >= trialLength && random.nextInt(3) == 0 ) { // We definitely want to end at this specific location nextPos.setX(endPos.getX().intValue()); nextPos.setY(endPos.getY().intValue()); // Make sure we go there at the correct speed double distanceTraveled = currentPos.distance(nextPos); double timeUsed = 1000.0 * ( distanceTraveled / speed ); totalTime += timeUsed; fp.addFrame(nextPos, totalTime); trialLength = totalTime; break; } // Distractors and targets change trajectories only at centers of grids? boolean useCentersOnly = exp.getMotionConstraintType().equals(MotionConstraintType.GRID); double timeUsed = getNextPixelPos(currentPos, nextPos, xBuffer, yBuffer, speed, maxDistWithoutJump, useCentersOnly); totalTime += timeUsed; // Logic for the distractors only if( endPos == null) { // Keyframe's time as a fraction of total trial time double timeFrac = (double) totalTime / (double) trialLength; // If we won't make it to our end point in time, only go part-way if(timeFrac > 1.0) { double diffX = nextPos.getX() - currentPos.getX(); double diffY = nextPos.getY() - currentPos.getY(); double diffFrac = 1.0- ((timeFrac - 1.0) / ((totalTime - oldTime) / trialLength)); nextPos.setX( (int) Math.round(currentPos.getX() + (diffFrac * diffX)) ); nextPos.setY( (int) Math.round(currentPos.getY() + (diffFrac * diffY)) ); timeFrac = 1.0; } Interpolator interp; if(exp.getMotionInterpolationType().equals(MotionInterpolationType.LINEAR)) { interp = LinearInterpolator.getInstance(); } else { interp = SPLINES[random.nextInt(SPLINES.length)]; } builderX.addFrame(nextPos.getX().intValue(), timeFrac, interp); builderY.addFrame(nextPos.getY().intValue(), timeFrac, interp); } else { fp.addFrame(nextPos, totalTime); } currentPos = nextPos; } while( endPos != null || totalTime < trialLength ); if( endPos != null ) { for(int idx=0; idx<fp.getPoints().size(); idx++) { // For each recorded point, calculate the now-proper fraction of time and // add as a new frame Coordinate<Integer> nextPos = fp.getPoints().get(idx); double timeFrac = fp.getTimes().get(idx) / trialLength; Interpolator interp; if(exp.getMotionInterpolationType().equals(MotionInterpolationType.LINEAR)) { interp = LinearInterpolator.getInstance(); } else { interp = SPLINES[random.nextInt(SPLINES.length)]; } builderX.addFrame(nextPos.getX().intValue(), timeFrac, interp); builderY.addFrame(nextPos.getY().intValue(), timeFrac, interp); } } final KeyFrames<Integer> framesX = builderX.build(); final KeyFrames<Integer> framesY = builderY.build(); // Need to return keyframes for x and y framesList.add(framesX); framesList.add(framesY); return totalTime; } public static List<Color> getColorList() { return colorList; } @SuppressWarnings("unused") private Interpolator genRandomSpline() { return SPLINES[ random.nextInt(SPLINES.length) ]; } private Coordinate<Integer> rawToGrid(int rawGridIdx) { int gridX = rawGridIdx % exp.getGridXSize(); int gridY = rawGridIdx / exp.getGridXSize(); return new Coordinate<Integer>(gridX, gridY); } private class FramePath { private List<Coordinate<Integer>> points; private List<Double> times; public FramePath() { points = new ArrayList<Coordinate<Integer>>(); times = new ArrayList<Double>(); } public void addFrame(Coordinate<Integer> point, double time) { points.add(point); times.add(time); } public List<Coordinate<Integer>> getPoints() { return points; } public List<Double> getTimes() { return times; } } }